Add per-item behavior registry for client item runtime
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.24 R225";
|
window.CHGRID_WEB_VERSION = "2026.02.24 R226";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
104
client/src/items/types/behaviorRegistry.ts
Normal file
104
client/src/items/types/behaviorRegistry.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { type IncomingMessage } from '../../network/protocol';
|
||||||
|
import { type GameMode, type WorldItem } from '../../state/gameState';
|
||||||
|
import { createClockBehavior } from './clock/behavior';
|
||||||
|
import { createDiceBehavior } from './dice/behavior';
|
||||||
|
import { createPianoBehavior } from './piano/behavior';
|
||||||
|
import { createRadioStationBehavior } from './radioStation/behavior';
|
||||||
|
import { type ItemBehavior, type ItemBehaviorDeps } from './runtimeShared';
|
||||||
|
import { createWheelBehavior } from './wheel/behavior';
|
||||||
|
import { createWidgetBehavior } from './widget/behavior';
|
||||||
|
|
||||||
|
/** Runtime registry that composes all per-item client behavior modules. */
|
||||||
|
export class ItemBehaviorRegistry {
|
||||||
|
private readonly behaviors: ItemBehavior[];
|
||||||
|
|
||||||
|
constructor(deps: ItemBehaviorDeps) {
|
||||||
|
this.behaviors = [
|
||||||
|
createClockBehavior(deps),
|
||||||
|
createDiceBehavior(deps),
|
||||||
|
createPianoBehavior(deps),
|
||||||
|
createRadioStationBehavior(deps),
|
||||||
|
createWheelBehavior(deps),
|
||||||
|
createWidgetBehavior(deps),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs per-item initialization hooks after app bootstrap. */
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
await behavior.onInit?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs all per-item teardown hooks during disconnect/reset flows. */
|
||||||
|
cleanup(): void {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
behavior.onCleanup?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forwards incoming messages to behavior-specific use-result hooks. */
|
||||||
|
onUseResultMessage(message: IncomingMessage): void {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
behavior.onUseResultMessage?.(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lets item behaviors consume custom action-result status handling. */
|
||||||
|
onActionResultStatus(message: Extract<IncomingMessage, { type: 'item_action_result' }>): boolean {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
if (behavior.onActionResultStatus?.(message)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs per-item world-update hooks after state changes. */
|
||||||
|
onWorldUpdate(): void {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
behavior.onWorldUpdate?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Routes property preview changes into per-item behavior hooks. */
|
||||||
|
onPropertyPreviewChange(item: WorldItem, key: string, value: unknown): void {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
behavior.onPropertyPreviewChange?.(item, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gives item behaviors first chance to handle mode input. */
|
||||||
|
handleModeInput(mode: GameMode, code: string): boolean {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
if (behavior.handleModeInput?.(mode, code)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gives item behaviors first chance to handle mode key-up events. */
|
||||||
|
handleModeKeyUp(mode: GameMode, code: string): boolean {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
if (behavior.handleModeKeyUp?.(mode, code)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Routes incoming item-piano-note packets to the item behavior owning that protocol. */
|
||||||
|
onRemotePianoNote(message: Extract<IncomingMessage, { type: 'item_piano_note' }>): void {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
behavior.onRemotePianoNote?.(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops all remote notes for one sender across behavior modules that own remote note runtimes. */
|
||||||
|
stopAllRemoteNotesForSender(senderId: string): void {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
behavior.onStopAllRemoteNotesForSender?.(senderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
client/src/items/types/clock/behavior.ts
Normal file
7
client/src/items/types/clock/behavior.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { type ItemBehavior, type ItemBehaviorDeps } from '../runtimeShared';
|
||||||
|
|
||||||
|
/** Creates runtime behavior hooks for clock items. */
|
||||||
|
export function createClockBehavior(_deps: ItemBehaviorDeps): ItemBehavior {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
7
client/src/items/types/dice/behavior.ts
Normal file
7
client/src/items/types/dice/behavior.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { type ItemBehavior, type ItemBehaviorDeps } from '../runtimeShared';
|
||||||
|
|
||||||
|
/** Creates runtime behavior hooks for dice items. */
|
||||||
|
export function createDiceBehavior(_deps: ItemBehaviorDeps): ItemBehavior {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
102
client/src/items/types/piano/behavior.ts
Normal file
102
client/src/items/types/piano/behavior.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { type ItemBehavior, type ItemBehaviorDeps } from '../runtimeShared';
|
||||||
|
import { PianoController } from './runtime';
|
||||||
|
|
||||||
|
/** Creates runtime behavior hooks for piano items. */
|
||||||
|
export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
|
||||||
|
const controller = new PianoController({
|
||||||
|
state: deps.state,
|
||||||
|
audio: deps.audio,
|
||||||
|
signalingSend: deps.signalingSend,
|
||||||
|
updateStatus: deps.updateStatus,
|
||||||
|
openHelpViewer: deps.openHelpViewer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusMessages = new Set([
|
||||||
|
'record',
|
||||||
|
'pause',
|
||||||
|
'resume',
|
||||||
|
'play',
|
||||||
|
'stop',
|
||||||
|
'No recording saved on this piano.',
|
||||||
|
'Stop recording before playback.',
|
||||||
|
'This piano is already recording.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onInit: async () => {
|
||||||
|
await controller.loadHelpFromUrl(deps.withBase('piano.json'));
|
||||||
|
await controller.loadDemoFromUrl(deps.withBase('piano_demo.json'));
|
||||||
|
},
|
||||||
|
onCleanup: () => {
|
||||||
|
controller.cleanup();
|
||||||
|
},
|
||||||
|
onUseResultMessage: (message) => {
|
||||||
|
controller.onUseResultMessage(message);
|
||||||
|
if (
|
||||||
|
message.type === 'item_action_result' &&
|
||||||
|
message.ok &&
|
||||||
|
message.action === 'use' &&
|
||||||
|
typeof message.itemId === 'string' &&
|
||||||
|
typeof message.message === 'string' &&
|
||||||
|
message.message.toLowerCase().includes('begin playing')
|
||||||
|
) {
|
||||||
|
const item = deps.state.items.get(message.itemId);
|
||||||
|
if (item?.type === 'piano') {
|
||||||
|
void controller.startUseMode(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onActionResultStatus: (message) => {
|
||||||
|
if (message.action !== 'use') return false;
|
||||||
|
if (!statusMessages.has(message.message)) return false;
|
||||||
|
deps.updateStatus(message.message);
|
||||||
|
if (message.ok) {
|
||||||
|
deps.audio.sfxUiBlip();
|
||||||
|
} else {
|
||||||
|
deps.audio.sfxUiCancel();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onPropertyPreviewChange: (item, key, value) => {
|
||||||
|
controller.onPreviewPropertyChange(item, key, value);
|
||||||
|
},
|
||||||
|
onWorldUpdate: () => {
|
||||||
|
controller.syncAfterWorldUpdate();
|
||||||
|
},
|
||||||
|
handleModeInput: (mode, code) => {
|
||||||
|
if (mode !== 'pianoUse') return false;
|
||||||
|
controller.handleModeInput(code);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
handleModeKeyUp: (mode, code) => {
|
||||||
|
if (mode !== 'pianoUse') return false;
|
||||||
|
controller.handleModeKeyUp(code);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onRemotePianoNote: (message) => {
|
||||||
|
if (message.on) {
|
||||||
|
controller.playRemoteNote({
|
||||||
|
itemId: message.itemId,
|
||||||
|
senderId: message.senderId,
|
||||||
|
keyId: message.keyId,
|
||||||
|
midi: message.midi,
|
||||||
|
instrument: message.instrument,
|
||||||
|
voiceMode: message.voiceMode,
|
||||||
|
octave: message.octave,
|
||||||
|
attack: message.attack,
|
||||||
|
decay: message.decay,
|
||||||
|
release: message.release,
|
||||||
|
brightness: message.brightness,
|
||||||
|
x: message.x,
|
||||||
|
y: message.y,
|
||||||
|
emitRange: message.emitRange,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
controller.stopRemoteNote(message.senderId, message.keyId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStopAllRemoteNotesForSender: (senderId) => {
|
||||||
|
controller.stopAllRemoteNotesForSender(senderId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export { pianoDefinition } from './definition';
|
export { pianoDefinition } from './definition';
|
||||||
export { PianoController } from './runtime';
|
export { createPianoBehavior } from './behavior';
|
||||||
|
|
||||||
|
|||||||
7
client/src/items/types/radioStation/behavior.ts
Normal file
7
client/src/items/types/radioStation/behavior.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { type ItemBehavior, type ItemBehaviorDeps } from '../runtimeShared';
|
||||||
|
|
||||||
|
/** Creates runtime behavior hooks for radio_station items. */
|
||||||
|
export function createRadioStationBehavior(_deps: ItemBehaviorDeps): ItemBehavior {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
36
client/src/items/types/runtimeShared.ts
Normal file
36
client/src/items/types/runtimeShared.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { type IncomingMessage, type OutgoingMessage } from '../../network/protocol';
|
||||||
|
import { type GameMode, type WorldItem } from '../../state/gameState';
|
||||||
|
|
||||||
|
/** Shared dependencies made available to all client item behavior modules. */
|
||||||
|
export type ItemBehaviorDeps = {
|
||||||
|
state: {
|
||||||
|
mode: GameMode;
|
||||||
|
items: Map<string, WorldItem>;
|
||||||
|
player: { id: string | null; x: number; y: number };
|
||||||
|
};
|
||||||
|
audio: {
|
||||||
|
ensureContext: () => Promise<void>;
|
||||||
|
context: AudioContext | null;
|
||||||
|
getOutputDestinationNode: () => AudioNode | null;
|
||||||
|
sfxUiBlip: () => void;
|
||||||
|
sfxUiCancel: () => void;
|
||||||
|
};
|
||||||
|
signalingSend: (message: OutgoingMessage) => void;
|
||||||
|
updateStatus: (message: string) => void;
|
||||||
|
openHelpViewer: (lines: string[], returnMode: GameMode) => void;
|
||||||
|
withBase: (path: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Optional per-item behavior hooks used by the client runtime. */
|
||||||
|
export type ItemBehavior = {
|
||||||
|
onInit?: () => void | Promise<void>;
|
||||||
|
onCleanup?: () => void;
|
||||||
|
onUseResultMessage?: (message: IncomingMessage) => void;
|
||||||
|
onActionResultStatus?: (message: Extract<IncomingMessage, { type: 'item_action_result' }>) => boolean;
|
||||||
|
onPropertyPreviewChange?: (item: WorldItem, key: string, value: unknown) => void;
|
||||||
|
onWorldUpdate?: () => void;
|
||||||
|
handleModeInput?: (mode: GameMode, code: string) => boolean;
|
||||||
|
handleModeKeyUp?: (mode: GameMode, code: string) => boolean;
|
||||||
|
onRemotePianoNote?: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
||||||
|
onStopAllRemoteNotesForSender?: (senderId: string) => void;
|
||||||
|
};
|
||||||
7
client/src/items/types/wheel/behavior.ts
Normal file
7
client/src/items/types/wheel/behavior.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { type ItemBehavior, type ItemBehaviorDeps } from '../runtimeShared';
|
||||||
|
|
||||||
|
/** Creates runtime behavior hooks for wheel items. */
|
||||||
|
export function createWheelBehavior(_deps: ItemBehaviorDeps): ItemBehavior {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
7
client/src/items/types/widget/behavior.ts
Normal file
7
client/src/items/types/widget/behavior.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { type ItemBehavior, type ItemBehaviorDeps } from '../runtimeShared';
|
||||||
|
|
||||||
|
/** Creates runtime behavior hooks for widget items. */
|
||||||
|
export function createWidgetBehavior(_deps: ItemBehaviorDeps): ItemBehavior {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ import {
|
|||||||
} from './items/itemRegistry';
|
} from './items/itemRegistry';
|
||||||
import { createItemPropertyEditor } from './items/itemPropertyEditor';
|
import { createItemPropertyEditor } from './items/itemPropertyEditor';
|
||||||
import { createItemPropertyPresentation } from './items/itemPropertyPresentation';
|
import { createItemPropertyPresentation } from './items/itemPropertyPresentation';
|
||||||
import { PianoController } from './items/types/piano';
|
import { ItemBehaviorRegistry } from './items/types/behaviorRegistry';
|
||||||
import { NICKNAME_STORAGE_KEY, SettingsStore } from './settings/settingsStore';
|
import { NICKNAME_STORAGE_KEY, SettingsStore } from './settings/settingsStore';
|
||||||
import { runConnectFlow, runDisconnectFlow, type ConnectFlowDeps } from './session/connectionFlow';
|
import { runConnectFlow, runDisconnectFlow, type ConnectFlowDeps } from './session/connectionFlow';
|
||||||
import { MediaSession } from './session/mediaSession';
|
import { MediaSession } from './session/mediaSession';
|
||||||
@@ -281,12 +281,13 @@ const mediaSession = new MediaSession({
|
|||||||
micInputGainStep: MIC_INPUT_GAIN_STEP,
|
micInputGainStep: MIC_INPUT_GAIN_STEP,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pianoController = new PianoController({
|
const itemBehaviorRegistry = new ItemBehaviorRegistry({
|
||||||
state,
|
state,
|
||||||
audio,
|
audio,
|
||||||
signalingSend: (message) => signaling.send(message),
|
signalingSend: (message) => signaling.send(message),
|
||||||
updateStatus,
|
updateStatus,
|
||||||
openHelpViewer: (lines, returnMode) => openHelpViewer(lines, returnMode),
|
openHelpViewer: (lines, returnMode) => openHelpViewer(lines, returnMode),
|
||||||
|
withBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.setOutputMode(outputMode);
|
audio.setOutputMode(outputMode);
|
||||||
@@ -296,8 +297,7 @@ loadAudioLayerState();
|
|||||||
loadMicInputGain();
|
loadMicInputGain();
|
||||||
loadMasterVolume();
|
loadMasterVolume();
|
||||||
void loadHelp();
|
void loadHelp();
|
||||||
void pianoController.loadHelpFromUrl(withBase('piano.json'));
|
void itemBehaviorRegistry.initialize();
|
||||||
void pianoController.loadDemoFromUrl(withBase('piano_demo.json'));
|
|
||||||
void loadChangelog();
|
void loadChangelog();
|
||||||
|
|
||||||
/** Fetches a required DOM element and casts it to the requested element type. */
|
/** Fetches a required DOM element and casts it to the requested element type. */
|
||||||
@@ -1308,7 +1308,7 @@ function disconnect(): void {
|
|||||||
lastSubscriptionRefreshTileY = Math.round(state.player.y);
|
lastSubscriptionRefreshTileY = Math.round(state.player.y);
|
||||||
stopTeleportLoopAudio();
|
stopTeleportLoopAudio();
|
||||||
activeTeleport = null;
|
activeTeleport = null;
|
||||||
pianoController.cleanup();
|
itemBehaviorRegistry.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
const onAppMessage = createOnMessageHandler({
|
const onAppMessage = createOnMessageHandler({
|
||||||
@@ -1344,9 +1344,9 @@ const onAppMessage = createOnMessageHandler({
|
|||||||
gain,
|
gain,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
playRemotePianoNote: (note) => pianoController.playRemoteNote(note),
|
handleItemActionResultStatus: (message) => itemBehaviorRegistry.onActionResultStatus(message),
|
||||||
stopRemotePianoNote: (senderId, keyId) => pianoController.stopRemoteNote(senderId, keyId),
|
handleRemotePianoNote: (message) => itemBehaviorRegistry.onRemotePianoNote(message),
|
||||||
stopAllRemotePianoNotesForSender: (senderId) => pianoController.stopAllRemoteNotesForSender(senderId),
|
stopAllRemoteNotesForSender: (senderId) => itemBehaviorRegistry.stopAllRemoteNotesForSender(senderId),
|
||||||
TELEPORT_SOUND_URL,
|
TELEPORT_SOUND_URL,
|
||||||
TELEPORT_START_SOUND_URL,
|
TELEPORT_START_SOUND_URL,
|
||||||
getAudioLayers: () => audioLayers,
|
getAudioLayers: () => audioLayers,
|
||||||
@@ -1408,21 +1408,8 @@ async function onSignalingMessage(message: IncomingMessage): Promise<void> {
|
|||||||
startHeartbeat();
|
startHeartbeat();
|
||||||
}
|
}
|
||||||
await onAppMessage(message);
|
await onAppMessage(message);
|
||||||
pianoController.onUseResultMessage(message);
|
itemBehaviorRegistry.onUseResultMessage(message);
|
||||||
if (
|
itemBehaviorRegistry.onWorldUpdate();
|
||||||
message.type === 'item_action_result' &&
|
|
||||||
message.ok &&
|
|
||||||
message.action === 'use' &&
|
|
||||||
typeof message.itemId === 'string' &&
|
|
||||||
typeof message.message === 'string' &&
|
|
||||||
message.message.toLowerCase().includes('begin playing')
|
|
||||||
) {
|
|
||||||
const item = state.items.get(message.itemId);
|
|
||||||
if (item?.type === 'piano') {
|
|
||||||
await pianoController.startUseMode(item.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pianoController.syncAfterWorldUpdate();
|
|
||||||
applyConfiguredPeerListenGains();
|
applyConfiguredPeerListenGains();
|
||||||
if (restartAnnouncement) {
|
if (restartAnnouncement) {
|
||||||
setConnectionStatus(restartAnnouncement);
|
setConnectionStatus(restartAnnouncement);
|
||||||
@@ -2145,7 +2132,7 @@ const itemPropertyEditor = createItemPropertyEditor({
|
|||||||
suppressItemPropertyEchoUntilMs = Math.max(suppressItemPropertyEchoUntilMs, Date.now() + Math.max(0, ms));
|
suppressItemPropertyEchoUntilMs = Math.max(suppressItemPropertyEchoUntilMs, Date.now() + Math.max(0, ms));
|
||||||
},
|
},
|
||||||
onPreviewPropertyChange: (item, key, value) => {
|
onPreviewPropertyChange: (item, key, value) => {
|
||||||
pianoController.onPreviewPropertyChange(item, key, value);
|
itemBehaviorRegistry.onPropertyPreviewChange(item, key, value);
|
||||||
},
|
},
|
||||||
updateStatus,
|
updateStatus,
|
||||||
sfxUiBlip: () => audio.sfxUiBlip(),
|
sfxUiBlip: () => audio.sfxUiBlip(),
|
||||||
@@ -2299,7 +2286,9 @@ function setupInputHandlers(): void {
|
|||||||
nickname: handleNicknameModeInput,
|
nickname: handleNicknameModeInput,
|
||||||
chat: handleChatModeInput,
|
chat: handleChatModeInput,
|
||||||
micGainEdit: handleMicGainEditModeInput,
|
micGainEdit: handleMicGainEditModeInput,
|
||||||
pianoUse: (currentCode) => pianoController.handleModeInput(currentCode),
|
pianoUse: (currentCode) => {
|
||||||
|
itemBehaviorRegistry.handleModeInput(state.mode, currentCode);
|
||||||
|
},
|
||||||
effectSelect: (currentCode, currentKey) => handleEffectSelectModeInput(currentCode, currentKey),
|
effectSelect: (currentCode, currentKey) => handleEffectSelectModeInput(currentCode, currentKey),
|
||||||
helpView: (currentCode) => handleHelpViewModeInput(currentCode),
|
helpView: (currentCode) => handleHelpViewModeInput(currentCode),
|
||||||
listUsers: (currentCode, currentKey) => handleListModeInput(currentCode, currentKey),
|
listUsers: (currentCode, currentKey) => handleListModeInput(currentCode, currentKey),
|
||||||
@@ -2321,7 +2310,7 @@ function setupInputHandlers(): void {
|
|||||||
document.addEventListener('keyup', (event) => {
|
document.addEventListener('keyup', (event) => {
|
||||||
const code = normalizeInputCode(event);
|
const code = normalizeInputCode(event);
|
||||||
if (state.mode === 'pianoUse' && code) {
|
if (state.mode === 'pianoUse' && code) {
|
||||||
pianoController.handleModeKeyUp(code);
|
itemBehaviorRegistry.handleModeKeyUp(state.mode, code);
|
||||||
}
|
}
|
||||||
if (code) {
|
if (code) {
|
||||||
state.keysPressed[code] = false;
|
state.keysPressed[code] = false;
|
||||||
|
|||||||
@@ -46,24 +46,9 @@ type MessageHandlerDeps = {
|
|||||||
sanitizeName: (value: string) => string;
|
sanitizeName: (value: string) => string;
|
||||||
randomFootstepUrl: () => string;
|
randomFootstepUrl: () => string;
|
||||||
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
|
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
|
||||||
playRemotePianoNote: (note: {
|
handleItemActionResultStatus: (message: Extract<IncomingMessage, { type: 'item_action_result' }>) => boolean;
|
||||||
itemId: string;
|
handleRemotePianoNote: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
||||||
senderId: string;
|
stopAllRemoteNotesForSender: (senderId: string) => void;
|
||||||
keyId: string;
|
|
||||||
midi: number;
|
|
||||||
instrument: string;
|
|
||||||
voiceMode: 'mono' | 'poly';
|
|
||||||
octave: number;
|
|
||||||
attack: number;
|
|
||||||
decay: number;
|
|
||||||
release: number;
|
|
||||||
brightness: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
emitRange: number;
|
|
||||||
}) => void;
|
|
||||||
stopRemotePianoNote: (senderId: string, keyId: string) => void;
|
|
||||||
stopAllRemotePianoNotesForSender: (senderId: string) => void;
|
|
||||||
TELEPORT_SOUND_URL: string;
|
TELEPORT_SOUND_URL: string;
|
||||||
TELEPORT_START_SOUND_URL: string;
|
TELEPORT_START_SOUND_URL: string;
|
||||||
getAudioLayers: () => { world: boolean; item: boolean };
|
getAudioLayers: () => { world: boolean; item: boolean };
|
||||||
@@ -177,7 +162,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
if (peer) {
|
if (peer) {
|
||||||
deps.updateStatus(`${peer.nickname} has left.`);
|
deps.updateStatus(`${peer.nickname} has left.`);
|
||||||
}
|
}
|
||||||
deps.stopAllRemotePianoNotesForSender(message.id);
|
deps.stopAllRemoteNotesForSender(message.id);
|
||||||
deps.state.peers.delete(message.id);
|
deps.state.peers.delete(message.id);
|
||||||
deps.peerManager.removePeer(message.id);
|
deps.peerManager.removePeer(message.id);
|
||||||
break;
|
break;
|
||||||
@@ -241,23 +226,12 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'item_action_result': {
|
case 'item_action_result': {
|
||||||
const pianoStatusMessages = new Set([
|
const handledByItemBehavior = deps.handleItemActionResultStatus(message);
|
||||||
'record',
|
if (handledByItemBehavior) {
|
||||||
'pause',
|
|
||||||
'resume',
|
|
||||||
'play',
|
|
||||||
'stop',
|
|
||||||
'No recording saved on this piano.',
|
|
||||||
'Stop recording before playback.',
|
|
||||||
'This piano is already recording.',
|
|
||||||
]);
|
|
||||||
if (message.ok) {
|
|
||||||
if (message.action === 'use') {
|
|
||||||
if (pianoStatusMessages.has(message.message)) {
|
|
||||||
deps.updateStatus(message.message);
|
|
||||||
deps.audioUiBlip();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (message.ok) {
|
||||||
|
if (message.action === 'use') {
|
||||||
deps.pushChatMessage(message.message);
|
deps.pushChatMessage(message.message);
|
||||||
const item = message.itemId ? deps.getItemById(message.itemId) : null;
|
const item = message.itemId ? deps.getItemById(message.itemId) : null;
|
||||||
if (!item?.useSound && item && item.type !== 'piano') {
|
if (!item?.useSound && item && item.type !== 'piano') {
|
||||||
@@ -268,11 +242,6 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
deps.audioUiConfirm();
|
deps.audioUiConfirm();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (message.action === 'use' && pianoStatusMessages.has(message.message)) {
|
|
||||||
deps.updateStatus(message.message);
|
|
||||||
deps.audioUiCancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
deps.pushChatMessage(message.message);
|
deps.pushChatMessage(message.message);
|
||||||
deps.audioUiCancel();
|
deps.audioUiCancel();
|
||||||
}
|
}
|
||||||
@@ -290,26 +259,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
|
|
||||||
case 'item_piano_note': {
|
case 'item_piano_note': {
|
||||||
if (!deps.getAudioLayers().item) break;
|
if (!deps.getAudioLayers().item) break;
|
||||||
if (message.on) {
|
deps.handleRemotePianoNote(message);
|
||||||
deps.playRemotePianoNote({
|
|
||||||
itemId: message.itemId,
|
|
||||||
senderId: message.senderId,
|
|
||||||
keyId: message.keyId,
|
|
||||||
midi: message.midi,
|
|
||||||
instrument: message.instrument,
|
|
||||||
voiceMode: message.voiceMode,
|
|
||||||
octave: message.octave,
|
|
||||||
attack: message.attack,
|
|
||||||
decay: message.decay,
|
|
||||||
release: message.release,
|
|
||||||
brightness: message.brightness,
|
|
||||||
x: message.x,
|
|
||||||
y: message.y,
|
|
||||||
emitRange: message.emitRange,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deps.stopRemotePianoNote(message.senderId, message.keyId);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user