Save system: ceremonial PR #42
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										486
									
								
								src/save.ts
									
									
									
									
									
								
							
							
						
						
									
										486
									
								
								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"); | ||||
| } | ||||
|   localStorage.removeItem("FLEDGLING_SLOT_1"); | ||||
|   localStorage.removeItem("FLEDGLING_SLOT_2"); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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; | ||||
|   } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user