From 34818d30f5a548f6f2aa5f0d4188c8c0c63804f0 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 8 Mar 2026 20:31:35 -0400 Subject: [PATCH] Move piano glue behind behavior registry --- client/src/input/keyboardController.ts | 9 ++-- client/src/items/types/behaviorRegistry.ts | 43 +++++++++++----- client/src/items/types/piano/behavior.ts | 59 +++++++++++++--------- client/src/items/types/runtimeShared.ts | 7 +-- client/src/main.ts | 25 ++++----- client/src/network/messageHandlers.ts | 11 ++-- 6 files changed, 89 insertions(+), 65 deletions(-) diff --git a/client/src/input/keyboardController.ts b/client/src/input/keyboardController.ts index 3d6a20f..760653d 100644 --- a/client/src/input/keyboardController.ts +++ b/client/src/input/keyboardController.ts @@ -19,8 +19,8 @@ type KeyboardControllerDeps = { handleModeInput: (input: ModeInput) => void; canOpenCommandPaletteInMode: (mode: GameMode) => boolean; openCommandPalette: () => void; - shouldForwardModeKeyUp: (mode: GameMode) => boolean; - onModeKeyUp: (input: Pick) => void; + getModeKeyUpTarget: (activeMode: GameMode) => GameMode | null; + onModeKeyUp: (mode: GameMode, input: Pick) => void; pasteIntoActiveTextInput: (text: string) => boolean; updateStatus: (message: string) => void; setReplaceTextOnNextType: (value: boolean) => void; @@ -157,8 +157,9 @@ export function setupKeyboardInputHandlers(deps: KeyboardControllerDeps): void { document.addEventListener('keyup', (event) => { const code = normalizeInputCode(event); - if (code && deps.shouldForwardModeKeyUp(deps.state.mode)) { - deps.onModeKeyUp({ + const keyUpMode = deps.getModeKeyUpTarget(deps.state.mode); + if (code && keyUpMode) { + deps.onModeKeyUp(keyUpMode, { code, shiftKey: event.shiftKey, }); diff --git a/client/src/items/types/behaviorRegistry.ts b/client/src/items/types/behaviorRegistry.ts index 3832315..b876c4f 100644 --- a/client/src/items/types/behaviorRegistry.ts +++ b/client/src/items/types/behaviorRegistry.ts @@ -77,6 +77,27 @@ export class ItemBehaviorRegistry { return false; } + /** Returns whether any item-owned mode supports opening the command palette. */ + canOpenModeCommandPalette(mode: GameMode): boolean { + for (const behavior of this.behaviors) { + if (behavior.canOpenModeCommandPalette?.(mode)) { + return true; + } + } + return false; + } + + /** Resolves an optional suspended mode that still wants key-up events while another overlay is active. */ + getModeKeyUpTarget(activeMode: GameMode, returnMode: GameMode): GameMode | null { + for (const behavior of this.behaviors) { + const target = behavior.getModeKeyUpTarget?.(activeMode, returnMode); + if (target) { + return target; + } + } + return null; + } + /** Returns palette-visible commands for the active item-owned mode, if any. */ getModeCommands(mode: GameMode): CommandDescriptor[] { const commands: CommandDescriptor[] = []; @@ -99,24 +120,20 @@ export class ItemBehaviorRegistry { return false; } - /** Routes incoming item-piano-note packets to the item behavior owning that protocol. */ - onRemotePianoNote(message: Extract): void { + /** Gives item behaviors a chance to consume custom incoming packets. */ + onIncomingMessage(message: IncomingMessage): boolean { for (const behavior of this.behaviors) { - behavior.onRemotePianoNote?.(message); + if (behavior.onIncomingMessage?.(message)) { + return true; + } } + return false; } - /** Routes incoming item-piano-status packets to behavior modules that track piano runtime state. */ - onPianoStatus(message: Extract): void { + /** Notifies behaviors that a peer left so they can release sender-owned runtime state. */ + onPeerLeft(senderId: string): void { for (const behavior of this.behaviors) { - behavior.onPianoStatus?.(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); + behavior.onPeerLeft?.(senderId); } } } diff --git a/client/src/items/types/piano/behavior.ts b/client/src/items/types/piano/behavior.ts index d64b531..bc4a798 100644 --- a/client/src/items/types/piano/behavior.ts +++ b/client/src/items/types/piano/behavior.ts @@ -31,9 +31,6 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior { } return true; }, - onPianoStatus: (message) => { - controller.onPianoStatus(message); - }, onPropertyPreviewChange: (item, key, value) => { controller.onPreviewPropertyChange(item, key, value); }, @@ -50,6 +47,12 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior { controller.handleModeKeyUp(input); return true; }, + canOpenModeCommandPalette: (mode) => mode === 'pianoUse', + getModeKeyUpTarget: (activeMode, returnMode) => { + if (activeMode === 'pianoUse') return 'pianoUse'; + if (activeMode === 'commandPalette' && returnMode === 'pianoUse') return 'pianoUse'; + return null; + }, getModeCommands: (mode) => { if (mode !== 'pianoUse') return []; return controller.getModeCommands(); @@ -58,29 +61,37 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior { if (mode !== 'pianoUse') return false; return controller.runModeCommand(commandId); }, - 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); + onIncomingMessage: (message) => { + if (message.type === 'item_piano_note') { + 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); + } + return true; } + if (message.type === 'item_piano_status') { + controller.onPianoStatus(message); + return true; + } + return false; }, - onStopAllRemoteNotesForSender: (senderId) => { + onPeerLeft: (senderId) => { controller.stopAllRemoteNotesForSender(senderId); }, }; diff --git a/client/src/items/types/runtimeShared.ts b/client/src/items/types/runtimeShared.ts index 5caa777..2ea5b72 100644 --- a/client/src/items/types/runtimeShared.ts +++ b/client/src/items/types/runtimeShared.ts @@ -32,9 +32,10 @@ export type ItemBehavior = { onWorldUpdate?: () => void; handleModeInput?: (mode: GameMode, input: ModeInput) => boolean; handleModeKeyUp?: (mode: GameMode, input: Pick) => boolean; + canOpenModeCommandPalette?: (mode: GameMode) => boolean; + getModeKeyUpTarget?: (activeMode: GameMode, returnMode: GameMode) => GameMode | null; getModeCommands?: (mode: GameMode) => CommandDescriptor[]; runModeCommand?: (mode: GameMode, commandId: string) => boolean; - onRemotePianoNote?: (message: Extract) => void; - onPianoStatus?: (message: Extract) => void; - onStopAllRemoteNotesForSender?: (senderId: string) => void; + onIncomingMessage?: (message: IncomingMessage) => boolean; + onPeerLeft?: (senderId: string) => void; }; diff --git a/client/src/main.ts b/client/src/main.ts index e3f437a..fc60e04 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1483,9 +1483,8 @@ const onAppMessage = createOnMessageHandler({ ); }, handleItemActionResultStatus: (message) => itemBehaviorRegistry.onActionResultStatus(message), - handleRemotePianoNote: (message) => itemBehaviorRegistry.onRemotePianoNote(message), - handlePianoStatus: (message) => itemBehaviorRegistry.onPianoStatus(message), - stopAllRemoteNotesForSender: (senderId) => itemBehaviorRegistry.stopAllRemoteNotesForSender(senderId), + handleItemBehaviorIncomingMessage: (message) => itemBehaviorRegistry.onIncomingMessage(message), + handleItemBehaviorPeerLeft: (senderId) => itemBehaviorRegistry.onPeerLeft(senderId), TELEPORT_SOUND_URL, TELEPORT_START_SOUND_URL, getAudioLayers: () => audioLayers, @@ -2034,7 +2033,7 @@ function getAvailableCommandPaletteEntriesForMode(mode: GameMode): Array ({ ...descriptor, run: () => { @@ -2046,11 +2045,7 @@ function getAvailableCommandPaletteEntriesForMode(mode: GameMode): Array handleMicGainEditModeInput(currentCode, currentKey, currentCtrlKey), - pianoUse: (currentInput) => { - itemBehaviorRegistry.handleModeInput(state.mode, currentInput); - }, commandPalette: ({ code: currentCode, key: currentKey }) => handleCommandPaletteModeInput(currentCode, currentKey), effectSelect: ({ code: currentCode, key: currentKey }) => handleEffectSelectModeInput(currentCode, currentKey), helpView: ({ code: currentCode }) => handleHelpViewModeInput(currentCode), @@ -2617,9 +2612,9 @@ setupKeyboardInputHandlers({ handleModeInput, canOpenCommandPaletteInMode, openCommandPalette, - shouldForwardModeKeyUp: shouldForwardPianoKeyUp, - onModeKeyUp: ({ code, shiftKey }) => { - itemBehaviorRegistry.handleModeKeyUp('pianoUse', { + getModeKeyUpTarget: (activeMode) => itemBehaviorRegistry.getModeKeyUpTarget(activeMode, commandPaletteReturnMode), + onModeKeyUp: (mode, { code, shiftKey }) => { + itemBehaviorRegistry.handleModeKeyUp(mode, { code, shiftKey, }); diff --git a/client/src/network/messageHandlers.ts b/client/src/network/messageHandlers.ts index da8ee4b..37f5f9f 100644 --- a/client/src/network/messageHandlers.ts +++ b/client/src/network/messageHandlers.ts @@ -46,9 +46,8 @@ type MessageHandlerDeps = { randomFootstepUrl: () => string; playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void; handleItemActionResultStatus: (message: Extract) => boolean; - handleRemotePianoNote: (message: Extract) => void; - handlePianoStatus: (message: Extract) => void; - stopAllRemoteNotesForSender: (senderId: string) => void; + handleItemBehaviorIncomingMessage: (message: IncomingMessage) => boolean; + handleItemBehaviorPeerLeft: (senderId: string) => void; TELEPORT_SOUND_URL: string; TELEPORT_START_SOUND_URL: string; getAudioLayers: () => { world: boolean; item: boolean }; @@ -225,7 +224,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco if (peer) { deps.updateStatus(`${peer.nickname} has left.`); } - deps.stopAllRemoteNotesForSender(message.id); + deps.handleItemBehaviorPeerLeft(message.id); deps.state.peers.delete(message.id); deps.peerManager.removePeer(message.id); break; @@ -335,7 +334,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco case 'item_piano_note': { if (!deps.getAudioLayers().item) break; - deps.handleRemotePianoNote(message); + deps.handleItemBehaviorIncomingMessage(message); break; } @@ -346,7 +345,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco } case 'item_piano_status': { - deps.handlePianoStatus(message); + deps.handleItemBehaviorIncomingMessage(message); break; } }