From 3aba0beac5297fb9894cfb14b2e18680e901a320 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 23 Feb 2025 20:59:15 -0800 Subject: [PATCH 1/9] prototype for writing a save --- src/playerprogress.ts | 26 +++- src/save.ts | 321 ++++++++++++++++++++++++++++++++++++++++++ src/thralls.ts | 4 + 3 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 src/save.ts diff --git a/src/playerprogress.ts b/src/playerprogress.ts index b1e3232..88434d3 100644 --- a/src/playerprogress.ts +++ b/src/playerprogress.ts @@ -206,7 +206,11 @@ export class PlayerProgress { return skillsAvailable.slice(0, 6); } - getLearnedSkills() { + getUntrimmedAvailableSkillIds(): number[] { + return this.#untrimmedSkillsAvailable.map((s) => s.id); + } + + getLearnedSkills() : Skill[] { let learnedSkills = []; for (let s of this.#skillsLearned.values()) { learnedSkills.push({ id: s }); @@ -214,6 +218,10 @@ export class PlayerProgress { return learnedSkills; } + getRawLearnedSkills() : number[] { + return [...this.#skillsLearned]; + } + getStats() { return { ...this.#stats }; } @@ -233,6 +241,10 @@ export class PlayerProgress { return this.#thrallsUnlocked.indexOf(thrall.id) != -1; } + getUnlockedThrallIds() : number[] { + return [...this.#thrallsUnlocked]; + } + damageThrall(thrall: Thrall, amount: number) { if (amount <= 0.0) { throw new Error(`damage must be some positive amount, not ${amount}`); @@ -246,6 +258,10 @@ export class PlayerProgress { (this.#thrallDamage[thrall.id] ?? 0.0) + amount; } + getThrallDamage(thrall: Thrall): number { + return this.#thrallDamage[thrall.id] ?? 0.0; + } + getThrallLifeStage(thrall: Thrall): LifeStage { let damage = this.#thrallDamage[thrall.id] ?? 0; if (damage < 0.5) { @@ -270,6 +286,10 @@ export class PlayerProgress { this.#thrallsObtainedItem.push(thrall.id); } + getThrallObtainedItemIds() : number[] { + return [...this.#thrallsObtainedItem]; + } + deliverThrallItem(thrall: Thrall) { if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) { return; @@ -277,6 +297,10 @@ export class PlayerProgress { this.#thrallsDeliveredItem.push(thrall.id); } + getThrallDeliveredItemIds() : number[] { + return [...this.#thrallsDeliveredItem]; + } + getThrallItemStage(thrall: Thrall): ItemStage { if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) { return ItemStage.Delivered; diff --git a/src/save.ts b/src/save.ts new file mode 100644 index 0000000..e4900ad --- /dev/null +++ b/src/save.ts @@ -0,0 +1,321 @@ +import { getPlayerProgress } from "./playerprogress" +import { getStateManager } from "./statemanager"; +import { getThralls } from "./thralls"; + +export interface SaveFile { + version: string; + revision: number; +} + +export interface StatCounterV1{ + agi: number; + int: number; + cha: number; + psi: number; +} + +export interface SaveFileV1 { + version: "fledgling_save_v1"; + revision: number; + + turn: number; + + name: string; + thrallTemplateId: number; + nImprovements: number; + stats: StatCounterV1; + talents: StatCounterV1; + isInPenance: boolean; + wishId: number; // negative: Wish is absent + exp: number; + blood: number; + itemsPurloined: number; + skillsLearned: number[]; + untrimmedSkillsAvailableIds: number[]; + thrallsUnlocked: number[]; + thrallDamage: number[]; // 0: thrall is absent or undamaged + thrallsObtainedItem: number[]; + thrallsDeliveredItem: number[]; +} + +export type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2"; + +/// Checks whether obj is a valid save file, as far as we can tell, and returns +/// it unchanged if it is, or throws an error if it's not valid. +export function mustBeSaveFileV1(obj: unknown): SaveFileV1 { + if (obj === undefined || obj === null) { + throw new Error("nonexistent"); + } + if (typeof(obj) !== "object") { + throw new Error(`not an object; was ${typeof(obj)}`); + } + + if (!("version" in obj)) { + throw new Error("no magic number"); + } + if (obj.version !== "fledgling_save_v1") { + throw new Error(`bad magic number: ${obj.version}`); + } + + return { + version: "fledgling_save_v1", + revision: mustGetNumber(obj, "revision"), + turn: mustGetNumber(obj, "turn"), + name: mustGetString(obj, "name"), + thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"), + nImprovements: mustGetNumber(obj, "nImprovements"), + stats: mustGetStatCounterV1(obj, "stats"), + talents: mustGetStatCounterV1(obj, "talents"), + isInPenance: mustGetBoolean(obj, "isInPenance"), + wishId: mustGetNumber(obj, "wishId"), + exp: mustGetNumber(obj, "exp"), + blood: mustGetNumber(obj, "blood"), + itemsPurloined: mustGetNumber(obj, "itemsPurloined"), + skillsLearned: mustGetNumberArray(obj, "skillsLearned"), + untrimmedSkillsAvailableIds: mustGetNumberArray(obj, "untrimmedSkillsAvailableIds"), + thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"), + thrallDamage: mustGetNumberArray(obj, "thrallDamage"), + thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), + thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), + }; +} + +function mustGetNumber(obj: object, key: string) : number { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "number") { + throw new Error(`not a number: ${key}: ${val}`); + } + return val; +} + +function mustGetString(obj: object, key: string) : string { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "string") { + throw new Error(`not a string: ${key}: ${val}`); + } + return val; +} + +function mustGetStatCounterV1(obj: object, key: string) : StatCounterV1 { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } + + try { + return { + agi: mustGetNumber(val, "agi"), + int: mustGetNumber(val, "int"), + cha: mustGetNumber(val, "cha"), + psi: mustGetNumber(val, "psi"), + }; + } catch (e) { + let message = "unrecognizable error"; + if (e instanceof Error) { + message = e.message; + } + throw new Error(`reading ${key}: ${message}`); + } +} + +function mustGetBoolean(obj: object, key: string) : boolean { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "boolean") { + throw new Error(`not boolean: ${key}: ${val}`); + } + return val; +} + +function mustGetNumberArray(obj: object, key: string) : number[] { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } + + for (const x of val) { + if (typeof(x) !== "number") { + throw new Error(`contained non-number item in ${key}: ${val}`); + } + } + return val; +} + +/// The result of attempting to load a V1 save file. +interface SaveFileV1LoadResult { + // If present and valid, the loaded file. + file : SaveFileV1 | null; + + /// A file loading error, if any. If `file` is present, this refers + /// to an error reading from the *other* slot. + error: string | null; + + /// The slot this file was loaded from, or that a load attempt failed from. + /// If multiple load attempts failed and none succeeded, this refers to + /// any one attempted slot. + slot: SaveSlot; +} + +function readFromSlot(slot: SaveSlot): SaveFileV1LoadResult { + var serialized = localStorage.getItem(slot); + if (serialized === null) { + return { + file: null, + error: null, + slot: slot, + }; + } + try { + return { + file: mustBeSaveFileV1(JSON.parse(serialized)), + error: null, + slot: slot, + }; + } catch (e) { + let message = "unidentifiable error"; + if (e instanceof Error) { + message = e.message; + } + return { + file: null, + error: message, + slot: slot, + }; + } +} + +/// Reads the more recent valid save file, if either is valid. If no save files +/// are present, `file` and `error` will both be absent. If an invalid save +/// file is discovered, `error` will refer to the issue(s) detected. +function readBestSave(): SaveFileV1LoadResult { + const from1 = readFromSlot("FLEDGLING_SLOT_1"); + const from2 = readFromSlot("FLEDGLING_SLOT_2"); + if (from1.file && from2.file) { + return (from1.file.revision > from2.file.revision) ? from1 : from2; + } + + var errors : string[] = []; + if (from1.error) { + errors = ["slot 1 error: " + from1.error]; + } + if (from2.error) { + errors.push("slot 2 error: " + from2.error); + } + var msg : string | null = errors.length > 0 ? errors.join("\n") : null; + if (from1.file) { + return { + file: from1.file, + error: msg, + slot: "FLEDGLING_SLOT_1", + }; + } + return { + file: from2.file, + error: msg, + slot: "FLEDGLING_SLOT_2", + }; +} + +export function save() { + const targetSlot : SaveSlot = (readBestSave().slot === "FLEDGLING_SLOT_1") ? "FLEDGLING_SLOT_2" : "FLEDGLING_SLOT_1"; + return saveIntoSlot(targetSlot); +} + +function extractCurrentState() : SaveFileV1 { + const progress = getPlayerProgress(); + const stateManager = getStateManager(); + var thrallDamage : number[] = []; + const nThralls = getThralls().length; + for (let i = 0; i < nThralls; ++i) { + thrallDamage.push(progress.getThrallDamage({id: i})); + } + return { + version: "fledgling_save_v1", + revision: stateManager.nextRevision(), + turn: stateManager.getTurn(), + name: progress.name, + thrallTemplateId: progress.template.id, + nImprovements: progress.nImprovements, + stats: { + agi: progress.getStat("AGI"), + int: progress.getStat("INT"), + cha: progress.getStat("CHA"), + psi: progress.getStat("PSI"), + }, + talents: { + agi: progress.getStat("AGI"), + int: progress.getStat("INT"), + cha: progress.getStat("CHA"), + psi: progress.getStat("PSI"), + }, + isInPenance: progress.isInPenance, + wishId: progress.getWish()?.id ?? -1, + exp: progress.getExperience(), + blood: progress.getBlood(), + itemsPurloined: progress.getItemsPurloined(), + skillsLearned: progress.getRawLearnedSkills(), + untrimmedSkillsAvailableIds: progress.getUntrimmedAvailableSkillIds(), + thrallsUnlocked: progress.getUnlockedThrallIds(), + thrallDamage: thrallDamage, + thrallsObtainedItem: progress.getThrallObtainedItemIds(), + thrallsDeliveredItem: progress.getThrallDeliveredItemIds(), + }; +} + +function saveIntoSlot(slot: SaveSlot) { + localStorage.setItem(slot, JSON.stringify(extractCurrentState())); +} + +export function wipeSaves() { + localStorage.removeItem("FLEDGLING_SLOT_1"); + localStorage.removeItem("FLEDGLING_SLOT_2"); +} \ No newline at end of file diff --git a/src/thralls.ts b/src/thralls.ts index 743ac9b..45c3f71 100644 --- a/src/thralls.ts +++ b/src/thralls.ts @@ -52,6 +52,10 @@ class ThrallsTable { } return thralls; } + + get length(): number { + return this.#thralls.length; + } } export type ThrallData = { label: string; -- 2.43.0 From a149938f008ac64f71929bb44e0a90b916233b83 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 23 Feb 2025 22:20:05 -0800 Subject: [PATCH 2/9] violently read player from file --- src/playerprogress.ts | 80 ++++++++++++++----- src/save.ts | 34 +------- src/saveformat.ts | 180 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 51 deletions(-) create mode 100644 src/saveformat.ts diff --git a/src/playerprogress.ts b/src/playerprogress.ts index 88434d3..a73f4cf 100644 --- a/src/playerprogress.ts +++ b/src/playerprogress.ts @@ -1,6 +1,12 @@ import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts"; import { getSkills } from "./skills.ts"; import { getThralls, ItemStage, LifeStage, Thrall } from "./thralls.ts"; +import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat.ts"; + +interface NewRoundConfig { + asSuccessor: SuccessorOption, + withWish: Wish | null, +} export class PlayerProgress { #name: string; @@ -20,25 +26,61 @@ export class PlayerProgress { #thrallsObtainedItem: number[]; #thrallsDeliveredItem: number[]; - constructor(asSuccessor: SuccessorOption, withWish: Wish | null) { - this.#name = asSuccessor.name; - this.#thrallTemplate = asSuccessor.template.id; - this.#nImprovements = asSuccessor.nImprovements; - this.#stats = { ...asSuccessor.stats }; - this.#talents = { ...asSuccessor.talents }; - this.#isInPenance = asSuccessor.inPenance; - this.#wish = withWish; - this.#exp = 0; - this.#blood = 0; - this.#itemsPurloined = 0; - this.#skillsLearned = []; - this.#untrimmedSkillsAvailable = []; - this.#thrallsUnlocked = []; - this.#thrallDamage = {}; - this.#thrallsObtainedItem = []; - this.#thrallsDeliveredItem = []; + constructor(args: NewRoundConfig | SaveFileV1) { + if ("asSuccesor" in args) { + //asSuccessor: SuccessorOption, withWish: Wish | null) { + const config = args as NewRoundConfig; + const asSuccessor = config.asSuccessor; + this.#name = asSuccessor.name; + this.#thrallTemplate = asSuccessor.template.id; + this.#nImprovements = asSuccessor.nImprovements; + this.#stats = { ...asSuccessor.stats }; + this.#talents = { ...asSuccessor.talents }; + this.#isInPenance = asSuccessor.inPenance; + this.#wish = config.withWish; + this.#exp = 0; + this.#blood = 0; + this.#itemsPurloined = 0; + this.#skillsLearned = []; + this.#untrimmedSkillsAvailable = []; + this.#thrallsUnlocked = []; + this.#thrallDamage = {}; + this.#thrallsObtainedItem = []; + this.#thrallsDeliveredItem = []; - this.refill(); + this.refill(); + } else { + const file = mustBeSaveFileV1(args); + this.#name = file.name; + this.#thrallTemplate = file.thrallTemplateId; + this.#nImprovements = file.nImprovements; + this.#stats = { + AGI: file.stats.agi, + INT: file.stats.int, + CHA: file.stats.cha, + PSI: file.stats.psi, + } + this.#talents = { + AGI: file.talents.agi, + INT: file.talents.int, + CHA: file.talents.cha, + PSI: file.talents.psi, + } + this.#isInPenance = file.isInPenance, + this.#wish = file.wishId >= 0 ? {id: file.wishId} : null; + this.#exp = file.exp; + this.#blood = file.blood; + this.#itemsPurloined = file.itemsPurloined; + this.#skillsLearned = file.skillsLearned; + this.#untrimmedSkillsAvailable = file.untrimmedSkillsAvailableIds.map((id) => {return {id: id}}); + this.#thrallsUnlocked = file.thrallsUnlocked; + this.#thrallDamage = {}; + for(let i = 0; i < file.thrallDamage.length; ++i) { + this.#thrallDamage[i] = file.thrallDamage[i]; + } + this.#thrallsObtainedItem = file.thrallsObtainedItem; + this.#thrallsDeliveredItem = file.thrallsDeliveredItem; + } } applyEndOfTurn() { @@ -332,7 +374,7 @@ export function initPlayerProgress( asSuccessor: SuccessorOption, withWish: Wish | null, ) { - active = new PlayerProgress(asSuccessor, withWish); + active = new PlayerProgress({asSuccessor:asSuccessor, withWish:withWish}); } export function getPlayerProgress(): PlayerProgress { diff --git a/src/save.ts b/src/save.ts index e4900ad..68d7db7 100644 --- a/src/save.ts +++ b/src/save.ts @@ -1,44 +1,14 @@ import { getPlayerProgress } from "./playerprogress" import { getStateManager } from "./statemanager"; import { getThralls } from "./thralls"; +import {SaveFileV1, StatCounterV1} from "./saveformat"; export interface SaveFile { version: string; revision: number; } -export interface StatCounterV1{ - agi: number; - int: number; - cha: number; - psi: number; -} - -export interface SaveFileV1 { - version: "fledgling_save_v1"; - revision: number; - - turn: number; - - name: string; - thrallTemplateId: number; - nImprovements: number; - stats: StatCounterV1; - talents: StatCounterV1; - isInPenance: boolean; - wishId: number; // negative: Wish is absent - exp: number; - blood: number; - itemsPurloined: number; - skillsLearned: number[]; - untrimmedSkillsAvailableIds: number[]; - thrallsUnlocked: number[]; - thrallDamage: number[]; // 0: thrall is absent or undamaged - thrallsObtainedItem: number[]; - thrallsDeliveredItem: number[]; -} - -export type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2"; +type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2"; /// Checks whether obj is a valid save file, as far as we can tell, and returns /// it unchanged if it is, or throws an error if it's not valid. diff --git a/src/saveformat.ts b/src/saveformat.ts new file mode 100644 index 0000000..c141c30 --- /dev/null +++ b/src/saveformat.ts @@ -0,0 +1,180 @@ +export interface StatCounterV1{ + agi: number; + int: number; + cha: number; + psi: number; +} + +export interface SaveFileV1 { + version: "fledgling_save_v1"; + revision: number; + + turn: number; + + name: string; + thrallTemplateId: number; + nImprovements: number; + stats: StatCounterV1; + talents: StatCounterV1; + isInPenance: boolean; + wishId: number; // negative: Wish is absent + exp: number; + blood: number; + itemsPurloined: number; + skillsLearned: number[]; + untrimmedSkillsAvailableIds: number[]; + thrallsUnlocked: number[]; + thrallDamage: number[]; // 0: thrall is absent or undamaged + thrallsObtainedItem: number[]; + thrallsDeliveredItem: number[]; +} + +/// Checks whether obj is a valid save file, as far as we can tell, and returns +/// it unchanged if it is, or throws an error if it's not valid. +export function mustBeSaveFileV1(obj: unknown): SaveFileV1 { + if (obj === undefined || obj === null) { + throw new Error("nonexistent"); + } + if (typeof(obj) !== "object") { + throw new Error(`not an object; was ${typeof(obj)}`); + } + + if (!("version" in obj)) { + throw new Error("no magic number"); + } + if (obj.version !== "fledgling_save_v1") { + throw new Error(`bad magic number: ${obj.version}`); + } + + return { + version: "fledgling_save_v1", + revision: mustGetNumber(obj, "revision"), + turn: mustGetNumber(obj, "turn"), + name: mustGetString(obj, "name"), + thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"), + nImprovements: mustGetNumber(obj, "nImprovements"), + stats: mustGetStatCounterV1(obj, "stats"), + talents: mustGetStatCounterV1(obj, "talents"), + isInPenance: mustGetBoolean(obj, "isInPenance"), + wishId: mustGetNumber(obj, "wishId"), + exp: mustGetNumber(obj, "exp"), + blood: mustGetNumber(obj, "blood"), + itemsPurloined: mustGetNumber(obj, "itemsPurloined"), + skillsLearned: mustGetNumberArray(obj, "skillsLearned"), + untrimmedSkillsAvailableIds: mustGetNumberArray(obj, "untrimmedSkillsAvailableIds"), + thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"), + thrallDamage: mustGetNumberArray(obj, "thrallDamage"), + thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), + thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), + }; +} + +function mustGetNumber(obj: object, key: string) : number { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "number") { + throw new Error(`not a number: ${key}: ${val}`); + } + return val; +} + +function mustGetString(obj: object, key: string) : string { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "string") { + throw new Error(`not a string: ${key}: ${val}`); + } + return val; +} + +function mustGetStatCounterV1(obj: object, key: string) : StatCounterV1 { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } + + try { + return { + agi: mustGetNumber(val, "agi"), + int: mustGetNumber(val, "int"), + cha: mustGetNumber(val, "cha"), + psi: mustGetNumber(val, "psi"), + }; + } catch (e) { + let message = "unrecognizable error"; + if (e instanceof Error) { + message = e.message; + } + throw new Error(`reading ${key}: ${message}`); + } +} + +function mustGetBoolean(obj: object, key: string) : boolean { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "boolean") { + throw new Error(`not boolean: ${key}: ${val}`); + } + return val; +} + +function mustGetNumberArray(obj: object, key: string) : number[] { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof(obj) !== "object") { + throw new Error(`container was not an object; was ${typeof(obj)}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as {[key:string]:any}; + const val = dict[key]; + if (typeof(val) !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } + + for (const x of val) { + if (typeof(x) !== "number") { + throw new Error(`contained non-number item in ${key}: ${val}`); + } + } + return val; +} \ No newline at end of file -- 2.43.0 From 58b8bbc27bfdb59a840cd92eda72786fa7ace204 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 23 Feb 2025 22:21:10 -0800 Subject: [PATCH 3/9] oops, missed revisions in StateManager --- src/statemanager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/statemanager.ts b/src/statemanager.ts index a30cdb3..50489ec 100644 --- a/src/statemanager.ts +++ b/src/statemanager.ts @@ -15,15 +15,22 @@ const N_TURNS: number = 9; export class StateManager { #turn: number; + #revision: number; constructor() { this.#turn = 1; + this.#revision = 1; } getTurn(): number { return this.#turn; } + nextRevision(): number { + this.#revision++; + return this.#revision; + } + startFirstGame() { getVNModal().play([ ...openingScene, -- 2.43.0 From 2837461addfa3568c37f5696cfa7d862086cd1e4 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 23 Feb 2025 22:23:27 -0800 Subject: [PATCH 4/9] create StateManager from file --- src/statemanager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/statemanager.ts b/src/statemanager.ts index 50489ec..7b09f44 100644 --- a/src/statemanager.ts +++ b/src/statemanager.ts @@ -10,6 +10,7 @@ import { openingScene } from "./openingscene.ts"; import { generateName } from "./namegen.ts"; import { photogenicThralls } from "./thralls.ts"; import { choose } from "./utils.ts"; +import { SaveFileV1 } from "./saveformat.ts"; const N_TURNS: number = 9; @@ -17,9 +18,9 @@ export class StateManager { #turn: number; #revision: number; - constructor() { - this.#turn = 1; - this.#revision = 1; + constructor(file?:SaveFileV1) { + this.#turn = file?.turn ?? 1; + this.#revision = file?.revision ?? 1; } getTurn(): number { -- 2.43.0 From 3a968af5ca9147b2d4b74fc72bf62b5158f0e720 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 23 Feb 2025 22:25:19 -0800 Subject: [PATCH 5/9] autoformat the world --- src/playerprogress.ts | 34 +-- src/save.ts | 486 +++++++++++++++++++++--------------------- src/saveformat.ts | 323 ++++++++++++++-------------- src/statemanager.ts | 2 +- 4 files changed, 429 insertions(+), 416 deletions(-) diff --git a/src/playerprogress.ts b/src/playerprogress.ts index a73f4cf..a486d2d 100644 --- a/src/playerprogress.ts +++ b/src/playerprogress.ts @@ -4,8 +4,8 @@ import { getThralls, ItemStage, LifeStage, Thrall } from "./thralls.ts"; import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat.ts"; interface NewRoundConfig { - asSuccessor: SuccessorOption, - withWish: Wish | null, + asSuccessor: SuccessorOption; + withWish: Wish | null; } export class PlayerProgress { @@ -28,7 +28,7 @@ export class PlayerProgress { constructor(args: NewRoundConfig | SaveFileV1) { if ("asSuccesor" in args) { - //asSuccessor: SuccessorOption, withWish: Wish | null) { + //asSuccessor: SuccessorOption, withWish: Wish | null) { const config = args as NewRoundConfig; const asSuccessor = config.asSuccessor; this.#name = asSuccessor.name; @@ -59,23 +59,27 @@ export class PlayerProgress { INT: file.stats.int, CHA: file.stats.cha, PSI: file.stats.psi, - } + }; this.#talents = { AGI: file.talents.agi, INT: file.talents.int, CHA: file.talents.cha, PSI: file.talents.psi, - } - this.#isInPenance = file.isInPenance, - this.#wish = file.wishId >= 0 ? {id: file.wishId} : null; + }; + (this.#isInPenance = file.isInPenance), + (this.#wish = file.wishId >= 0 ? { id: file.wishId } : null); this.#exp = file.exp; this.#blood = file.blood; this.#itemsPurloined = file.itemsPurloined; this.#skillsLearned = file.skillsLearned; - this.#untrimmedSkillsAvailable = file.untrimmedSkillsAvailableIds.map((id) => {return {id: id}}); + this.#untrimmedSkillsAvailable = file.untrimmedSkillsAvailableIds.map( + (id) => { + return { id: id }; + }, + ); this.#thrallsUnlocked = file.thrallsUnlocked; this.#thrallDamage = {}; - for(let i = 0; i < file.thrallDamage.length; ++i) { + for (let i = 0; i < file.thrallDamage.length; ++i) { this.#thrallDamage[i] = file.thrallDamage[i]; } this.#thrallsObtainedItem = file.thrallsObtainedItem; @@ -252,7 +256,7 @@ export class PlayerProgress { return this.#untrimmedSkillsAvailable.map((s) => s.id); } - getLearnedSkills() : Skill[] { + getLearnedSkills(): Skill[] { let learnedSkills = []; for (let s of this.#skillsLearned.values()) { learnedSkills.push({ id: s }); @@ -260,7 +264,7 @@ export class PlayerProgress { return learnedSkills; } - getRawLearnedSkills() : number[] { + getRawLearnedSkills(): number[] { return [...this.#skillsLearned]; } @@ -283,7 +287,7 @@ export class PlayerProgress { return this.#thrallsUnlocked.indexOf(thrall.id) != -1; } - getUnlockedThrallIds() : number[] { + getUnlockedThrallIds(): number[] { return [...this.#thrallsUnlocked]; } @@ -328,7 +332,7 @@ export class PlayerProgress { this.#thrallsObtainedItem.push(thrall.id); } - getThrallObtainedItemIds() : number[] { + getThrallObtainedItemIds(): number[] { return [...this.#thrallsObtainedItem]; } @@ -339,7 +343,7 @@ export class PlayerProgress { this.#thrallsDeliveredItem.push(thrall.id); } - getThrallDeliveredItemIds() : number[] { + getThrallDeliveredItemIds(): number[] { return [...this.#thrallsDeliveredItem]; } @@ -374,7 +378,7 @@ export function initPlayerProgress( asSuccessor: SuccessorOption, withWish: Wish | null, ) { - active = new PlayerProgress({asSuccessor:asSuccessor, withWish:withWish}); + active = new PlayerProgress({ asSuccessor: asSuccessor, withWish: withWish }); } export function getPlayerProgress(): PlayerProgress { diff --git a/src/save.ts b/src/save.ts index 68d7db7..c147759 100644 --- a/src/save.ts +++ b/src/save.ts @@ -1,11 +1,11 @@ -import { getPlayerProgress } from "./playerprogress" +import { getPlayerProgress } from "./playerprogress"; import { getStateManager } from "./statemanager"; import { getThralls } from "./thralls"; -import {SaveFileV1, StatCounterV1} from "./saveformat"; +import { SaveFileV1, StatCounterV1 } from "./saveformat"; export interface SaveFile { - version: string; - revision: number; + version: string; + revision: number; } type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2"; @@ -13,279 +13,285 @@ type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2"; /// Checks whether obj is a valid save file, as far as we can tell, and returns /// it unchanged if it is, or throws an error if it's not valid. export function mustBeSaveFileV1(obj: unknown): SaveFileV1 { - if (obj === undefined || obj === null) { - throw new Error("nonexistent"); - } - if (typeof(obj) !== "object") { - throw new Error(`not an object; was ${typeof(obj)}`); - } + if (obj === undefined || obj === null) { + throw new Error("nonexistent"); + } + if (typeof obj !== "object") { + throw new Error(`not an object; was ${typeof obj}`); + } - if (!("version" in obj)) { - throw new Error("no magic number"); - } - if (obj.version !== "fledgling_save_v1") { - throw new Error(`bad magic number: ${obj.version}`); - } + if (!("version" in obj)) { + throw new Error("no magic number"); + } + if (obj.version !== "fledgling_save_v1") { + throw new Error(`bad magic number: ${obj.version}`); + } + return { + version: "fledgling_save_v1", + revision: mustGetNumber(obj, "revision"), + turn: mustGetNumber(obj, "turn"), + name: mustGetString(obj, "name"), + thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"), + nImprovements: mustGetNumber(obj, "nImprovements"), + stats: mustGetStatCounterV1(obj, "stats"), + talents: mustGetStatCounterV1(obj, "talents"), + isInPenance: mustGetBoolean(obj, "isInPenance"), + wishId: mustGetNumber(obj, "wishId"), + exp: mustGetNumber(obj, "exp"), + blood: mustGetNumber(obj, "blood"), + itemsPurloined: mustGetNumber(obj, "itemsPurloined"), + skillsLearned: mustGetNumberArray(obj, "skillsLearned"), + untrimmedSkillsAvailableIds: mustGetNumberArray( + obj, + "untrimmedSkillsAvailableIds", + ), + thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"), + thrallDamage: mustGetNumberArray(obj, "thrallDamage"), + thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), + thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), + }; +} + +function mustGetNumber(obj: object, key: string): number { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "number") { + throw new Error(`not a number: ${key}: ${val}`); + } + return val; +} + +function mustGetString(obj: object, key: string): string { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "string") { + throw new Error(`not a string: ${key}: ${val}`); + } + return val; +} + +function mustGetStatCounterV1(obj: object, key: string): StatCounterV1 { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } + + try { return { - version: "fledgling_save_v1", - revision: mustGetNumber(obj, "revision"), - turn: mustGetNumber(obj, "turn"), - name: mustGetString(obj, "name"), - thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"), - nImprovements: mustGetNumber(obj, "nImprovements"), - stats: mustGetStatCounterV1(obj, "stats"), - talents: mustGetStatCounterV1(obj, "talents"), - isInPenance: mustGetBoolean(obj, "isInPenance"), - wishId: mustGetNumber(obj, "wishId"), - exp: mustGetNumber(obj, "exp"), - blood: mustGetNumber(obj, "blood"), - itemsPurloined: mustGetNumber(obj, "itemsPurloined"), - skillsLearned: mustGetNumberArray(obj, "skillsLearned"), - untrimmedSkillsAvailableIds: mustGetNumberArray(obj, "untrimmedSkillsAvailableIds"), - thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"), - thrallDamage: mustGetNumberArray(obj, "thrallDamage"), - thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), - thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), + agi: mustGetNumber(val, "agi"), + int: mustGetNumber(val, "int"), + cha: mustGetNumber(val, "cha"), + psi: mustGetNumber(val, "psi"), }; + } catch (e) { + let message = "unrecognizable error"; + if (e instanceof Error) { + message = e.message; + } + throw new Error(`reading ${key}: ${message}`); + } } -function mustGetNumber(obj: object, key: string) : number { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "number") { - throw new Error(`not a number: ${key}: ${val}`); - } - return val; +function mustGetBoolean(obj: object, key: string): boolean { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "boolean") { + throw new Error(`not boolean: ${key}: ${val}`); + } + return val; } -function mustGetString(obj: object, key: string) : string { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "string") { - throw new Error(`not a string: ${key}: ${val}`); - } - return val; -} +function mustGetNumberArray(obj: object, key: string): number[] { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } -function mustGetStatCounterV1(obj: object, key: string) : StatCounterV1 { - if (obj === null || obj === undefined) { - throw new Error("container absent"); + for (const x of val) { + if (typeof x !== "number") { + throw new Error(`contained non-number item in ${key}: ${val}`); } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "object") { - throw new Error(`not an object: ${key}: ${val}`); - } - - try { - return { - agi: mustGetNumber(val, "agi"), - int: mustGetNumber(val, "int"), - cha: mustGetNumber(val, "cha"), - psi: mustGetNumber(val, "psi"), - }; - } catch (e) { - let message = "unrecognizable error"; - if (e instanceof Error) { - message = e.message; - } - throw new Error(`reading ${key}: ${message}`); - } -} - -function mustGetBoolean(obj: object, key: string) : boolean { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "boolean") { - throw new Error(`not boolean: ${key}: ${val}`); - } - return val; -} - -function mustGetNumberArray(obj: object, key: string) : number[] { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "object") { - throw new Error(`not an object: ${key}: ${val}`); - } - - for (const x of val) { - if (typeof(x) !== "number") { - throw new Error(`contained non-number item in ${key}: ${val}`); - } - } - return val; + } + return val; } /// The result of attempting to load a V1 save file. interface SaveFileV1LoadResult { - // If present and valid, the loaded file. - file : SaveFileV1 | null; + // If present and valid, the loaded file. + file: SaveFileV1 | null; - /// A file loading error, if any. If `file` is present, this refers - /// to an error reading from the *other* slot. - error: string | null; + /// A file loading error, if any. If `file` is present, this refers + /// to an error reading from the *other* slot. + error: string | null; - /// The slot this file was loaded from, or that a load attempt failed from. - /// If multiple load attempts failed and none succeeded, this refers to - /// any one attempted slot. - slot: SaveSlot; + /// The slot this file was loaded from, or that a load attempt failed from. + /// If multiple load attempts failed and none succeeded, this refers to + /// any one attempted slot. + slot: SaveSlot; } function readFromSlot(slot: SaveSlot): SaveFileV1LoadResult { - var serialized = localStorage.getItem(slot); - if (serialized === null) { - return { - file: null, - error: null, - slot: slot, - }; - } - try { - return { - file: mustBeSaveFileV1(JSON.parse(serialized)), - error: null, - slot: slot, - }; - } catch (e) { - let message = "unidentifiable error"; - if (e instanceof Error) { - message = e.message; - } - return { - file: null, - error: message, - slot: slot, - }; + var serialized = localStorage.getItem(slot); + if (serialized === null) { + return { + file: null, + error: null, + slot: slot, + }; + } + try { + return { + file: mustBeSaveFileV1(JSON.parse(serialized)), + error: null, + slot: slot, + }; + } catch (e) { + let message = "unidentifiable error"; + if (e instanceof Error) { + message = e.message; } + return { + file: null, + error: message, + slot: slot, + }; + } } /// Reads the more recent valid save file, if either is valid. If no save files /// are present, `file` and `error` will both be absent. If an invalid save /// file is discovered, `error` will refer to the issue(s) detected. function readBestSave(): SaveFileV1LoadResult { - const from1 = readFromSlot("FLEDGLING_SLOT_1"); - const from2 = readFromSlot("FLEDGLING_SLOT_2"); - if (from1.file && from2.file) { - return (from1.file.revision > from2.file.revision) ? from1 : from2; - } + const from1 = readFromSlot("FLEDGLING_SLOT_1"); + const from2 = readFromSlot("FLEDGLING_SLOT_2"); + if (from1.file && from2.file) { + return from1.file.revision > from2.file.revision ? from1 : from2; + } - var errors : string[] = []; - if (from1.error) { - errors = ["slot 1 error: " + from1.error]; - } - if (from2.error) { - errors.push("slot 2 error: " + from2.error); - } - var msg : string | null = errors.length > 0 ? errors.join("\n") : null; - if (from1.file) { - return { - file: from1.file, - error: msg, - slot: "FLEDGLING_SLOT_1", - }; - } + var errors: string[] = []; + if (from1.error) { + errors = ["slot 1 error: " + from1.error]; + } + if (from2.error) { + errors.push("slot 2 error: " + from2.error); + } + var msg: string | null = errors.length > 0 ? errors.join("\n") : null; + if (from1.file) { return { - file: from2.file, - error: msg, - slot: "FLEDGLING_SLOT_2", + file: from1.file, + error: msg, + slot: "FLEDGLING_SLOT_1", }; + } + return { + file: from2.file, + error: msg, + slot: "FLEDGLING_SLOT_2", + }; } export function save() { - const targetSlot : SaveSlot = (readBestSave().slot === "FLEDGLING_SLOT_1") ? "FLEDGLING_SLOT_2" : "FLEDGLING_SLOT_1"; - return saveIntoSlot(targetSlot); + const targetSlot: SaveSlot = + readBestSave().slot === "FLEDGLING_SLOT_1" + ? "FLEDGLING_SLOT_2" + : "FLEDGLING_SLOT_1"; + return saveIntoSlot(targetSlot); } -function extractCurrentState() : SaveFileV1 { - const progress = getPlayerProgress(); - const stateManager = getStateManager(); - var thrallDamage : number[] = []; - const nThralls = getThralls().length; - for (let i = 0; i < nThralls; ++i) { - thrallDamage.push(progress.getThrallDamage({id: i})); - } - return { - version: "fledgling_save_v1", - revision: stateManager.nextRevision(), - turn: stateManager.getTurn(), - name: progress.name, - thrallTemplateId: progress.template.id, - nImprovements: progress.nImprovements, - stats: { - agi: progress.getStat("AGI"), - int: progress.getStat("INT"), - cha: progress.getStat("CHA"), - psi: progress.getStat("PSI"), - }, - talents: { - agi: progress.getStat("AGI"), - int: progress.getStat("INT"), - cha: progress.getStat("CHA"), - psi: progress.getStat("PSI"), - }, - isInPenance: progress.isInPenance, - wishId: progress.getWish()?.id ?? -1, - exp: progress.getExperience(), - blood: progress.getBlood(), - itemsPurloined: progress.getItemsPurloined(), - skillsLearned: progress.getRawLearnedSkills(), - untrimmedSkillsAvailableIds: progress.getUntrimmedAvailableSkillIds(), - thrallsUnlocked: progress.getUnlockedThrallIds(), - thrallDamage: thrallDamage, - thrallsObtainedItem: progress.getThrallObtainedItemIds(), - thrallsDeliveredItem: progress.getThrallDeliveredItemIds(), - }; +function extractCurrentState(): SaveFileV1 { + const progress = getPlayerProgress(); + const stateManager = getStateManager(); + var thrallDamage: number[] = []; + const nThralls = getThralls().length; + for (let i = 0; i < nThralls; ++i) { + thrallDamage.push(progress.getThrallDamage({ id: i })); + } + return { + version: "fledgling_save_v1", + revision: stateManager.nextRevision(), + turn: stateManager.getTurn(), + name: progress.name, + thrallTemplateId: progress.template.id, + nImprovements: progress.nImprovements, + stats: { + agi: progress.getStat("AGI"), + int: progress.getStat("INT"), + cha: progress.getStat("CHA"), + psi: progress.getStat("PSI"), + }, + talents: { + agi: progress.getStat("AGI"), + int: progress.getStat("INT"), + cha: progress.getStat("CHA"), + psi: progress.getStat("PSI"), + }, + isInPenance: progress.isInPenance, + wishId: progress.getWish()?.id ?? -1, + exp: progress.getExperience(), + blood: progress.getBlood(), + itemsPurloined: progress.getItemsPurloined(), + skillsLearned: progress.getRawLearnedSkills(), + untrimmedSkillsAvailableIds: progress.getUntrimmedAvailableSkillIds(), + thrallsUnlocked: progress.getUnlockedThrallIds(), + thrallDamage: thrallDamage, + thrallsObtainedItem: progress.getThrallObtainedItemIds(), + thrallsDeliveredItem: progress.getThrallDeliveredItemIds(), + }; } function saveIntoSlot(slot: SaveSlot) { - localStorage.setItem(slot, JSON.stringify(extractCurrentState())); + localStorage.setItem(slot, JSON.stringify(extractCurrentState())); } export function wipeSaves() { - localStorage.removeItem("FLEDGLING_SLOT_1"); - localStorage.removeItem("FLEDGLING_SLOT_2"); -} \ No newline at end of file + localStorage.removeItem("FLEDGLING_SLOT_1"); + localStorage.removeItem("FLEDGLING_SLOT_2"); +} diff --git a/src/saveformat.ts b/src/saveformat.ts index c141c30..2c04f0b 100644 --- a/src/saveformat.ts +++ b/src/saveformat.ts @@ -1,180 +1,183 @@ -export interface StatCounterV1{ - agi: number; - int: number; - cha: number; - psi: number; +export interface StatCounterV1 { + agi: number; + int: number; + cha: number; + psi: number; } export interface SaveFileV1 { - version: "fledgling_save_v1"; - revision: number; + version: "fledgling_save_v1"; + revision: number; - turn: number; + turn: number; - name: string; - thrallTemplateId: number; - nImprovements: number; - stats: StatCounterV1; - talents: StatCounterV1; - isInPenance: boolean; - wishId: number; // negative: Wish is absent - exp: number; - blood: number; - itemsPurloined: number; - skillsLearned: number[]; - untrimmedSkillsAvailableIds: number[]; - thrallsUnlocked: number[]; - thrallDamage: number[]; // 0: thrall is absent or undamaged - thrallsObtainedItem: number[]; - thrallsDeliveredItem: number[]; + name: string; + thrallTemplateId: number; + nImprovements: number; + stats: StatCounterV1; + talents: StatCounterV1; + isInPenance: boolean; + wishId: number; // negative: Wish is absent + exp: number; + blood: number; + itemsPurloined: number; + skillsLearned: number[]; + untrimmedSkillsAvailableIds: number[]; + thrallsUnlocked: number[]; + thrallDamage: number[]; // 0: thrall is absent or undamaged + thrallsObtainedItem: number[]; + thrallsDeliveredItem: number[]; } /// Checks whether obj is a valid save file, as far as we can tell, and returns /// it unchanged if it is, or throws an error if it's not valid. export function mustBeSaveFileV1(obj: unknown): SaveFileV1 { - if (obj === undefined || obj === null) { - throw new Error("nonexistent"); - } - if (typeof(obj) !== "object") { - throw new Error(`not an object; was ${typeof(obj)}`); - } + if (obj === undefined || obj === null) { + throw new Error("nonexistent"); + } + if (typeof obj !== "object") { + throw new Error(`not an object; was ${typeof obj}`); + } - if (!("version" in obj)) { - throw new Error("no magic number"); - } - if (obj.version !== "fledgling_save_v1") { - throw new Error(`bad magic number: ${obj.version}`); - } + if (!("version" in obj)) { + throw new Error("no magic number"); + } + if (obj.version !== "fledgling_save_v1") { + throw new Error(`bad magic number: ${obj.version}`); + } + return { + version: "fledgling_save_v1", + revision: mustGetNumber(obj, "revision"), + turn: mustGetNumber(obj, "turn"), + name: mustGetString(obj, "name"), + thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"), + nImprovements: mustGetNumber(obj, "nImprovements"), + stats: mustGetStatCounterV1(obj, "stats"), + talents: mustGetStatCounterV1(obj, "talents"), + isInPenance: mustGetBoolean(obj, "isInPenance"), + wishId: mustGetNumber(obj, "wishId"), + exp: mustGetNumber(obj, "exp"), + blood: mustGetNumber(obj, "blood"), + itemsPurloined: mustGetNumber(obj, "itemsPurloined"), + skillsLearned: mustGetNumberArray(obj, "skillsLearned"), + untrimmedSkillsAvailableIds: mustGetNumberArray( + obj, + "untrimmedSkillsAvailableIds", + ), + thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"), + thrallDamage: mustGetNumberArray(obj, "thrallDamage"), + thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), + thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), + }; +} + +function mustGetNumber(obj: object, key: string): number { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "number") { + throw new Error(`not a number: ${key}: ${val}`); + } + return val; +} + +function mustGetString(obj: object, key: string): string { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "string") { + throw new Error(`not a string: ${key}: ${val}`); + } + return val; +} + +function mustGetStatCounterV1(obj: object, key: string): StatCounterV1 { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } + + try { return { - version: "fledgling_save_v1", - revision: mustGetNumber(obj, "revision"), - turn: mustGetNumber(obj, "turn"), - name: mustGetString(obj, "name"), - thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"), - nImprovements: mustGetNumber(obj, "nImprovements"), - stats: mustGetStatCounterV1(obj, "stats"), - talents: mustGetStatCounterV1(obj, "talents"), - isInPenance: mustGetBoolean(obj, "isInPenance"), - wishId: mustGetNumber(obj, "wishId"), - exp: mustGetNumber(obj, "exp"), - blood: mustGetNumber(obj, "blood"), - itemsPurloined: mustGetNumber(obj, "itemsPurloined"), - skillsLearned: mustGetNumberArray(obj, "skillsLearned"), - untrimmedSkillsAvailableIds: mustGetNumberArray(obj, "untrimmedSkillsAvailableIds"), - thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"), - thrallDamage: mustGetNumberArray(obj, "thrallDamage"), - thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), - thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), + agi: mustGetNumber(val, "agi"), + int: mustGetNumber(val, "int"), + cha: mustGetNumber(val, "cha"), + psi: mustGetNumber(val, "psi"), }; + } catch (e) { + let message = "unrecognizable error"; + if (e instanceof Error) { + message = e.message; + } + throw new Error(`reading ${key}: ${message}`); + } } -function mustGetNumber(obj: object, key: string) : number { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "number") { - throw new Error(`not a number: ${key}: ${val}`); - } - return val; +function mustGetBoolean(obj: object, key: string): boolean { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "boolean") { + throw new Error(`not boolean: ${key}: ${val}`); + } + return val; } -function mustGetString(obj: object, key: string) : string { - if (obj === null || obj === undefined) { - throw new Error("container absent"); +function mustGetNumberArray(obj: object, key: string): number[] { + if (obj === null || obj === undefined) { + throw new Error("container absent"); + } + if (typeof obj !== "object") { + throw new Error(`container was not an object; was ${typeof obj}`); + } + if (!(key in obj)) { + throw new Error(`missing number: ${key}`); + } + const dict = obj as { [key: string]: any }; + const val = dict[key]; + if (typeof val !== "object") { + throw new Error(`not an object: ${key}: ${val}`); + } + + for (const x of val) { + if (typeof x !== "number") { + throw new Error(`contained non-number item in ${key}: ${val}`); } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "string") { - throw new Error(`not a string: ${key}: ${val}`); - } - return val; + } + return val; } - -function mustGetStatCounterV1(obj: object, key: string) : StatCounterV1 { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "object") { - throw new Error(`not an object: ${key}: ${val}`); - } - - try { - return { - agi: mustGetNumber(val, "agi"), - int: mustGetNumber(val, "int"), - cha: mustGetNumber(val, "cha"), - psi: mustGetNumber(val, "psi"), - }; - } catch (e) { - let message = "unrecognizable error"; - if (e instanceof Error) { - message = e.message; - } - throw new Error(`reading ${key}: ${message}`); - } -} - -function mustGetBoolean(obj: object, key: string) : boolean { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "boolean") { - throw new Error(`not boolean: ${key}: ${val}`); - } - return val; -} - -function mustGetNumberArray(obj: object, key: string) : number[] { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof(obj) !== "object") { - throw new Error(`container was not an object; was ${typeof(obj)}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as {[key:string]:any}; - const val = dict[key]; - if (typeof(val) !== "object") { - throw new Error(`not an object: ${key}: ${val}`); - } - - for (const x of val) { - if (typeof(x) !== "number") { - throw new Error(`contained non-number item in ${key}: ${val}`); - } - } - return val; -} \ No newline at end of file diff --git a/src/statemanager.ts b/src/statemanager.ts index 7b09f44..060998b 100644 --- a/src/statemanager.ts +++ b/src/statemanager.ts @@ -18,7 +18,7 @@ export class StateManager { #turn: number; #revision: number; - constructor(file?:SaveFileV1) { + constructor(file?: SaveFileV1) { this.#turn = file?.turn ?? 1; this.#revision = file?.revision ?? 1; } -- 2.43.0 From 93ef5125542bfa35458855ef9be1a30410026ba4 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 23 Feb 2025 22:28:34 -0800 Subject: [PATCH 6/9] oops, forgot to save the split-up of save.ts --- src/save.ts | 155 +--------------------------------------------------- 1 file changed, 1 insertion(+), 154 deletions(-) diff --git a/src/save.ts b/src/save.ts index c147759..a0e80d8 100644 --- a/src/save.ts +++ b/src/save.ts @@ -1,7 +1,7 @@ import { getPlayerProgress } from "./playerprogress"; import { getStateManager } from "./statemanager"; import { getThralls } from "./thralls"; -import { SaveFileV1, StatCounterV1 } from "./saveformat"; +import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat"; export interface SaveFile { version: string; @@ -10,159 +10,6 @@ export interface SaveFile { type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2"; -/// Checks whether obj is a valid save file, as far as we can tell, and returns -/// it unchanged if it is, or throws an error if it's not valid. -export function mustBeSaveFileV1(obj: unknown): SaveFileV1 { - if (obj === undefined || obj === null) { - throw new Error("nonexistent"); - } - if (typeof obj !== "object") { - throw new Error(`not an object; was ${typeof obj}`); - } - - if (!("version" in obj)) { - throw new Error("no magic number"); - } - if (obj.version !== "fledgling_save_v1") { - throw new Error(`bad magic number: ${obj.version}`); - } - - return { - version: "fledgling_save_v1", - revision: mustGetNumber(obj, "revision"), - turn: mustGetNumber(obj, "turn"), - name: mustGetString(obj, "name"), - thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"), - nImprovements: mustGetNumber(obj, "nImprovements"), - stats: mustGetStatCounterV1(obj, "stats"), - talents: mustGetStatCounterV1(obj, "talents"), - isInPenance: mustGetBoolean(obj, "isInPenance"), - wishId: mustGetNumber(obj, "wishId"), - exp: mustGetNumber(obj, "exp"), - blood: mustGetNumber(obj, "blood"), - itemsPurloined: mustGetNumber(obj, "itemsPurloined"), - skillsLearned: mustGetNumberArray(obj, "skillsLearned"), - untrimmedSkillsAvailableIds: mustGetNumberArray( - obj, - "untrimmedSkillsAvailableIds", - ), - thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"), - thrallDamage: mustGetNumberArray(obj, "thrallDamage"), - thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), - thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), - }; -} - -function mustGetNumber(obj: object, key: string): number { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof obj !== "object") { - throw new Error(`container was not an object; was ${typeof obj}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as { [key: string]: any }; - const val = dict[key]; - if (typeof val !== "number") { - throw new Error(`not a number: ${key}: ${val}`); - } - return val; -} - -function mustGetString(obj: object, key: string): string { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof obj !== "object") { - throw new Error(`container was not an object; was ${typeof obj}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as { [key: string]: any }; - const val = dict[key]; - if (typeof val !== "string") { - throw new Error(`not a string: ${key}: ${val}`); - } - return val; -} - -function mustGetStatCounterV1(obj: object, key: string): StatCounterV1 { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof obj !== "object") { - throw new Error(`container was not an object; was ${typeof obj}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as { [key: string]: any }; - const val = dict[key]; - if (typeof val !== "object") { - throw new Error(`not an object: ${key}: ${val}`); - } - - try { - return { - agi: mustGetNumber(val, "agi"), - int: mustGetNumber(val, "int"), - cha: mustGetNumber(val, "cha"), - psi: mustGetNumber(val, "psi"), - }; - } catch (e) { - let message = "unrecognizable error"; - if (e instanceof Error) { - message = e.message; - } - throw new Error(`reading ${key}: ${message}`); - } -} - -function mustGetBoolean(obj: object, key: string): boolean { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof obj !== "object") { - throw new Error(`container was not an object; was ${typeof obj}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as { [key: string]: any }; - const val = dict[key]; - if (typeof val !== "boolean") { - throw new Error(`not boolean: ${key}: ${val}`); - } - return val; -} - -function mustGetNumberArray(obj: object, key: string): number[] { - if (obj === null || obj === undefined) { - throw new Error("container absent"); - } - if (typeof obj !== "object") { - throw new Error(`container was not an object; was ${typeof obj}`); - } - if (!(key in obj)) { - throw new Error(`missing number: ${key}`); - } - const dict = obj as { [key: string]: any }; - const val = dict[key]; - if (typeof val !== "object") { - throw new Error(`not an object: ${key}: ${val}`); - } - - for (const x of val) { - if (typeof x !== "number") { - throw new Error(`contained non-number item in ${key}: ${val}`); - } - } - return val; -} - /// The result of attempting to load a V1 save file. interface SaveFileV1LoadResult { // If present and valid, the loaded file. -- 2.43.0 From 6fe843bf5546cb7a21f92f776ecec98fc52124f5 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 23 Feb 2025 22:30:57 -0800 Subject: [PATCH 7/9] Save on end-of-day, or after endgame. Putting it here avoids a circular reference problem --- src/hotbar.ts | 2 ++ src/save.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hotbar.ts b/src/hotbar.ts index 612d35f..6ce230a 100644 --- a/src/hotbar.ts +++ b/src/hotbar.ts @@ -6,6 +6,7 @@ import { addButton } from "./button.ts"; import { getPlayerProgress } from "./playerprogress.ts"; import { getStateManager } from "./statemanager.ts"; import { getCheckModal } from "./checkmodal.ts"; +import { saveGame } from "./save.ts"; type Button = { label: string; @@ -112,6 +113,7 @@ export class Hotbar { }, () => { getStateManager().advance(); + saveGame(); }, ); } diff --git a/src/save.ts b/src/save.ts index a0e80d8..ec36afa 100644 --- a/src/save.ts +++ b/src/save.ts @@ -85,7 +85,7 @@ function readBestSave(): SaveFileV1LoadResult { }; } -export function save() { +export function saveGame() { const targetSlot: SaveSlot = readBestSave().slot === "FLEDGLING_SLOT_1" ? "FLEDGLING_SLOT_2" -- 2.43.0 From 2c121f0c8a29a66347566945e162e4d461b20c82 Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Mon, 24 Feb 2025 20:09:15 -0800 Subject: [PATCH 8/9] Integrate save system --- src/hotbar.ts | 2 - src/main.ts | 2 +- src/playerprogress.ts | 6 ++- src/save.ts | 13 +++--- src/saveformat.ts | 2 + src/statemanager.ts | 34 +++++++++++++++- src/vnmodal.ts | 94 ++++++++++++++++++++++++++++++++++++++++++- src/vnscene.ts | 12 +++++- 8 files changed, 152 insertions(+), 13 deletions(-) diff --git a/src/hotbar.ts b/src/hotbar.ts index 6ce230a..612d35f 100644 --- a/src/hotbar.ts +++ b/src/hotbar.ts @@ -6,7 +6,6 @@ import { addButton } from "./button.ts"; import { getPlayerProgress } from "./playerprogress.ts"; import { getStateManager } from "./statemanager.ts"; import { getCheckModal } from "./checkmodal.ts"; -import { saveGame } from "./save.ts"; type Button = { label: string; @@ -113,7 +112,6 @@ export class Hotbar { }, () => { getStateManager().advance(); - saveGame(); }, ); } diff --git a/src/main.ts b/src/main.ts index d81cd62..fd764fe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,5 +2,5 @@ import { hostGame } from "./engine/internal/host.ts"; import { game } from "./game.ts"; import { getStateManager } from "./statemanager.ts"; -getStateManager().startFirstGame(); +getStateManager().startOrLoadFirstGame(); hostGame(game); diff --git a/src/playerprogress.ts b/src/playerprogress.ts index a486d2d..bd923ab 100644 --- a/src/playerprogress.ts +++ b/src/playerprogress.ts @@ -27,7 +27,7 @@ export class PlayerProgress { #thrallsDeliveredItem: number[]; constructor(args: NewRoundConfig | SaveFileV1) { - if ("asSuccesor" in args) { + if ("asSuccessor" in args) { //asSuccessor: SuccessorOption, withWish: Wish | null) { const config = args as NewRoundConfig; const asSuccessor = config.asSuccessor; @@ -381,6 +381,10 @@ export function initPlayerProgress( active = new PlayerProgress({ asSuccessor: asSuccessor, withWish: withWish }); } +export function rehydratePlayerProgress(savefile: SaveFileV1) { + active = new PlayerProgress(savefile); +} + export function getPlayerProgress(): PlayerProgress { if (active == null) { throw new Error( diff --git a/src/save.ts b/src/save.ts index ec36afa..46d0e36 100644 --- a/src/save.ts +++ b/src/save.ts @@ -2,6 +2,7 @@ import { getPlayerProgress } from "./playerprogress"; import { getStateManager } from "./statemanager"; import { getThralls } from "./thralls"; import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat"; +import { getHuntMode } from "./huntmode.ts"; export interface SaveFile { version: string; @@ -56,7 +57,7 @@ function readFromSlot(slot: SaveSlot): SaveFileV1LoadResult { /// Reads the more recent valid save file, if either is valid. If no save files /// are present, `file` and `error` will both be absent. If an invalid save /// file is discovered, `error` will refer to the issue(s) detected. -function readBestSave(): SaveFileV1LoadResult { +export function readBestSave(): SaveFileV1LoadResult { const from1 = readFromSlot("FLEDGLING_SLOT_1"); const from2 = readFromSlot("FLEDGLING_SLOT_2"); if (from1.file && from2.file) { @@ -96,6 +97,7 @@ export function saveGame() { function extractCurrentState(): SaveFileV1 { const progress = getPlayerProgress(); const stateManager = getStateManager(); + const huntMode = getHuntMode(); var thrallDamage: number[] = []; const nThralls = getThralls().length; for (let i = 0; i < nThralls; ++i) { @@ -115,10 +117,10 @@ function extractCurrentState(): SaveFileV1 { psi: progress.getStat("PSI"), }, talents: { - agi: progress.getStat("AGI"), - int: progress.getStat("INT"), - cha: progress.getStat("CHA"), - psi: progress.getStat("PSI"), + agi: progress.getTalent("AGI"), + int: progress.getTalent("INT"), + cha: progress.getTalent("CHA"), + psi: progress.getTalent("PSI"), }, isInPenance: progress.isInPenance, wishId: progress.getWish()?.id ?? -1, @@ -131,6 +133,7 @@ function extractCurrentState(): SaveFileV1 { thrallDamage: thrallDamage, thrallsObtainedItem: progress.getThrallObtainedItemIds(), thrallsDeliveredItem: progress.getThrallDeliveredItemIds(), + depth: huntMode.getDepth(), }; } diff --git a/src/saveformat.ts b/src/saveformat.ts index 2c04f0b..762d93d 100644 --- a/src/saveformat.ts +++ b/src/saveformat.ts @@ -27,6 +27,7 @@ export interface SaveFileV1 { thrallDamage: number[]; // 0: thrall is absent or undamaged thrallsObtainedItem: number[]; thrallsDeliveredItem: number[]; + depth: number; } /// Checks whether obj is a valid save file, as far as we can tell, and returns @@ -69,6 +70,7 @@ export function mustBeSaveFileV1(obj: unknown): SaveFileV1 { thrallDamage: mustGetNumberArray(obj, "thrallDamage"), thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), + depth: mustGetNumber(obj, "depth"), }; } diff --git a/src/statemanager.ts b/src/statemanager.ts index 060998b..35e1efa 100644 --- a/src/statemanager.ts +++ b/src/statemanager.ts @@ -1,4 +1,8 @@ -import { getPlayerProgress, initPlayerProgress } from "./playerprogress.ts"; +import { + getPlayerProgress, + initPlayerProgress, + rehydratePlayerProgress, +} from "./playerprogress.ts"; import { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts"; import { getVNModal } from "./vnmodal.ts"; import { getScorer } from "./scorer.ts"; @@ -11,6 +15,7 @@ import { generateName } from "./namegen.ts"; import { photogenicThralls } from "./thralls.ts"; import { choose } from "./utils.ts"; import { SaveFileV1 } from "./saveformat.ts"; +import { readBestSave, saveGame } from "./save.ts"; const N_TURNS: number = 9; @@ -32,6 +37,23 @@ export class StateManager { return this.#revision; } + startOrLoadFirstGame() { + let save = readBestSave(); + if (save.file != null || save.error != null) { + const file = save.file; + const error = save.error; + getVNModal().play([ + { + type: "saveGameScreen", + file: file, + error: error, + }, + ]); + return; + } + + this.startFirstGame(); + } startFirstGame() { getVNModal().play([ ...openingScene, @@ -65,7 +87,17 @@ export class StateManager { sndSleep.play({ bgm: true }); } + resumeGame(saveFile: SaveFileV1) { + // hack: prepare depth which advance() uses + this.#turn = saveFile.turn; + this.#revision = saveFile.revision; + rehydratePlayerProgress(saveFile); + initHuntMode(new HuntMode(saveFile.depth, generateManor())); + this.advance(); + } + advance() { + saveGame(); if (this.#turn + 1 <= N_TURNS) { this.#turn += 1; getPlayerProgress().applyEndOfTurn(); diff --git a/src/vnmodal.ts b/src/vnmodal.ts index 7afd81a..46c83c0 100644 --- a/src/vnmodal.ts +++ b/src/vnmodal.ts @@ -1,8 +1,13 @@ import { D, I } from "./engine/public.ts"; -import { AlignX, AlignY, Point } from "./engine/datatypes.ts"; +import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; import { withCamera } from "./layout.ts"; import { VNScene, VNSceneMessage, VNScenePart } from "./vnscene.ts"; import { C } from "./colors.ts"; +import { DrawPile } from "./drawpile.ts"; +import { wipeSaves } from "./save.ts"; +import { SaveFileV1 } from "./saveformat.ts"; +import { addButton } from "./button.ts"; +import { getStateManager } from "./statemanager.ts"; const WIDTH = 384; const HEIGHT = 384; @@ -90,6 +95,8 @@ function createCathexis(part: VNScenePart): SceneCathexis { case "callback": part?.callback(); return new SkipCathexis(); + case "saveGameScreen": + return new SaveGameCathexis(part.file, part.error); } } @@ -112,7 +119,6 @@ class SceneMessageCathexis { let firstFrame = !this.#gotOneFrame; this.#gotOneFrame = true; - // TODO: SFX if (!firstFrame && I.isMouseClicked("leftMouse")) { this.#done = true; } @@ -152,3 +158,87 @@ class SkipCathexis { throw new Error("shouldn't ever be drawn"); } } + +class SaveGameCathexis { + #drawpile: DrawPile; + #file: SaveFileV1 | null; + #error: string | null; + #done: boolean; + + constructor(file: SaveFileV1 | null, error: string | null) { + this.#drawpile = new DrawPile(); + this.#file = file; + this.#error = error; + if (this.#error != null && this.#file != null) { + throw new Error("can't have a savefile _and_ an error"); + } + this.#done = false; + } + + isDone() { + return this.#done; + } + + update() { + let name = this.#file?.name; + let turn = this.#file?.turn ?? 0; + let turnText = turn < 9 ? `${name}, Turn ${turn + 1}` : "Sentence of Fate"; + this.#drawpile.clear(); + this.#drawpile.add(0, () => { + D.drawText( + this.#error + ? `Your save file was invalid and could not be loaded: + +${this.#error}` + : "Resume from save?", + new Point(WIDTH / 2, HEIGHT / 2), + C.FG_BOLD, + { + alignX: AlignX.Center, + alignY: AlignY.Middle, + forceWidth: WIDTH, + }, + ); + }); + + addButton( + this.#drawpile, + "Clear Save", + new Rect(new Point(0, HEIGHT - 32), new Size(128, 32)), + this.#file != null, + () => { + wipeSaves(); + this.#file = null; + }, + ); + if (this.#file) { + let file = this.#file; + addButton( + this.#drawpile, + `Continue (${turnText})`, + new Rect(new Point(128, HEIGHT - 32), new Size(WIDTH - 128, 32)), + true, + () => { + getStateManager().resumeGame(file); + this.#done = true; + }, + ); + } else { + addButton( + this.#drawpile, + `Start New Game`, + new Rect(new Point(128, HEIGHT - 32), new Size(WIDTH - 128, 32)), + true, + () => { + getStateManager().startFirstGame(); + this.#done = true; + }, + ); + } + this.#drawpile.executeOnClick(); + } + + draw() { + this.#drawpile.draw(); + } +} diff --git a/src/vnscene.ts b/src/vnscene.ts index 765c56f..90f3faa 100644 --- a/src/vnscene.ts +++ b/src/vnscene.ts @@ -1,4 +1,5 @@ import { Sound } from "./sound.ts"; +import { SaveFileV1 } from "./saveformat.ts"; export type VNSceneMessage = { type: "message"; @@ -11,9 +12,18 @@ export type VNSceneCallback = { callback: () => void; }; +export type VNSceneSaveGameScreen = { + type: "saveGameScreen"; + file: SaveFileV1 | null; + error: string | null; +}; + export type VNSceneBasisPart = string | VNSceneMessage | VNSceneCallback; export type VNSceneBasis = VNSceneBasisPart[]; -export type VNScenePart = VNSceneMessage | VNSceneCallback; +export type VNScenePart = + | VNSceneMessage + | VNSceneCallback + | VNSceneSaveGameScreen; export type VNScene = VNScenePart[]; export function compile(basis: VNSceneBasis): VNScene { -- 2.43.0 From a3c16e1acae1a75043c1055ed18615592744971e Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Mon, 24 Feb 2025 20:13:48 -0800 Subject: [PATCH 9/9] Deal with save corruption correctly --- src/vnmodal.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/vnmodal.ts b/src/vnmodal.ts index 46c83c0..a16a1e6 100644 --- a/src/vnmodal.ts +++ b/src/vnmodal.ts @@ -169,9 +169,6 @@ class SaveGameCathexis { this.#drawpile = new DrawPile(); this.#file = file; this.#error = error; - if (this.#error != null && this.#file != null) { - throw new Error("can't have a savefile _and_ an error"); - } this.#done = false; } @@ -186,8 +183,12 @@ class SaveGameCathexis { this.#drawpile.clear(); this.#drawpile.add(0, () => { D.drawText( - this.#error - ? `Your save file was invalid and could not be loaded: + this.#error && this.#file + ? `A save was invalid. Continue from an alternate save? + +${this.#error}` + : this.#error + ? `Your save was invalid: ${this.#error}` : "Resume from save?", -- 2.43.0