feat(m1): PR3.1 automation unlock and recipes #19

Merged
ginnoir merged 9 commits from feat/m1-progression into main 2026-06-12 01:26:51 -05:00
3 changed files with 46 additions and 0 deletions
Showing only changes of commit 4b5372cdf9 - Show all commits
+33
View File
@@ -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
+2
View File
@@ -42,6 +42,8 @@ export const useGameStore = create<GameStoreState>((set, get) => ({
actionProgress: 0,
queuedActionIds: [],
queuedActionNames: [],
automationQueueIds: [],
automationQueueNames: [],
actions: [],
story: { currentProse: null, atBootIntro: false, choices: [], tree: [] },
actionColumns: [],
+11
View File
@@ -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<string, ActionView>();
@@ -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,