Add campaign shell and map generation recovery
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import type { ContentPack } from "@/types/content";
|
||||
import type { CampaignState, RunState, RunSummary } from "@/types/state";
|
||||
|
||||
import { createRunState } from "./runState";
|
||||
|
||||
export const RULES_VERSION = "0.1.0";
|
||||
|
||||
export type CampaignSession = {
|
||||
campaign: CampaignState;
|
||||
run: RunState;
|
||||
};
|
||||
|
||||
export type CreateCampaignSessionOptions = {
|
||||
content: ContentPack;
|
||||
adventurer: CampaignState["adventurer"];
|
||||
at?: string;
|
||||
campaignId?: string;
|
||||
runId?: string;
|
||||
rulesVersion?: string;
|
||||
};
|
||||
|
||||
function dedupeNumbers(values: number[]) {
|
||||
return [...new Set(values)].sort((left, right) => left - right);
|
||||
}
|
||||
|
||||
function getCompletedLevels(run: RunState) {
|
||||
return run.dungeon.globalFlags
|
||||
.map((flag) => /^level:(\d+):completed$/.exec(flag)?.[1])
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((value) => Number.parseInt(value, 10))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
}
|
||||
|
||||
function inferRunOutcome(run: RunState): RunSummary["outcome"] {
|
||||
if (run.status === "failed") {
|
||||
return "defeated";
|
||||
}
|
||||
|
||||
if (run.status === "completed") {
|
||||
return "escaped";
|
||||
}
|
||||
|
||||
return "saved-in-progress";
|
||||
}
|
||||
|
||||
export function summarizeRun(run: RunState, endedAt?: string): RunSummary {
|
||||
const roomsVisited = Object.values(run.dungeon.levels).reduce(
|
||||
(total, level) =>
|
||||
total + Object.values(level.rooms).filter((room) => room.discovery.entered).length,
|
||||
0,
|
||||
);
|
||||
const treasureValue = run.lootedItems.reduce((total, item) => total + item.quantity, 0);
|
||||
|
||||
return {
|
||||
runId: run.id,
|
||||
startedAt: run.startedAt,
|
||||
endedAt,
|
||||
deepestLevel: run.currentLevel,
|
||||
roomsVisited,
|
||||
creaturesDefeated: [...run.defeatedCreatureIds],
|
||||
xpGained: run.xpGained,
|
||||
treasureValue,
|
||||
outcome: inferRunOutcome(run),
|
||||
};
|
||||
}
|
||||
|
||||
function upsertRunSummary(runHistory: RunSummary[], summary: RunSummary) {
|
||||
const nextHistory = runHistory.filter((entry) => entry.runId !== summary.runId);
|
||||
nextHistory.unshift(summary);
|
||||
return nextHistory;
|
||||
}
|
||||
|
||||
export function createCampaignFromRun(
|
||||
content: ContentPack,
|
||||
run: RunState,
|
||||
options?: {
|
||||
at?: string;
|
||||
rulesVersion?: string;
|
||||
},
|
||||
): CampaignState {
|
||||
const at = options?.at ?? run.startedAt;
|
||||
|
||||
return {
|
||||
id: run.campaignId,
|
||||
createdAt: at,
|
||||
updatedAt: at,
|
||||
rulesVersion: options?.rulesVersion ?? RULES_VERSION,
|
||||
contentVersion: content.version,
|
||||
adventurer: structuredClone(run.adventurerSnapshot),
|
||||
unlockedLevels: [1],
|
||||
completedLevels: [],
|
||||
townState: structuredClone(run.townState),
|
||||
questState: [],
|
||||
campaignFlags: [],
|
||||
runHistory: [summarizeRun(run)],
|
||||
};
|
||||
}
|
||||
|
||||
export function syncCampaignFromRun(
|
||||
content: ContentPack,
|
||||
campaign: CampaignState,
|
||||
run: RunState,
|
||||
at = new Date().toISOString(),
|
||||
): CampaignState {
|
||||
const completedLevels = dedupeNumbers([...campaign.completedLevels, ...getCompletedLevels(run)]);
|
||||
const unlockedLevels = dedupeNumbers([
|
||||
...campaign.unlockedLevels,
|
||||
run.currentLevel,
|
||||
...completedLevels,
|
||||
...completedLevels.map((level) => level + 1),
|
||||
]);
|
||||
|
||||
return {
|
||||
...structuredClone(campaign),
|
||||
updatedAt: at,
|
||||
contentVersion: content.version,
|
||||
adventurer: structuredClone(run.adventurerSnapshot),
|
||||
townState: structuredClone(run.townState),
|
||||
unlockedLevels,
|
||||
completedLevels,
|
||||
runHistory: upsertRunSummary(campaign.runHistory, summarizeRun(run)),
|
||||
};
|
||||
}
|
||||
|
||||
export function createCampaignSession(
|
||||
options: CreateCampaignSessionOptions,
|
||||
): CampaignSession {
|
||||
const run = createRunState({
|
||||
content: options.content,
|
||||
adventurer: options.adventurer,
|
||||
campaignId: options.campaignId ?? "campaign.demo",
|
||||
runId: options.runId,
|
||||
at: options.at,
|
||||
});
|
||||
const campaign = createCampaignFromRun(options.content, run, {
|
||||
at: options.at,
|
||||
rulesVersion: options.rulesVersion,
|
||||
});
|
||||
|
||||
return {
|
||||
campaign,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSessionRun(
|
||||
content: ContentPack,
|
||||
session: CampaignSession,
|
||||
nextRun: RunState,
|
||||
at = new Date().toISOString(),
|
||||
): CampaignSession {
|
||||
return {
|
||||
run: nextRun,
|
||||
campaign: syncCampaignFromRun(content, session.campaign, nextRun, at),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user