157 lines
4.0 KiB
TypeScript
157 lines
4.0 KiB
TypeScript
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),
|
|
};
|
|
}
|