feat(state): view model action availability and story passage

This commit is contained in:
ginnoir
2026-06-11 18:53:50 -05:00
parent b88399ac14
commit f29b05bd70
3 changed files with 195 additions and 8 deletions
+83 -3
View File
@@ -1,5 +1,11 @@
import type { Content } from '../content/schema';
import type { GameState } from '../engine/game';
import type { GameContent } from '../content/index';
import {
canAffordAction,
canUnlockAction,
isActionAvailable,
type GameState,
} from '../engine/game';
import { getAvailableChoices, getCurrentNode } from '../engine/story';
/**
* Pure mapping from engine state to the view model the React shell renders.
@@ -12,6 +18,29 @@ export interface ResourceView {
amount: number;
}
export interface ActionView {
id: string;
name: string;
available: boolean;
disabledReason: string | null;
storyHint?: string;
storyTooltip?: string;
costsSummary: string | null;
yieldsSummary: string | null;
}
export interface StoryChoiceView {
id: string;
label: string;
disabled: boolean;
disabledReason: string | null;
}
export interface StoryView {
currentProse: string | null;
choices: StoryChoiceView[];
}
export interface GameView {
resources: ResourceView[];
activeActionId: string | null;
@@ -20,9 +49,30 @@ export interface GameView {
actionProgress: number;
queuedActionIds: string[];
queuedActionNames: string[];
actions: ActionView[];
story: StoryView;
}
export function toView(state: GameState, content: Content): GameView {
function actionDisabledReason(
state: GameState,
content: GameContent,
actionId: string,
): string | null {
if (!canUnlockAction(state, content, actionId)) return 'Locked';
if (!canAffordAction(state, content, actionId)) return 'Not enough resources';
return null;
}
function formatResourceList(
items: { resourceId: string; amount: number }[],
content: GameContent,
): string {
return items
.map((i) => `${i.amount} ${content.resourcesById[i.resourceId]?.name ?? i.resourceId}`)
.join(', ');
}
export function toView(state: GameState, content: GameContent): GameView {
const resources: ResourceView[] = content.resources.map((resource) => ({
id: resource.id,
name: resource.name,
@@ -34,6 +84,34 @@ export function toView(state: GameState, content: Content): GameView {
const queuedActionIds = [...state.actionQueue];
const queuedActionNames = queuedActionIds.map((id) => content.actionsById[id]?.name ?? id);
const actions: ActionView[] = content.actions.map((a) => ({
id: a.id,
name: a.name,
available: isActionAvailable(state, content, a.id),
disabledReason: actionDisabledReason(state, content, a.id),
storyHint: a.storyHint,
storyTooltip: a.storyTooltip,
costsSummary: a.costs.length ? formatResourceList(a.costs, content) : null,
yieldsSummary: formatResourceList(a.yields, content),
}));
const node = getCurrentNode(state, content);
const availableChoices = getAvailableChoices(state, content);
const allChoices = node?.choices ?? [];
const story: StoryView = {
currentProse: node?.prose ?? null,
choices: allChoices.map((choice) => {
const available = availableChoices.some((c) => c.id === choice.id);
return {
id: choice.id,
label: choice.label,
disabled: !available,
disabledReason: available ? null : 'Requirements not met',
};
}),
};
return {
resources,
activeActionId: state.activeActionId,
@@ -41,6 +119,8 @@ export function toView(state: GameState, content: Content): GameView {
actionProgress,
queuedActionIds,
queuedActionNames,
actions,
story,
};
}