Add context-aware command palette
This commit is contained in:
19
client/src/input/commandTypes.ts
Normal file
19
client/src/input/commandTypes.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type ModeInput = {
|
||||
code: string;
|
||||
key: string;
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
};
|
||||
|
||||
export type CommandDescriptor<CommandId extends string = string> = {
|
||||
id: CommandId;
|
||||
label: string;
|
||||
shortcut: string;
|
||||
tooltip: string;
|
||||
section: string;
|
||||
};
|
||||
|
||||
/** Formats a palette/menu label as `Name: Key`. */
|
||||
export function formatCommandMenuLabel(command: Pick<CommandDescriptor, 'label' | 'shortcut'>): string {
|
||||
return `${command.label}: ${command.shortcut}`;
|
||||
}
|
||||
@@ -22,12 +22,15 @@ export type MainModeCommand =
|
||||
| 'secondaryUseItem'
|
||||
| 'speakUsers'
|
||||
| 'addItem'
|
||||
| 'locateOrListItems'
|
||||
| 'locateNearestItem'
|
||||
| 'listItems'
|
||||
| 'pickupDropItem'
|
||||
| 'openItemManagement'
|
||||
| 'editOrInspectItem'
|
||||
| 'editItem'
|
||||
| 'inspectItem'
|
||||
| 'pingServer'
|
||||
| 'locateOrListUsers'
|
||||
| 'locateNearestUser'
|
||||
| 'listUsers'
|
||||
| 'openHelp'
|
||||
| 'openChat'
|
||||
| 'openAdminMenu'
|
||||
@@ -44,9 +47,9 @@ export function resolveMainModeCommand(code: string, shiftKey: boolean): MainMod
|
||||
if (code === 'KeyN') return shiftKey ? null : 'editNickname';
|
||||
if (code === 'KeyM') return shiftKey ? 'toggleOutputMode' : 'toggleMute';
|
||||
if (code === 'Digit1') return shiftKey ? 'toggleLoopback' : 'toggleVoiceLayer';
|
||||
if (code === 'Digit2') return 'toggleItemLayer';
|
||||
if (code === 'Digit3') return 'toggleMediaLayer';
|
||||
if (code === 'Digit4') return 'toggleWorldLayer';
|
||||
if (code === 'Digit2') return shiftKey ? null : 'toggleItemLayer';
|
||||
if (code === 'Digit3') return shiftKey ? null : 'toggleMediaLayer';
|
||||
if (code === 'Digit4') return shiftKey ? null : 'toggleWorldLayer';
|
||||
if (code === 'KeyE') return shiftKey ? null : 'openEffectSelect';
|
||||
if (code === 'Equal') return shiftKey ? 'effectValueUp' : 'masterVolumeUp';
|
||||
if (code === 'Minus') return shiftKey ? 'effectValueDown' : 'masterVolumeDown';
|
||||
@@ -57,15 +60,15 @@ export function resolveMainModeCommand(code: string, shiftKey: boolean): MainMod
|
||||
if (code === 'Enter') return shiftKey ? 'secondaryUseItem' : 'useItem';
|
||||
if (code === 'KeyU') return shiftKey ? null : 'speakUsers';
|
||||
if (code === 'KeyA') return shiftKey ? null : 'addItem';
|
||||
if (code === 'KeyI') return 'locateOrListItems';
|
||||
if (code === 'KeyI') return shiftKey ? 'listItems' : 'locateNearestItem';
|
||||
if (code === 'KeyD') return shiftKey ? null : 'pickupDropItem';
|
||||
if (code === 'KeyO') return 'editOrInspectItem';
|
||||
if (code === 'KeyO') return shiftKey ? 'inspectItem' : 'editItem';
|
||||
if (code === 'KeyP') return shiftKey ? null : 'pingServer';
|
||||
if (code === 'KeyL') return 'locateOrListUsers';
|
||||
if (code === 'KeyL') return shiftKey ? 'listUsers' : 'locateNearestUser';
|
||||
if (code === 'Slash') return shiftKey ? 'openHelp' : 'openChat';
|
||||
if (code === 'KeyZ') return shiftKey ? 'openAdminMenu' : 'openItemManagement';
|
||||
if (code === 'Comma') return shiftKey ? 'chatFirst' : 'chatPrev';
|
||||
if (code === 'Period') return shiftKey ? 'chatLast' : 'chatNext';
|
||||
if (code === 'Escape') return 'escape';
|
||||
if (code === 'Escape') return shiftKey ? null : 'escape';
|
||||
return null;
|
||||
}
|
||||
|
||||
327
client/src/input/mainModeCommands.ts
Normal file
327
client/src/input/mainModeCommands.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import type { CommandDescriptor } from './commandTypes';
|
||||
import type { MainModeCommand } from './mainCommandRouter';
|
||||
|
||||
export type MainModeCommandAvailabilityContext = {
|
||||
voiceSendAllowed: boolean;
|
||||
mainHelpAvailable: boolean;
|
||||
hasAdminActions: boolean;
|
||||
itemTypeCount: number;
|
||||
visibleItemCount: number;
|
||||
userCount: number;
|
||||
chatMessageCount: number;
|
||||
hasCarriedItem: boolean;
|
||||
squareItemCount: number;
|
||||
usableItemCount: number;
|
||||
manageableItemCount: number;
|
||||
hasEditableItemTarget: boolean;
|
||||
hasInspectableItemTarget: boolean;
|
||||
};
|
||||
|
||||
type MainModeCommandDescriptor = CommandDescriptor<MainModeCommand> & {
|
||||
isAvailable: (context: MainModeCommandAvailabilityContext) => boolean;
|
||||
};
|
||||
|
||||
const MAIN_MODE_COMMANDS: MainModeCommandDescriptor[] = [
|
||||
{
|
||||
id: 'editNickname',
|
||||
label: 'Edit nickname',
|
||||
shortcut: 'N',
|
||||
tooltip: 'Edit your current nickname.',
|
||||
section: 'Users',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'toggleMute',
|
||||
label: 'Mute or unmute microphone',
|
||||
shortcut: 'M',
|
||||
tooltip: 'Toggle local microphone mute.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'toggleOutputMode',
|
||||
label: 'Toggle stereo or mono output',
|
||||
shortcut: 'Shift+M',
|
||||
tooltip: 'Switch between stereo and mono output.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'toggleLoopback',
|
||||
label: 'Toggle loopback monitor',
|
||||
shortcut: 'Shift+1',
|
||||
tooltip: 'Toggle local microphone loopback monitoring.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'toggleVoiceLayer',
|
||||
label: 'Toggle voice layer',
|
||||
shortcut: '1',
|
||||
tooltip: 'Enable or disable voice audio.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'toggleItemLayer',
|
||||
label: 'Toggle item layer',
|
||||
shortcut: '2',
|
||||
tooltip: 'Enable or disable item sounds.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'toggleMediaLayer',
|
||||
label: 'Toggle media layer',
|
||||
shortcut: '3',
|
||||
tooltip: 'Enable or disable media audio such as radio.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'toggleWorldLayer',
|
||||
label: 'Toggle world layer',
|
||||
shortcut: '4',
|
||||
tooltip: 'Enable or disable other world sounds.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'masterVolumeUp',
|
||||
label: 'Raise master volume',
|
||||
shortcut: '=',
|
||||
tooltip: 'Increase overall output volume.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'masterVolumeDown',
|
||||
label: 'Lower master volume',
|
||||
shortcut: '-',
|
||||
tooltip: 'Decrease overall output volume.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'openEffectSelect',
|
||||
label: 'Open effect select',
|
||||
shortcut: 'E',
|
||||
tooltip: 'Open the effects menu.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'effectValueUp',
|
||||
label: 'Raise active effect value',
|
||||
shortcut: 'Shift+=',
|
||||
tooltip: 'Increase the selected effect amount.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'effectValueDown',
|
||||
label: 'Lower active effect value',
|
||||
shortcut: 'Shift+-',
|
||||
tooltip: 'Decrease the selected effect amount.',
|
||||
section: 'Audio',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'speakCoordinates',
|
||||
label: 'Speak coordinates',
|
||||
shortcut: 'C',
|
||||
tooltip: 'Announce your current coordinates.',
|
||||
section: 'Navigation',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'openMicGainEdit',
|
||||
label: 'Set microphone gain',
|
||||
shortcut: 'V',
|
||||
tooltip: 'Open microphone gain editing.',
|
||||
section: 'Audio',
|
||||
isAvailable: (context) => context.voiceSendAllowed,
|
||||
},
|
||||
{
|
||||
id: 'calibrateMicrophone',
|
||||
label: 'Calibrate microphone',
|
||||
shortcut: 'Shift+V',
|
||||
tooltip: 'Run microphone calibration.',
|
||||
section: 'Audio',
|
||||
isAvailable: (context) => context.voiceSendAllowed,
|
||||
},
|
||||
{
|
||||
id: 'useItem',
|
||||
label: 'Use item',
|
||||
shortcut: 'Enter',
|
||||
tooltip: 'Use the carried item or a usable item on your current square.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.hasCarriedItem || context.usableItemCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'secondaryUseItem',
|
||||
label: 'Secondary item action',
|
||||
shortcut: 'Shift+Enter',
|
||||
tooltip: 'Run the secondary action for the carried item or a usable item on your current square.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.hasCarriedItem || context.usableItemCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'speakUsers',
|
||||
label: 'Speak connected users',
|
||||
shortcut: 'U',
|
||||
tooltip: 'Announce connected users including yourself.',
|
||||
section: 'Users',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'addItem',
|
||||
label: 'Add item',
|
||||
shortcut: 'A',
|
||||
tooltip: 'Open the add-item menu.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.itemTypeCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'locateNearestItem',
|
||||
label: 'Locate nearest item',
|
||||
shortcut: 'I',
|
||||
tooltip: 'Announce the nearest visible item.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.visibleItemCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'listItems',
|
||||
label: 'List items',
|
||||
shortcut: 'Shift+I',
|
||||
tooltip: 'Open the nearby item list and teleport with Enter.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.visibleItemCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'pickupDropItem',
|
||||
label: 'Pick up or drop item',
|
||||
shortcut: 'D',
|
||||
tooltip: 'Pick up an item on your square or drop your carried item.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.hasCarriedItem || context.squareItemCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'openItemManagement',
|
||||
label: 'Item management',
|
||||
shortcut: 'Z',
|
||||
tooltip: 'Open item management actions for items on your square.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.manageableItemCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'editItem',
|
||||
label: 'Edit item properties',
|
||||
shortcut: 'O',
|
||||
tooltip: 'Edit the carried item or an item on your current square.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.hasEditableItemTarget,
|
||||
},
|
||||
{
|
||||
id: 'inspectItem',
|
||||
label: 'Inspect item properties',
|
||||
shortcut: 'Shift+O',
|
||||
tooltip: 'Inspect all properties for the carried item or an item on your current square.',
|
||||
section: 'Items',
|
||||
isAvailable: (context) => context.hasInspectableItemTarget,
|
||||
},
|
||||
{
|
||||
id: 'pingServer',
|
||||
label: 'Ping server',
|
||||
shortcut: 'P',
|
||||
tooltip: 'Measure round-trip latency to the server.',
|
||||
section: 'Network',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'locateNearestUser',
|
||||
label: 'Locate nearest user',
|
||||
shortcut: 'L',
|
||||
tooltip: 'Announce the nearest connected user.',
|
||||
section: 'Users',
|
||||
isAvailable: (context) => context.userCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'listUsers',
|
||||
label: 'List users',
|
||||
shortcut: 'Shift+L',
|
||||
tooltip: 'Open the user list; Enter teleports and left or right adjust listen volume.',
|
||||
section: 'Users',
|
||||
isAvailable: (context) => context.userCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'openHelp',
|
||||
label: 'Open help',
|
||||
shortcut: '?',
|
||||
tooltip: 'Open the main help viewer.',
|
||||
section: 'Help',
|
||||
isAvailable: (context) => context.mainHelpAvailable,
|
||||
},
|
||||
{
|
||||
id: 'openChat',
|
||||
label: 'Open chat',
|
||||
shortcut: '/',
|
||||
tooltip: 'Start typing a chat message.',
|
||||
section: 'Chat',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
{
|
||||
id: 'openAdminMenu',
|
||||
label: 'Open admin menu',
|
||||
shortcut: 'Shift+Z',
|
||||
tooltip: 'Open the admin actions menu when permitted.',
|
||||
section: 'Admin',
|
||||
isAvailable: (context) => context.hasAdminActions,
|
||||
},
|
||||
{
|
||||
id: 'chatPrev',
|
||||
label: 'Previous chat message',
|
||||
shortcut: ',',
|
||||
tooltip: 'Read the previous buffered chat message.',
|
||||
section: 'Chat',
|
||||
isAvailable: (context) => context.chatMessageCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'chatNext',
|
||||
label: 'Next chat message',
|
||||
shortcut: '.',
|
||||
tooltip: 'Read the next buffered chat message.',
|
||||
section: 'Chat',
|
||||
isAvailable: (context) => context.chatMessageCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'chatFirst',
|
||||
label: 'First chat message',
|
||||
shortcut: 'Shift+,',
|
||||
tooltip: 'Jump to the first buffered chat message.',
|
||||
section: 'Chat',
|
||||
isAvailable: (context) => context.chatMessageCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'chatLast',
|
||||
label: 'Last chat message',
|
||||
shortcut: 'Shift+.',
|
||||
tooltip: 'Jump to the last buffered chat message.',
|
||||
section: 'Chat',
|
||||
isAvailable: (context) => context.chatMessageCount > 0,
|
||||
},
|
||||
{
|
||||
id: 'escape',
|
||||
label: 'Disconnect prompt',
|
||||
shortcut: 'Escape',
|
||||
tooltip: 'Press once for a disconnect prompt and again to disconnect.',
|
||||
section: 'System',
|
||||
isAvailable: () => true,
|
||||
},
|
||||
];
|
||||
|
||||
export function getAvailableMainModeCommands(
|
||||
context: MainModeCommandAvailabilityContext,
|
||||
): MainModeCommandDescriptor[] {
|
||||
return MAIN_MODE_COMMANDS.filter((command) => command.isAvailable(context));
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import type { GameMode } from '../state/gameState';
|
||||
import type { ModeInput } from './commandTypes';
|
||||
|
||||
type ModeHandler = (code: string, key: string, ctrlKey: boolean) => void;
|
||||
type ModeHandler = (input: ModeInput) => void;
|
||||
|
||||
type ModeHandlers = Partial<Record<GameMode, ModeHandler>>;
|
||||
|
||||
type DispatchOptions = {
|
||||
mode: GameMode;
|
||||
code: string;
|
||||
key: string;
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
input: ModeInput;
|
||||
handlers: ModeHandlers;
|
||||
onNormalMode: (code: string, shiftKey: boolean) => void;
|
||||
};
|
||||
@@ -20,8 +18,8 @@ type DispatchOptions = {
|
||||
export function dispatchModeInput(options: DispatchOptions): void {
|
||||
const modeHandler = options.handlers[options.mode];
|
||||
if (modeHandler) {
|
||||
modeHandler(options.code, options.key, options.ctrlKey);
|
||||
modeHandler(options.input);
|
||||
return;
|
||||
}
|
||||
options.onNormalMode(options.code, options.shiftKey);
|
||||
options.onNormalMode(options.input.code, options.input.shiftKey);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user