2026-05-09 00:18:25 -05:00
|
|
|
import type { World } from '../world/types'
|
|
|
|
|
import type { GameState, Direction } from '../engine/types'
|
2026-05-09 00:37:45 -05:00
|
|
|
import { getItemsInRoom } from '../engine/dispatcher'
|
2026-05-09 00:18:25 -05:00
|
|
|
|
|
|
|
|
export type ChipKind = 'direction' | 'item' | 'encounter' | 'meta'
|
|
|
|
|
|
|
|
|
|
export interface Chip {
|
|
|
|
|
kind: ChipKind
|
|
|
|
|
label: string
|
|
|
|
|
command: string // the literal string to inject as input
|
|
|
|
|
disabled: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DIRECTION_LABELS: Record<Direction, string> = {
|
|
|
|
|
n: '↑ N', s: '↓ S', e: '→ E', w: '← W', u: '↑ U', d: '↓ D',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function computeChips(state: GameState, world: World): Chip[] {
|
|
|
|
|
const out: Chip[] = []
|
|
|
|
|
const room = world.rooms[state.location]
|
|
|
|
|
if (!room) return out
|
|
|
|
|
|
|
|
|
|
// Direction chips: enabled if exit exists, dimmed otherwise.
|
|
|
|
|
const dirs: Direction[] = ['n', 's', 'e', 'w', 'u', 'd']
|
|
|
|
|
for (const d of dirs) {
|
|
|
|
|
const present = !!room.exits[d]
|
|
|
|
|
if (present || ['n', 's', 'e', 'w'].includes(d)) {
|
|
|
|
|
out.push({
|
|
|
|
|
kind: 'direction',
|
|
|
|
|
label: DIRECTION_LABELS[d],
|
|
|
|
|
command: d,
|
|
|
|
|
disabled: !present,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 00:37:45 -05:00
|
|
|
// Item chips: TAKE for visible items (dynamic list excludes taken items).
|
|
|
|
|
for (const itemId of getItemsInRoom(state, world, state.location)) {
|
2026-05-09 00:18:25 -05:00
|
|
|
const item = world.items[itemId]
|
|
|
|
|
if (!item || !item.takeable) continue
|
2026-05-09 00:37:45 -05:00
|
|
|
if (state.inventory.find((inst) => inst.id === itemId)) continue // already held
|
2026-05-09 00:18:25 -05:00
|
|
|
out.push({
|
|
|
|
|
kind: 'item',
|
|
|
|
|
label: `TAKE ${item.names[0]?.toUpperCase() ?? itemId.toUpperCase()}`,
|
|
|
|
|
command: `take ${item.names[0] ?? itemId}`,
|
|
|
|
|
disabled: false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encounter chips: surface the verbs from the current phase as suggestions.
|
|
|
|
|
if (room.encounter && state.encounterState[room.encounter]) {
|
|
|
|
|
const def = world.encounters[room.encounter]
|
|
|
|
|
const phase = def?.phases[state.encounterState[room.encounter]!]
|
|
|
|
|
if (def && phase) {
|
|
|
|
|
for (const t of phase.transitions) {
|
2026-05-10 05:53:32 -05:00
|
|
|
const targetLabel = t.target && t.target !== '*' ? ` ${t.target.replace(/-/g, ' ').toUpperCase()}` : ''
|
|
|
|
|
const command = t.target && t.target !== '*' ? `${t.verb} ${t.target.replace(/-/g, ' ')}` : t.verb
|
2026-05-09 00:18:25 -05:00
|
|
|
out.push({
|
|
|
|
|
kind: 'encounter',
|
2026-05-10 05:53:32 -05:00
|
|
|
label: t.chipLabel ?? `${t.verb.toUpperCase()}${targetLabel}`,
|
|
|
|
|
command: t.chipCommand ?? command,
|
2026-05-09 00:18:25 -05:00
|
|
|
disabled: false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Persistent meta chips.
|
|
|
|
|
out.push({ kind: 'meta', label: 'LOOK', command: 'look', disabled: false })
|
|
|
|
|
out.push({ kind: 'meta', label: 'INV', command: 'inventory', disabled: false })
|
2026-05-10 05:53:32 -05:00
|
|
|
out.push({ kind: 'meta', label: 'USE', command: 'use ', disabled: false })
|
2026-05-10 07:56:31 -05:00
|
|
|
out.push({ kind: 'meta', label: 'WAIT', command: 'wait', disabled: false })
|
2026-05-09 21:51:12 -05:00
|
|
|
out.push({ kind: 'meta', label: 'HELP', command: 'help', disabled: false })
|
2026-05-09 00:18:25 -05:00
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
}
|