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'