= ({
+ 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: [],
- },
-});