diff --git a/client/index.html b/client/index.html index 3cfd7fb..7f8df3c 100644 --- a/client/index.html +++ b/client/index.html @@ -53,6 +53,7 @@

Shift+I: List items

A: Add item

O: Edit item properties

+

Shift+O: Read all item properties

D: Pick up/drop item

Shift+D: Delete item

U: Use item

diff --git a/client/public/version.js b/client/public/version.js index 1535e81..fca52e0 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,3 +1,3 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.20 R61"; +window.CHGRID_WEB_VERSION = "2026.02.20 R62"; diff --git a/client/src/main.ts b/client/src/main.ts index 9f16d37..d4fbd20 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -85,6 +85,10 @@ dom.appVersion.textContent = APP_VERSION ? `Another AI experiment with Jage. Version ${APP_VERSION}` : 'Another AI experiment with Jage. Version unknown'; const ITEM_TYPE_SEQUENCE: ItemType[] = ['radio_station', 'dice']; +const ITEM_TYPE_GLOBAL_PROPERTIES: Record> = { + radio_station: { useCooldownMs: 1000 }, + dice: { useCooldownMs: 1000 }, +}; const APP_BASE_URL = import.meta.env.BASE_URL || '/'; function withBase(path: string): string { const normalizedBase = APP_BASE_URL.endsWith('/') ? APP_BASE_URL : `${APP_BASE_URL}/`; @@ -311,7 +315,7 @@ function getCarriedItem(): WorldItem | null { return Array.from(state.items.values()).find((item) => item.carrierId === state.player.id) || null; } -function beginItemSelection(context: 'pickup' | 'delete' | 'edit' | 'use', items: WorldItem[]): void { +function beginItemSelection(context: 'pickup' | 'delete' | 'edit' | 'use' | 'inspect', items: WorldItem[]): void { if (items.length === 0) { updateStatus('No items available.'); audio.sfxUiCancel(); @@ -345,6 +349,25 @@ function useItem(item: WorldItem): void { signaling.send({ type: 'item_use', itemId: item.id }); } +function announceAllItemProperties(item: WorldItem): void { + const details: string[] = []; + details.push(`title: ${item.title}`); + details.push(`type: ${item.type}`); + details.push(`position: ${item.x}, ${item.y}`); + details.push(`carrierId: ${item.carrierId ?? 'none'}`); + details.push(`version: ${item.version}`); + details.push(`capabilities: ${item.capabilities.join(', ') || 'none'}`); + details.push(`useSound: ${item.useSound ?? 'none'}`); + for (const [key, value] of Object.entries(item.params).sort(([a], [b]) => a.localeCompare(b))) { + details.push(`${key}: ${String(value)}`); + } + const globalProperties = ITEM_TYPE_GLOBAL_PROPERTIES[item.type] ?? {}; + for (const [key, value] of Object.entries(globalProperties).sort(([a], [b]) => a.localeCompare(b))) { + details.push(`${key}: ${String(value)} (global)`); + } + updateStatus(details.join('; ')); +} + function releaseSharedRadioSource(streamUrl: string): void { const shared = sharedRadioSources.get(streamUrl); if (!shared) return; @@ -1140,8 +1163,28 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { if (code === 'KeyO') { const squareItems = getItemsAtPosition(state.player.x, state.player.y); + const carried = getCarriedItem(); + if (shiftKey) { + if (squareItems.length === 0) { + if (!carried) { + updateStatus('No item to inspect.'); + audio.sfxUiCancel(); + return; + } + announceAllItemProperties(carried); + audio.sfxUiBlip(); + return; + } + if (squareItems.length === 1) { + announceAllItemProperties(squareItems[0]); + audio.sfxUiBlip(); + return; + } + beginItemSelection('inspect', squareItems); + return; + } + if (squareItems.length === 0) { - const carried = getCarriedItem(); if (!carried) { updateStatus('No editable item here.'); audio.sfxUiCancel(); @@ -1432,6 +1475,11 @@ function handleSelectItemModeInput(code: string): void { useItem(selected); return; } + if (context === 'inspect') { + announceAllItemProperties(selected); + audio.sfxUiBlip(); + return; + } return; } if (code === 'Escape') { diff --git a/client/src/state/gameState.ts b/client/src/state/gameState.ts index 6eb7d33..bde474e 100644 --- a/client/src/state/gameState.ts +++ b/client/src/state/gameState.ts @@ -20,7 +20,7 @@ export type WorldItem = { carrierId?: string | null; }; -export type SelectionContext = 'pickup' | 'drop' | 'delete' | 'edit' | 'use' | null; +export type SelectionContext = 'pickup' | 'drop' | 'delete' | 'edit' | 'use' | 'inspect' | null; export type GameMode = | 'normal'