diff --git a/src/game/GamePage.tsx b/src/game/GamePage.tsx index 3a245977..95855d2d 100644 --- a/src/game/GamePage.tsx +++ b/src/game/GamePage.tsx @@ -12,7 +12,7 @@ import { GameWarmup, GameEmergencyStop, GameSettings, - GameVibrator, + GameToyClient, } from './components'; import { GameProvider } from './GameProvider'; @@ -89,7 +89,7 @@ export const GamePage = () => { - + diff --git a/src/game/components/GameToyClient.tsx b/src/game/components/GameToyClient.tsx new file mode 100644 index 00000000..61a009b2 --- /dev/null +++ b/src/game/components/GameToyClient.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; +import { GamePhase, useGameValue } from '../GameProvider'; +import { useAutoRef } from '../../utils'; +import { useToyClientValue } from '../../toy'; + +export const GameToyClient = () => { + const [stroke] = useGameValue('stroke'); + const [intensity] = useGameValue('intensity'); + const [pace] = useGameValue('pace'); + const [phase] = useGameValue('phase'); + const [devices] = useToyClientValue('devices'); + + const data = useAutoRef({ + intensity, + pace, + devices, + }); + + const [currentPhase, setCurrentPhase] = useState(phase); + + useEffect(() => { + const { intensity, pace, devices } = data.current; + devices.forEach(device => { + device.actuate(stroke, intensity, pace); + }); + }, [data, stroke]); + + useEffect(() => { + const { devices } = data.current; + if (currentPhase == phase) return; + switch (phase) { + case GamePhase.pause: + case GamePhase.break: + devices.forEach(device => device.stop()); + break; + case GamePhase.climax: + devices.forEach(device => device.climax()); + } + setCurrentPhase(phase); + }, [data, currentPhase, phase]); + + return null; +}; diff --git a/src/game/components/GameVibrator.tsx b/src/game/components/GameVibrator.tsx deleted file mode 100644 index 7ed42337..00000000 --- a/src/game/components/GameVibrator.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect, useState } from 'react'; -import { GamePhase, Stroke, useGameValue } from '../GameProvider'; -import { useAutoRef, useVibratorValue, VibrationMode, wait } from '../../utils'; -import { useSetting } from '../../settings'; - -export const GameVibrator = () => { - const [stroke] = useGameValue('stroke'); - const [intensity] = useGameValue('intensity'); - const [pace] = useGameValue('pace'); - const [phase] = useGameValue('phase'); - const [mode] = useSetting('vibrations'); - const [devices] = useVibratorValue('devices'); - - const data = useAutoRef({ - intensity, - pace, - devices, - mode, - }); - - const [currentPhase, setCurrentPhase] = useState(phase); - - useEffect(() => { - const { intensity, pace, devices, mode } = data.current; - switch (stroke) { - case Stroke.up: - switch (mode) { - case VibrationMode.constant: { - const strength = intensity / 100; - devices.forEach(device => device.setVibration(strength)); - break; - } - case VibrationMode.thump: { - const length = (1 / pace) * 1000; - const strength = Math.max(0.25, intensity / 100); - devices.forEach(device => device.thump(length, strength)); - break; - } - } - break; - case Stroke.down: - break; - } - }, [data, stroke]); - - useEffect(() => { - const { devices, mode } = data.current; - if (currentPhase == phase) return; - if ([GamePhase.break, GamePhase.pause].includes(phase)) { - devices.forEach(device => device.setVibration(0)); - } - if (phase === GamePhase.climax) { - (async () => { - for (let i = 0; i < 15; i++) { - const strength = Math.max(0, 1 - i * 0.067); - switch (mode) { - case VibrationMode.constant: - devices.forEach(device => device.setVibration(strength)); - break; - case VibrationMode.thump: - devices.forEach(device => device.thump(400, strength)); - break; - } - await wait(400); - } - })(); - } - setCurrentPhase(phase); - }, [data, currentPhase, phase]); - - return null; -}; diff --git a/src/game/components/index.ts b/src/game/components/index.ts index 4cb0ba1e..85a504e0 100644 --- a/src/game/components/index.ts +++ b/src/game/components/index.ts @@ -10,5 +10,5 @@ export * from './GameMeter'; export * from './GamePace'; export * from './GameSettings'; export * from './GameSound'; -export * from './GameVibrator'; +export * from './GameToyClient'; export * from './GameWarmup'; diff --git a/src/index.tsx b/src/index.tsx index 6273059e..2030b7cb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,7 @@ import { App } from './app/App.tsx'; import './index.css'; import { SettingsProvider, ImageProvider } from './settings'; import { E621Provider } from './e621'; -import { VibratorProvider } from './utils'; +import { ToyClientProvider } from './toy'; import { LocalImageProvider } from './local/LocalProvider.tsx'; ReactDOM.createRoot(document.getElementById('root')!).render( @@ -12,11 +12,11 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - + - + diff --git a/src/settings/SettingsProvider.tsx b/src/settings/SettingsProvider.tsx index 64816ec0..14aa216b 100644 --- a/src/settings/SettingsProvider.tsx +++ b/src/settings/SettingsProvider.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { GameEvent, GameHypnoType, PlayerBody, PlayerGender } from '../types'; -import { createLocalStorageProvider, VibrationMode } from '../utils'; +import { createLocalStorageProvider } from '../utils'; import { interpolateWith } from '../utils/translate'; export interface Settings { @@ -18,7 +18,6 @@ export interface Settings { body: PlayerBody; highRes: boolean; videoSound: boolean; - vibrations: VibrationMode; } export const defaultSettings: Settings = { @@ -36,7 +35,6 @@ export const defaultSettings: Settings = { body: PlayerBody.penis, highRes: false, videoSound: false, - vibrations: VibrationMode.thump, }; const settingsStorageKey = 'settings'; diff --git a/src/settings/components/ToyActuatorSettings.tsx b/src/settings/components/ToyActuatorSettings.tsx new file mode 100644 index 00000000..1920fb63 --- /dev/null +++ b/src/settings/components/ToyActuatorSettings.tsx @@ -0,0 +1,110 @@ +import { + ToyActuator, + VibrationActuator, + ActuatorMode, + ActuatorModeLabels, +} from '../../toy'; +import { PropsWithChildren, useState } from 'react'; +import { SettingsTile } from '../../common'; +import { SettingsDescription } from '../../common/SettingsDescription'; +import { Dropdown } from '../../common/Dropdown'; +import { ActuatorType } from 'buttplug'; +import { Space } from '../../common/Space'; +import { SettingsLabel } from '../../common/SettingsLabel'; + +export interface ToySettingsProps + extends PropsWithChildren> { + toyActuator: ToyActuator; +} + +export const ToyActuatorSettings: React.FC = ({ + toyActuator, +}) => { + return ( + + {(() => { + switch (toyActuator.actuatorType) { + case ActuatorType.Vibrate: + return ; + default: + return ; + } + })()} + + ); +}; + +export const VibratorActuatorSettings: React.FC = ({ + toyActuator, +}) => { + const vibratorActuator = toyActuator as VibrationActuator; + const descriptor = `${vibratorActuator.actuatorType}_${vibratorActuator.index}`; + const [mode, setMode] = useState(vibratorActuator.mode); + const [min, setMin] = useState(vibratorActuator.minIntensity); + const [max, setMax] = useState(vibratorActuator.maxIntensity); + + return ( +
+ + Select when this component will activate. + + { + const newMode = value as ActuatorMode; + setMode(newMode); + vibratorActuator.setMode(newMode); + }} + options={Object.values(ActuatorMode).map(value => ({ + value, + label: ActuatorModeLabels[value], + }))} + /> + + + Change the range of intensity for this component. + + From + { + const newMin = +value; + if (newMin > vibratorActuator.maxIntensity) return; + setMin(newMin); + vibratorActuator.setMinIntensity(newMin); + }} + options={vibratorActuator.intensityRange.map(value => ({ + value: `${value}`, + label: `${(value * 100).toFixed(0)}%`, + }))} + /> + To + { + const newMax = +value; + if (newMax < vibratorActuator.minIntensity) return; + setMax(newMax); + vibratorActuator.setMaxIntensity(newMax); + }} + options={vibratorActuator.intensityRange.map(value => ({ + value: `${value}`, + label: `${(value * 100).toFixed(0)}%`, + }))} + /> +
+ ); +}; + +export const UnknownActuatorSettings = () => { + return ( + + This component is not currently supported. + + ); +}; diff --git a/src/settings/components/VibratorSettings.tsx b/src/settings/components/ToyClientSettings.tsx similarity index 81% rename from src/settings/components/VibratorSettings.tsx rename to src/settings/components/ToyClientSettings.tsx index 89e0216b..fb74cda9 100644 --- a/src/settings/components/VibratorSettings.tsx +++ b/src/settings/components/ToyClientSettings.tsx @@ -11,15 +11,18 @@ import { SettingsLabel, Spinner, } from '../../common'; -import { defaultTransition, useVibratorValue, Vibrator } from '../../utils'; +import { defaultTransition } from '../../utils'; +import { useToyClientValue, ToyClient } from '../../toy'; import { ButtplugBrowserWebsocketClientConnector, ButtplugClientDevice, + ButtplugClient, } from 'buttplug'; import styled from 'styled-components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGear } from '@fortawesome/free-solid-svg-icons'; import { AnimatePresence, motion } from 'framer-motion'; +import { ToySettings } from './ToySettings'; const StyledDeviceList = styled.ul` list-style: none; @@ -40,10 +43,10 @@ const StyledUrlFields = styled.div` `; export const VibratorSettings = () => { - const [client] = useVibratorValue('client'); - const [connection, setConnection] = useVibratorValue('connection'); - const [devices, setDevices] = useVibratorValue('devices'); - const [error, setError] = useVibratorValue('error'); + const [client, setClient] = useToyClientValue('client'); + const [connection, setConnection] = useToyClientValue('connection'); + const [devices, setDevices] = useToyClientValue('devices'); + const [error, setError] = useToyClientValue('error'); const [loading, setLoading] = useState(false); const [host, setHost] = useState('127.0.0.1'); @@ -59,6 +62,7 @@ export const VibratorSettings = () => { if (connection) { try { await client.disconnect(); + setClient(new ButtplugClient('JOI.how')); } catch (e) { setError(String(e)); } finally { @@ -73,15 +77,18 @@ export const VibratorSettings = () => { await client.connect(new ButtplugBrowserWebsocketClientConnector(url)); await client.startScanning(); client.devices.forEach(e => - setDevices(devices => [...devices, new Vibrator(e)]) + setDevices(devices => [...devices, new ToyClient(e)]) ); client.addListener('deviceadded', (device: ButtplugClientDevice) => { - setDevices(devices => [...devices, new Vibrator(device)]); + setDevices(devices => [...devices, new ToyClient(device)]); }); client.addListener('deviceremoved', (device: ButtplugClientDevice) => { setDevices(devices => devices.filter(e => e.name !== device.name)); }); client.addListener('disconnect', () => { + client.removeListener('deviceadded'); + client.removeListener('deviceremoved'); + client.removeListener('disconnect'); setDevices([]); setConnection(undefined); }); @@ -92,10 +99,19 @@ export const VibratorSettings = () => { } finally { setLoading(false); } - }, [client, connection, host, port, setConnection, setDevices, setError]); + }, [ + client, + connection, + host, + port, + setClient, + setConnection, + setDevices, + setError, + ]); return ( - + { } > - Use compatible device during your game + Use compatible devices during your game { <> {devices.length > 0 ? ( - devices.map((device, index) =>
  • {device.name}
  • ) + devices.map((device: ToyClient, index: number) => ( + + )) ) : (
  • No devices found
  • )} diff --git a/src/settings/components/ToySettings.tsx b/src/settings/components/ToySettings.tsx new file mode 100644 index 00000000..0cd6f430 --- /dev/null +++ b/src/settings/components/ToySettings.tsx @@ -0,0 +1,25 @@ +import { ToyClient, type ToyActuator } from '../../toy'; +import { ToyActuatorSettings } from './ToyActuatorSettings'; +import { SettingsTile } from '../../common'; +import { PropsWithChildren } from 'react'; +import { SettingsDescription } from '../../common/SettingsDescription'; + +export interface ToySettingsProps + extends PropsWithChildren> { + device: ToyClient; +} + +export const ToySettings: React.FC = ({ device }) => { + return ( +
  • + + + Change the settings for each controllable component on your toy. + + {device.actuators.map((a: ToyActuator) => ( + + ))} + +
  • + ); +}; diff --git a/src/settings/components/index.ts b/src/settings/components/index.ts index bf45d051..6ad87f60 100644 --- a/src/settings/components/index.ts +++ b/src/settings/components/index.ts @@ -8,5 +8,6 @@ export * from './PaceSettings'; export * from './PlayerSettings'; export * from './ServiceSettings'; export * from './TradeSettings'; -export * from './VibratorSettings'; +export * from './ToyClientSettings'; export * from './WalltakerSettings'; +export * from './ToySettings'; diff --git a/src/toy/index.ts b/src/toy/index.ts new file mode 100644 index 00000000..79cc0468 --- /dev/null +++ b/src/toy/index.ts @@ -0,0 +1,2 @@ +export * from './toyactuator'; +export * from './toyclient'; diff --git a/src/toy/toyactuator.tsx b/src/toy/toyactuator.tsx new file mode 100644 index 00000000..55d2ae14 --- /dev/null +++ b/src/toy/toyactuator.tsx @@ -0,0 +1,97 @@ +import { type GenericDeviceMessageAttributes, ActuatorType } from 'buttplug'; +import { Stroke } from '../game/GameProvider'; + +export enum ActuatorMode { + alwaysOn = 'alwaysOn', + alwaysOff = 'alwaysOff', + activeUp = 'activeUp', + activeDown = 'activeDown', +} + +export const ActuatorModeLabels: Record = { + [ActuatorMode.alwaysOn]: 'Always On', + [ActuatorMode.alwaysOff]: 'Always Off', + [ActuatorMode.activeUp]: 'Active on Upstroke', + [ActuatorMode.activeDown]: 'Active on Downstroke', +}; + +export class ToyActuator implements ToyActuatorSettings { + index: number; + actuatorType: ActuatorType; + + constructor(attributes: GenericDeviceMessageAttributes) { + this.actuatorType = attributes.ActuatorType; + this.index = attributes.Index; + } + + getOutput?(stroke: Stroke, intensity: number, pace: number): unknown; +} + +export class VibrationActuator + extends ToyActuator + implements VibrationActuatorSettings +{ + mode: ActuatorMode = ActuatorMode.activeUp; + minIntensity: number = 0; + maxIntensity: number = 1.0; + intensityRange: number[] = []; + + constructor(attributes: GenericDeviceMessageAttributes) { + super(attributes); + const intensityStep = 100 / attributes.StepCount / 100.0; + for (let i = 0; i <= attributes.StepCount; ++i) { + this.intensityRange[i] = intensityStep * i; + } + } + + setMode(newMode: ActuatorMode) { + this.mode = newMode; + } + + setMinIntensity(newMin: number) { + this.minIntensity = newMin; + } + + setMaxIntensity(newMax: number) { + this.maxIntensity = newMax; + } + + override getOutput(stroke: Stroke, intensity: number) { + let output = this.minIntensity; + switch (this.mode) { + case ActuatorMode.activeUp: + if (stroke == Stroke.up) { + output = this.mapToRange(intensity / 100); + } + break; + case ActuatorMode.activeDown: + if (stroke == Stroke.down) { + output = this.mapToRange(intensity / 100); + } + break; + case ActuatorMode.alwaysOn: + output = this.mapToRange(intensity / 100); + break; + case ActuatorMode.alwaysOff: + output = 0; + break; + } + return output; + } + + mapToRange(input: number): number { + const slope = this.maxIntensity - this.minIntensity; + return this.minIntensity + slope * input; + } +} + +export interface VibrationActuatorSettings { + mode: ActuatorMode; + minIntensity: number; + maxIntensity: number; +} + +export interface ToyActuatorSettings { + index: number; + actuatorType: ActuatorType; +} diff --git a/src/toy/toyclient.tsx b/src/toy/toyclient.tsx new file mode 100644 index 00000000..959b5870 --- /dev/null +++ b/src/toy/toyclient.tsx @@ -0,0 +1,100 @@ +import { + ButtplugClient, + type ButtplugClientDevice, + ActuatorType, +} from 'buttplug'; +import { ToyActuator, VibrationActuator } from './toyactuator'; +import { Stroke } from '../game/GameProvider'; +import { wait, createStateProvider } from '../utils'; + +export class ToyClient { + actuators: ToyActuator[] = []; + + constructor(private readonly device: ButtplugClientDevice) { + device.vibrateAttributes.forEach( + attribute => + (this.actuators = [...this.actuators, new VibrationActuator(attribute)]) + ); + device.linearAttributes.forEach( + attribute => + (this.actuators = [...this.actuators, new ToyActuator(attribute)]) + ); + device.oscillateAttributes.forEach( + attribute => + (this.actuators = [...this.actuators, new ToyActuator(attribute)]) + ); + device.rotateAttributes.forEach( + attribute => + (this.actuators = [...this.actuators, new ToyActuator(attribute)]) + ); + } + + async actuate(stroke: Stroke, intensity: number, pace: number) { + const vibrationArray: number[] = []; + this.actuators.forEach(actuator => { + switch (actuator.actuatorType) { + case ActuatorType.Vibrate: + vibrationArray[actuator.index] = actuator.getOutput?.( + stroke, + intensity, + pace + ) as number; + break; + default: + break; + } + }); + if (vibrationArray.length > 0) { + this.device.vibrate(vibrationArray); + } + } + + async climax() { + for (let i = 0; i < 15; i++) { + const strength = Math.max(0, 1 - i * 0.067); + const vibrationArray: number[] = []; + this.actuators.forEach(actuator => { + switch (actuator.actuatorType) { + case ActuatorType.Vibrate: { + const vibrationActuator = actuator as VibrationActuator; + vibrationArray[actuator.index] = + vibrationActuator.mapToRange(strength); + break; + } + default: + break; + } + if (vibrationArray.length > 0) { + this.device.vibrate(vibrationArray); + } + }); + await wait(400); + } + } + + async stop() { + await this.device.stop(); + } + + get name(): string { + return this.device.name; + } +} + +export interface ToyClientSettings { + client: ButtplugClient; + connection?: string; + devices: ToyClient[]; + error?: string; +} + +export const { + Provider: ToyClientProvider, + useProvider: useToyClients, + useProviderSelector: useToyClientValue, +} = createStateProvider({ + defaultData: { + client: new ButtplugClient('JOI.how'), + devices: [], + }, +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index c0c1952b..e1171d74 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,5 +11,4 @@ export * from './setters'; export * from './sound'; export * from './state'; export * from './translate'; -export * from './vibrator'; export * from './wait'; diff --git a/src/utils/vibrator.tsx b/src/utils/vibrator.tsx deleted file mode 100644 index fe17d2d9..00000000 --- a/src/utils/vibrator.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { createStateProvider } from './state'; -import { ButtplugClient, type ButtplugClientDevice } from 'buttplug'; -import { wait } from './wait'; - -export enum VibrationMode { - constant = 'constant', - thump = 'thump', -} - -export class Vibrator { - private commandId = 0; - - constructor(private readonly device: ButtplugClientDevice) {} - - async setVibration(intensity: number): Promise { - ++this.commandId; - await this.device.vibrate(intensity); - } - - async thump(timeout: number, intensity = 1): Promise { - const id = ++this.commandId; - await this.device.vibrate(intensity); - await wait(timeout); - if (id === this.commandId) { - await this.device.stop(); - } - } - - get name(): string { - return this.device.name; - } -} - -export interface VibratorSettings { - client: ButtplugClient; - connection?: string; - devices: Vibrator[]; - error?: string; -} - -export const { - Provider: VibratorProvider, - useProvider: useVibrators, - useProviderSelector: useVibratorValue, -} = createStateProvider({ - defaultData: { - client: new ButtplugClient('JOI.how'), - devices: [], - }, -});