From 4b5372cdf91b201ff4974b0e7e31978cb7532a40 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Fri, 12 Jun 2026 00:49:09 -0500 Subject: [PATCH] feat(state): automation fields on action view model --- src/state/__tests__/viewModel.test.ts | 33 +++++++++++++++++++++++++++ src/state/store.ts | 2 ++ src/state/viewModel.ts | 11 +++++++++ 3 files changed, 46 insertions(+) diff --git a/src/state/__tests__/viewModel.test.ts b/src/state/__tests__/viewModel.test.ts index 157d2a5..8d2d467 100644 --- a/src/state/__tests__/viewModel.test.ts +++ b/src/state/__tests__/viewModel.test.ts @@ -239,6 +239,39 @@ describe('action columns projection', () => { expect(rest?.loopEnabled).toBe(true); }); + it('marks automationUnlocked on actions after manual completion', () => { + const state = createGameState(content); + state.manualCompletionCounts.gather_supplies = 1; + const view = toView(state, content); + const gather = view.actionColumns + .flatMap((c) => c.groups) + .flatMap((g) => g.actions) + .find((a) => a.id === 'gather_supplies'); + + expect(gather?.automationUnlocked).toBe(true); + }); + + it('marks actions already in the automation queue', () => { + const state = createGameState(content); + state.automationQueue = ['gather_supplies']; + const view = toView(state, content); + const gather = view.actionColumns + .flatMap((c) => c.groups) + .flatMap((g) => g.actions) + .find((a) => a.id === 'gather_supplies'); + + expect(gather?.inAutomationQueue).toBe(true); + }); + + it('projects automation queue ids and display names', () => { + const state = createGameState(content); + state.automationQueue = ['gather_supplies', 'missing_action']; + const view = toView(state, content); + + expect(view.automationQueueIds).toEqual(['gather_supplies', 'missing_action']); + expect(view.automationQueueNames).toEqual(['Gather supplies', 'missing_action']); + }); + it('shows story actions only when their choice is available, hiding siblings after a fork is taken', () => { const state = createGameState(content); // before reaching the fork, story actions are hidden diff --git a/src/state/store.ts b/src/state/store.ts index 90e010e..154a49b 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -42,6 +42,8 @@ export const useGameStore = create((set, get) => ({ actionProgress: 0, queuedActionIds: [], queuedActionNames: [], + automationQueueIds: [], + automationQueueNames: [], actions: [], story: { currentProse: null, atBootIntro: false, choices: [], tree: [] }, actionColumns: [], diff --git a/src/state/viewModel.ts b/src/state/viewModel.ts index c49e35a..473caa1 100644 --- a/src/state/viewModel.ts +++ b/src/state/viewModel.ts @@ -1,4 +1,5 @@ import type { GameContent } from '../content/index'; +import { isAutomationUnlocked } from '../engine/automation'; import { canAffordAction, canUnlockAction, @@ -40,6 +41,8 @@ export interface ActionView { yieldsSummary: string | null; kind: ActionColumnKind; loopEnabled: boolean; + automationUnlocked: boolean; + inAutomationQueue: boolean; } export interface ActionGroupView { @@ -84,6 +87,8 @@ export interface GameView { actionProgress: number; queuedActionIds: string[]; queuedActionNames: string[]; + automationQueueIds: string[]; + automationQueueNames: string[]; actions: ActionView[]; story: StoryView; actionColumns: ActionColumnView[]; @@ -153,6 +158,8 @@ export function toView(state: GameState, content: GameContent): GameView { : 0; const queuedActionIds = [...state.actionQueue]; const queuedActionNames = queuedActionIds.map((id) => content.actionsById[id]?.name ?? id); + const automationQueueIds = [...state.automationQueue]; + const automationQueueNames = automationQueueIds.map((id) => content.actionsById[id]?.name ?? id); // Build a map of ActionView by id for column assembly const actionViewMap = new Map(); @@ -172,6 +179,8 @@ export function toView(state: GameState, content: GameContent): GameView { yieldsSummary: formatResourceList(a.yields, content), kind: a.kind, loopEnabled: !!state.enabledLoopActionIds[a.id], + automationUnlocked: isAutomationUnlocked(state, content, a.id), + inAutomationQueue: state.automationQueue.includes(a.id), }; actionViewMap.set(a.id, view); return view; @@ -243,6 +252,8 @@ export function toView(state: GameState, content: GameContent): GameView { actionProgress, queuedActionIds, queuedActionNames, + automationQueueIds, + automationQueueNames, actions, story, actionColumns,