diff --git a/src/state/runtime.ts b/src/state/runtime.ts index 015dcb8..fd0c1dc 100644 --- a/src/state/runtime.ts +++ b/src/state/runtime.ts @@ -135,6 +135,10 @@ export class GameRuntime { } } + toggleActionGroupCollapsed(groupKey: string): void { + useGameStore.getState().toggleActionGroupCollapsed(groupKey); + } + enqueueAction(actionId: string): void { this.performAction(actionId); } diff --git a/src/ui/ActionCard.tsx b/src/ui/ActionCard.tsx new file mode 100644 index 0000000..bf00255 --- /dev/null +++ b/src/ui/ActionCard.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { gameRuntime } from '../state/runtime'; +import { useGameStore } from '../state/store'; +import type { ActionView } from '../state/viewModel'; + +interface ActionCardProps { + action: ActionView; +} + +export function ActionCard({ action }: ActionCardProps) { + const activeActionId = useGameStore((s) => s.activeActionId); + const actionProgress = useGameStore((s) => s.actionProgress); + const prefs = useGameStore((s) => s.prefs); + const [isOpen, setIsOpen] = useState(false); + + const isActive = action.id === activeActionId; + const isDisabled = !action.available && !isActive; + + const summaryParts = [ + action.costsSummary ? `Cost: ${action.costsSummary}` : null, + action.yieldsSummary ? `Yield: ${action.yieldsSummary}` : null, + ].filter(Boolean); + + let borderBgClass = ''; + if (isDisabled) { + borderBgClass = 'border-slate-800 bg-slate-900/40 opacity-50 cursor-not-allowed'; + } else { + borderBgClass = + 'border-slate-700 bg-slate-800/70 hover:border-amber-500/60 hover:bg-slate-800 cursor-pointer'; + if (action.kind === 'story') { + borderBgClass = + 'border-amber-500/50 bg-slate-800/70 hover:border-amber-500/80 hover:bg-slate-800 cursor-pointer'; + } else if (action.kind === 'loop' && action.loopEnabled) { + borderBgClass = + 'border-amber-500 bg-amber-500/10 shadow-[0_0_8px_rgba(245,158,11,0.15)] hover:border-amber-400 hover:bg-amber-500/15 cursor-pointer'; + } + } + + return ( +
+ + {prefs.actionDetailMode === 'info-button' && action.storyTooltip ? ( +
+ + {isOpen ? ( +
+ {action.storyTooltip} +
+ ) : null} +
+ ) : null} +
+ ); +} diff --git a/src/ui/ActionColumn.tsx b/src/ui/ActionColumn.tsx new file mode 100644 index 0000000..ee7229c --- /dev/null +++ b/src/ui/ActionColumn.tsx @@ -0,0 +1,23 @@ +import type { ActionGroupView } from '../state/viewModel'; +import { ActionGroup } from './ActionGroup'; + +interface ActionColumnProps { + label: string; + groups: ActionGroupView[]; + actionKind: string; +} + +export function ActionColumn({ label, groups, actionKind }: ActionColumnProps) { + return ( +
+

+ {label} +

+
+ {groups.map((group) => ( + + ))} +
+
+ ); +} diff --git a/src/ui/ActionGroup.tsx b/src/ui/ActionGroup.tsx new file mode 100644 index 0000000..3974a80 --- /dev/null +++ b/src/ui/ActionGroup.tsx @@ -0,0 +1,35 @@ +import { gameRuntime } from '../state/runtime'; +import { useGameStore } from '../state/store'; +import type { ActionGroupView } from '../state/viewModel'; +import { ActionCard } from './ActionCard'; + +interface ActionGroupProps { + group: ActionGroupView; + actionKind: string; +} + +export function ActionGroup({ group, actionKind }: ActionGroupProps) { + const collapsed = useGameStore( + (s) => !!s.prefs.collapsedActionGroups[`${actionKind}:${group.id}`], + ); + + return ( +
+ + {!collapsed && ( +
+ {group.actions.map((action) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/ui/ActionPanel.tsx b/src/ui/ActionPanel.tsx deleted file mode 100644 index 5dd2f37..0000000 --- a/src/ui/ActionPanel.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useState } from 'react'; -import { gameRuntime } from '../state/runtime'; -import { useGameStore } from '../state/store'; - -/** Action list with queue, cancel, disabled states, and story hints/tooltips. */ -export function ActionPanel() { - const actions = useGameStore((s) => s.actions); - const activeActionId = useGameStore((s) => s.activeActionId); - const actionProgress = useGameStore((s) => s.actionProgress); - const queuedActionIds = useGameStore((s) => s.queuedActionIds); - const queuedActionNames = useGameStore((s) => s.queuedActionNames); - const prefs = useGameStore((s) => s.prefs); - const [openInfoId, setOpenInfoId] = useState(null); - - return ( -
-

Actions

- {actions.map((action) => { - const isActive = action.id === activeActionId; - const isDisabled = !action.available && !isActive; - const summaryParts = [ - action.costsSummary ? `Cost: ${action.costsSummary}` : null, - action.yieldsSummary ? `Yield: ${action.yieldsSummary}` : null, - ].filter(Boolean); - - return ( -
- - {prefs.actionDetailMode === 'info-button' && action.storyTooltip ? ( -
- - {openInfoId === action.id ? ( -
- {action.storyTooltip} -
- ) : null} -
- ) : null} -
- ); - })} - {queuedActionNames.length > 0 ? ( -
    - {queuedActionNames.map((name, index) => ( -
  1. - {name} - -
  2. - ))} -
- ) : null} -
- ); -} diff --git a/src/ui/PlayPanel.tsx b/src/ui/PlayPanel.tsx index 788e26b..a151c12 100644 --- a/src/ui/PlayPanel.tsx +++ b/src/ui/PlayPanel.tsx @@ -1,14 +1,60 @@ -import { ActionPanel } from './ActionPanel'; +import { gameRuntime } from '../state/runtime'; +import { useGameStore } from '../state/store'; +import { ActionColumn } from './ActionColumn'; import { EventLog } from './EventLog'; import { ResourceBar } from './ResourceBar'; export function PlayPanel() { + const columns = useGameStore((s) => s.actionColumns).filter( + (col) => col.groups.length > 0 && col.groups.some((g) => g.actions.length > 0), + ); + const queuedActionIds = useGameStore((s) => s.queuedActionIds); + const queuedActionNames = useGameStore((s) => s.queuedActionNames); + return (
- + +
+ {columns.map((col) => ( + + ))} +
+ + {queuedActionNames.length > 0 ? ( +
+

+ Action Queue +

+
    + {queuedActionNames.map((name, index) => ( +
  1. + {name} + +
  2. + ))} +
+
+ ) : null} +