feat(engine): costs on start, unlock checks, queue completion advancement
This commit is contained in:
+107
-24
@@ -26,33 +26,121 @@ export function createGameState(content: Content): GameState {
|
||||
return { resources, activeActionId: null, actionElapsedMs: 0, actionQueue: [] };
|
||||
}
|
||||
|
||||
function assertKnownAction(content: Content, actionId: string): void {
|
||||
if (!content.actionsById[actionId]) {
|
||||
throw new Error(`Unknown action "${actionId}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export function canAffordAction(state: GameState, content: Content, actionId: string): boolean {
|
||||
const action = content.actionsById[actionId];
|
||||
if (!action) return false;
|
||||
return action.costs.every((cost) => (state.resources[cost.resourceId] ?? 0) >= cost.amount);
|
||||
}
|
||||
|
||||
/** Story flags land in PR2; placeholder field keeps unlock schema honest. */
|
||||
export function canUnlockAction(
|
||||
state: GameState,
|
||||
content: Content,
|
||||
actionId: string,
|
||||
storyFlags: Record<string, boolean> = {},
|
||||
): boolean {
|
||||
const action = content.actionsById[actionId];
|
||||
if (!action) return false;
|
||||
const unlock = action.unlock;
|
||||
if (!unlock) return true;
|
||||
if (unlock.minResources) {
|
||||
for (const [resourceId, min] of Object.entries(unlock.minResources)) {
|
||||
if ((state.resources[resourceId] ?? 0) < min) return false;
|
||||
}
|
||||
}
|
||||
if (unlock.requireStoryFlags) {
|
||||
for (const flag of unlock.requireStoryFlags) {
|
||||
if (!storyFlags[flag]) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isActionAvailable(
|
||||
state: GameState,
|
||||
content: Content,
|
||||
actionId: string,
|
||||
storyFlags: Record<string, boolean> = {},
|
||||
): boolean {
|
||||
return (
|
||||
canAffordAction(state, content, actionId) &&
|
||||
canUnlockAction(state, content, actionId, storyFlags)
|
||||
);
|
||||
}
|
||||
|
||||
function deductCosts(state: GameState, content: Content, actionId: string): void {
|
||||
const action = content.actionsById[actionId];
|
||||
if (!action) return;
|
||||
for (const cost of action.costs) {
|
||||
state.resources[cost.resourceId] -= cost.amount;
|
||||
}
|
||||
}
|
||||
|
||||
function grantYields(state: GameState, content: Content, actionId: string): void {
|
||||
const action = content.actionsById[actionId];
|
||||
if (!action) return;
|
||||
for (const y of action.yields) {
|
||||
state.resources[y.resourceId] = (state.resources[y.resourceId] ?? 0) + y.amount;
|
||||
}
|
||||
}
|
||||
|
||||
function beginAction(state: GameState, content: Content, actionId: string): void {
|
||||
assertKnownAction(content, actionId);
|
||||
deductCosts(state, content, actionId);
|
||||
state.activeActionId = actionId;
|
||||
state.actionElapsedMs = 0;
|
||||
}
|
||||
|
||||
function startNextFromQueue(state: GameState, content: Content): void {
|
||||
while (state.actionQueue.length > 0) {
|
||||
const nextId = state.actionQueue.shift()!;
|
||||
if (isActionAvailable(state, content, nextId)) {
|
||||
beginAction(state, content, nextId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
state.activeActionId = null;
|
||||
state.actionElapsedMs = 0;
|
||||
}
|
||||
|
||||
function completeActiveAction(state: GameState, content: Content): void {
|
||||
const actionId = state.activeActionId;
|
||||
if (!actionId) return;
|
||||
grantYields(state, content, actionId);
|
||||
startNextFromQueue(state, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin running an action, resetting its progress. Throws on an unknown id.
|
||||
*
|
||||
* @deprecated Prefer `enqueueAction` — it starts immediately when idle and queues otherwise.
|
||||
*/
|
||||
export function startAction(state: GameState, content: Content, actionId: string): void {
|
||||
if (!content.actionsById[actionId]) {
|
||||
throw new Error(`Unknown action "${actionId}"`);
|
||||
}
|
||||
assertKnownAction(content, actionId);
|
||||
state.activeActionId = actionId;
|
||||
state.actionElapsedMs = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an action immediately when idle, otherwise append it to the queue.
|
||||
* Throws on an unknown id. Cost/unlock checks are not applied yet.
|
||||
* Costs deduct on start (D-0012). Throws when unknown, unaffordable, or locked.
|
||||
*/
|
||||
export function enqueueAction(state: GameState, content: Content, actionId: string): void {
|
||||
if (!content.actionsById[actionId]) {
|
||||
throw new Error(`Unknown action "${actionId}"`);
|
||||
assertKnownAction(content, actionId);
|
||||
if (!isActionAvailable(state, content, actionId)) {
|
||||
throw new Error(`Cannot enqueue action "${actionId}"`);
|
||||
}
|
||||
if (state.activeActionId === null) {
|
||||
state.activeActionId = actionId;
|
||||
state.actionElapsedMs = 0;
|
||||
return;
|
||||
beginAction(state, content, actionId);
|
||||
} else {
|
||||
state.actionQueue.push(actionId);
|
||||
}
|
||||
state.actionQueue.push(actionId);
|
||||
}
|
||||
|
||||
/** Remove a queued action by index. Throws RangeError when out of range. */
|
||||
@@ -69,24 +157,19 @@ export function clearQueue(state: GameState): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the active action by `tickMs`. Each time it reaches its duration it
|
||||
* grants its yield and repeats, carrying the remainder — so one large tick (the
|
||||
* offline catch-up path) can complete an action many times.
|
||||
* Advance the active action by `tickMs`. On completion, grants yields and
|
||||
* advances the queue — actions do not auto-repeat when the queue is empty.
|
||||
*/
|
||||
export function tickGame(state: GameState, content: Content, tickMs: number): void {
|
||||
if (!state.activeActionId) {
|
||||
return;
|
||||
}
|
||||
const action = content.actionsById[state.activeActionId];
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
if (!state.activeActionId) return;
|
||||
|
||||
state.actionElapsedMs += tickMs;
|
||||
while (state.actionElapsedMs >= action.durationMs) {
|
||||
while (state.activeActionId) {
|
||||
const action = content.actionsById[state.activeActionId];
|
||||
if (!action) return;
|
||||
if (state.actionElapsedMs < action.durationMs) return;
|
||||
|
||||
state.actionElapsedMs -= action.durationMs;
|
||||
for (const y of action.yields) {
|
||||
state.resources[y.resourceId] += y.amount;
|
||||
}
|
||||
completeActiveAction(state, content);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user