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) => (
- -
- {name}
-
-
- ))}
-
- ) : 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) => (
+ -
+ {name}
+
+
+ ))}
+
+
+ ) : null}
+