Move piano glue behind behavior registry

This commit is contained in:
Jage9
2026-03-08 20:31:35 -04:00
parent ffba9530c8
commit 34818d30f5
6 changed files with 89 additions and 65 deletions

View File

@@ -19,8 +19,8 @@ type KeyboardControllerDeps = {
handleModeInput: (input: ModeInput) => void; handleModeInput: (input: ModeInput) => void;
canOpenCommandPaletteInMode: (mode: GameMode) => boolean; canOpenCommandPaletteInMode: (mode: GameMode) => boolean;
openCommandPalette: () => void; openCommandPalette: () => void;
shouldForwardModeKeyUp: (mode: GameMode) => boolean; getModeKeyUpTarget: (activeMode: GameMode) => GameMode | null;
onModeKeyUp: (input: Pick<ModeInput, 'code' | 'shiftKey'>) => void; onModeKeyUp: (mode: GameMode, input: Pick<ModeInput, 'code' | 'shiftKey'>) => void;
pasteIntoActiveTextInput: (text: string) => boolean; pasteIntoActiveTextInput: (text: string) => boolean;
updateStatus: (message: string) => void; updateStatus: (message: string) => void;
setReplaceTextOnNextType: (value: boolean) => void; setReplaceTextOnNextType: (value: boolean) => void;
@@ -157,8 +157,9 @@ export function setupKeyboardInputHandlers(deps: KeyboardControllerDeps): void {
document.addEventListener('keyup', (event) => { document.addEventListener('keyup', (event) => {
const code = normalizeInputCode(event); const code = normalizeInputCode(event);
if (code && deps.shouldForwardModeKeyUp(deps.state.mode)) { const keyUpMode = deps.getModeKeyUpTarget(deps.state.mode);
deps.onModeKeyUp({ if (code && keyUpMode) {
deps.onModeKeyUp(keyUpMode, {
code, code,
shiftKey: event.shiftKey, shiftKey: event.shiftKey,
}); });

View File

@@ -77,6 +77,27 @@ export class ItemBehaviorRegistry {
return false; 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. */ /** Returns palette-visible commands for the active item-owned mode, if any. */
getModeCommands(mode: GameMode): CommandDescriptor[] { getModeCommands(mode: GameMode): CommandDescriptor[] {
const commands: CommandDescriptor[] = []; const commands: CommandDescriptor[] = [];
@@ -99,24 +120,20 @@ export class ItemBehaviorRegistry {
return false; return false;
} }
/** Routes incoming item-piano-note packets to the item behavior owning that protocol. */ /** Gives item behaviors a chance to consume custom incoming packets. */
onRemotePianoNote(message: Extract<IncomingMessage, { type: 'item_piano_note' }>): void { onIncomingMessage(message: IncomingMessage): boolean {
for (const behavior of this.behaviors) { 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. */ /** Notifies behaviors that a peer left so they can release sender-owned runtime state. */
onPianoStatus(message: Extract<IncomingMessage, { type: 'item_piano_status' }>): void { onPeerLeft(senderId: string): void {
for (const behavior of this.behaviors) { for (const behavior of this.behaviors) {
behavior.onPianoStatus?.(message); behavior.onPeerLeft?.(senderId);
}
}
/** 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);
} }
} }
} }

View File

@@ -31,9 +31,6 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
} }
return true; return true;
}, },
onPianoStatus: (message) => {
controller.onPianoStatus(message);
},
onPropertyPreviewChange: (item, key, value) => { onPropertyPreviewChange: (item, key, value) => {
controller.onPreviewPropertyChange(item, key, value); controller.onPreviewPropertyChange(item, key, value);
}, },
@@ -50,6 +47,12 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
controller.handleModeKeyUp(input); controller.handleModeKeyUp(input);
return true; 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) => { getModeCommands: (mode) => {
if (mode !== 'pianoUse') return []; if (mode !== 'pianoUse') return [];
return controller.getModeCommands(); return controller.getModeCommands();
@@ -58,29 +61,37 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
if (mode !== 'pianoUse') return false; if (mode !== 'pianoUse') return false;
return controller.runModeCommand(commandId); return controller.runModeCommand(commandId);
}, },
onRemotePianoNote: (message) => { onIncomingMessage: (message) => {
if (message.on) { if (message.type === 'item_piano_note') {
controller.playRemoteNote({ if (message.on) {
itemId: message.itemId, controller.playRemoteNote({
senderId: message.senderId, itemId: message.itemId,
keyId: message.keyId, senderId: message.senderId,
midi: message.midi, keyId: message.keyId,
instrument: message.instrument, midi: message.midi,
voiceMode: message.voiceMode, instrument: message.instrument,
octave: message.octave, voiceMode: message.voiceMode,
attack: message.attack, octave: message.octave,
decay: message.decay, attack: message.attack,
release: message.release, decay: message.decay,
brightness: message.brightness, release: message.release,
x: message.x, brightness: message.brightness,
y: message.y, x: message.x,
emitRange: message.emitRange, y: message.y,
}); emitRange: message.emitRange,
} else { });
controller.stopRemoteNote(message.senderId, message.keyId); } 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); controller.stopAllRemoteNotesForSender(senderId);
}, },
}; };

View File

@@ -32,9 +32,10 @@ export type ItemBehavior = {
onWorldUpdate?: () => void; onWorldUpdate?: () => void;
handleModeInput?: (mode: GameMode, input: ModeInput) => boolean; handleModeInput?: (mode: GameMode, input: ModeInput) => boolean;
handleModeKeyUp?: (mode: GameMode, input: Pick<ModeInput, 'code' | 'shiftKey'>) => boolean; handleModeKeyUp?: (mode: GameMode, input: Pick<ModeInput, 'code' | 'shiftKey'>) => boolean;
canOpenModeCommandPalette?: (mode: GameMode) => boolean;
getModeKeyUpTarget?: (activeMode: GameMode, returnMode: GameMode) => GameMode | null;
getModeCommands?: (mode: GameMode) => CommandDescriptor[]; getModeCommands?: (mode: GameMode) => CommandDescriptor[];
runModeCommand?: (mode: GameMode, commandId: string) => boolean; runModeCommand?: (mode: GameMode, commandId: string) => boolean;
onRemotePianoNote?: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void; onIncomingMessage?: (message: IncomingMessage) => boolean;
onPianoStatus?: (message: Extract<IncomingMessage, { type: 'item_piano_status' }>) => void; onPeerLeft?: (senderId: string) => void;
onStopAllRemoteNotesForSender?: (senderId: string) => void;
}; };

View File

@@ -1483,9 +1483,8 @@ const onAppMessage = createOnMessageHandler({
); );
}, },
handleItemActionResultStatus: (message) => itemBehaviorRegistry.onActionResultStatus(message), handleItemActionResultStatus: (message) => itemBehaviorRegistry.onActionResultStatus(message),
handleRemotePianoNote: (message) => itemBehaviorRegistry.onRemotePianoNote(message), handleItemBehaviorIncomingMessage: (message) => itemBehaviorRegistry.onIncomingMessage(message),
handlePianoStatus: (message) => itemBehaviorRegistry.onPianoStatus(message), handleItemBehaviorPeerLeft: (senderId) => itemBehaviorRegistry.onPeerLeft(senderId),
stopAllRemoteNotesForSender: (senderId) => itemBehaviorRegistry.stopAllRemoteNotesForSender(senderId),
TELEPORT_SOUND_URL, TELEPORT_SOUND_URL,
TELEPORT_START_SOUND_URL, TELEPORT_START_SOUND_URL,
getAudioLayers: () => audioLayers, getAudioLayers: () => audioLayers,
@@ -2034,7 +2033,7 @@ function getAvailableCommandPaletteEntriesForMode(mode: GameMode): Array<Command
run: mainModeCommandHandlers[descriptor.id], run: mainModeCommandHandlers[descriptor.id],
})); }));
} }
if (mode === 'pianoUse') { if (itemBehaviorRegistry.canOpenModeCommandPalette(mode)) {
return itemBehaviorRegistry.getModeCommands(mode).map((descriptor) => ({ return itemBehaviorRegistry.getModeCommands(mode).map((descriptor) => ({
...descriptor, ...descriptor,
run: () => { run: () => {
@@ -2046,11 +2045,7 @@ function getAvailableCommandPaletteEntriesForMode(mode: GameMode): Array<Command
} }
function canOpenCommandPaletteInMode(mode: GameMode): boolean { function canOpenCommandPaletteInMode(mode: GameMode): boolean {
return mode === 'normal' || mode === 'pianoUse' || mode === 'commandPalette'; return mode === 'normal' || mode === 'commandPalette' || itemBehaviorRegistry.canOpenModeCommandPalette(mode);
}
function shouldForwardPianoKeyUp(currentMode: GameMode): boolean {
return currentMode === 'pianoUse' || (currentMode === 'commandPalette' && commandPaletteReturnMode === 'pianoUse');
} }
function openCommandPalette(): void { function openCommandPalette(): void {
@@ -2536,6 +2531,9 @@ function handleNicknameModeInput(code: string, key: string, ctrlKey: boolean): v
} }
function handleModeInput(input: ModeInput): void { function handleModeInput(input: ModeInput): void {
if (itemBehaviorRegistry.handleModeInput(state.mode, input)) {
return;
}
dispatchModeInput({ dispatchModeInput({
mode: state.mode, mode: state.mode,
input, input,
@@ -2546,9 +2544,6 @@ function handleModeInput(input: ModeInput): void {
handleChatModeInput(currentCode, currentKey, currentCtrlKey), handleChatModeInput(currentCode, currentKey, currentCtrlKey),
micGainEdit: ({ code: currentCode, key: currentKey, ctrlKey: currentCtrlKey }) => micGainEdit: ({ code: currentCode, key: currentKey, ctrlKey: currentCtrlKey }) =>
handleMicGainEditModeInput(currentCode, currentKey, currentCtrlKey), handleMicGainEditModeInput(currentCode, currentKey, currentCtrlKey),
pianoUse: (currentInput) => {
itemBehaviorRegistry.handleModeInput(state.mode, currentInput);
},
commandPalette: ({ code: currentCode, key: currentKey }) => handleCommandPaletteModeInput(currentCode, currentKey), commandPalette: ({ code: currentCode, key: currentKey }) => handleCommandPaletteModeInput(currentCode, currentKey),
effectSelect: ({ code: currentCode, key: currentKey }) => handleEffectSelectModeInput(currentCode, currentKey), effectSelect: ({ code: currentCode, key: currentKey }) => handleEffectSelectModeInput(currentCode, currentKey),
helpView: ({ code: currentCode }) => handleHelpViewModeInput(currentCode), helpView: ({ code: currentCode }) => handleHelpViewModeInput(currentCode),
@@ -2617,9 +2612,9 @@ setupKeyboardInputHandlers({
handleModeInput, handleModeInput,
canOpenCommandPaletteInMode, canOpenCommandPaletteInMode,
openCommandPalette, openCommandPalette,
shouldForwardModeKeyUp: shouldForwardPianoKeyUp, getModeKeyUpTarget: (activeMode) => itemBehaviorRegistry.getModeKeyUpTarget(activeMode, commandPaletteReturnMode),
onModeKeyUp: ({ code, shiftKey }) => { onModeKeyUp: (mode, { code, shiftKey }) => {
itemBehaviorRegistry.handleModeKeyUp('pianoUse', { itemBehaviorRegistry.handleModeKeyUp(mode, {
code, code,
shiftKey, shiftKey,
}); });

View File

@@ -46,9 +46,8 @@ type MessageHandlerDeps = {
randomFootstepUrl: () => string; randomFootstepUrl: () => string;
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void; playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
handleItemActionResultStatus: (message: Extract<IncomingMessage, { type: 'item_action_result' }>) => boolean; handleItemActionResultStatus: (message: Extract<IncomingMessage, { type: 'item_action_result' }>) => boolean;
handleRemotePianoNote: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void; handleItemBehaviorIncomingMessage: (message: IncomingMessage) => boolean;
handlePianoStatus: (message: Extract<IncomingMessage, { type: 'item_piano_status' }>) => void; handleItemBehaviorPeerLeft: (senderId: string) => void;
stopAllRemoteNotesForSender: (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 };
@@ -225,7 +224,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.stopAllRemoteNotesForSender(message.id); deps.handleItemBehaviorPeerLeft(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;
@@ -335,7 +334,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;
deps.handleRemotePianoNote(message); deps.handleItemBehaviorIncomingMessage(message);
break; break;
} }
@@ -346,7 +345,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
} }
case 'item_piano_status': { case 'item_piano_status': {
deps.handlePianoStatus(message); deps.handleItemBehaviorIncomingMessage(message);
break; break;
} }
} }