Save system: ceremonial PR #42
| @@ -1,6 +1,12 @@ | |||||||
| import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts"; | import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts"; | ||||||
| import { getSkills } from "./skills.ts"; | import { getSkills } from "./skills.ts"; | ||||||
| import { getThralls, ItemStage, LifeStage, Thrall } from "./thralls.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 { | export class PlayerProgress { | ||||||
|   #name: string; |   #name: string; | ||||||
| @@ -20,14 +26,18 @@ export class PlayerProgress { | |||||||
|   #thrallsObtainedItem: number[]; |   #thrallsObtainedItem: number[]; | ||||||
|   #thrallsDeliveredItem: number[]; |   #thrallsDeliveredItem: number[]; | ||||||
|  |  | ||||||
|   constructor(asSuccessor: SuccessorOption, withWish: Wish | null) { |   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.#name = asSuccessor.name; | ||||||
|       this.#thrallTemplate = asSuccessor.template.id; |       this.#thrallTemplate = asSuccessor.template.id; | ||||||
|       this.#nImprovements = asSuccessor.nImprovements; |       this.#nImprovements = asSuccessor.nImprovements; | ||||||
|       this.#stats = { ...asSuccessor.stats }; |       this.#stats = { ...asSuccessor.stats }; | ||||||
|       this.#talents = { ...asSuccessor.talents }; |       this.#talents = { ...asSuccessor.talents }; | ||||||
|       this.#isInPenance = asSuccessor.inPenance; |       this.#isInPenance = asSuccessor.inPenance; | ||||||
|     this.#wish = withWish; |       this.#wish = config.withWish; | ||||||
|       this.#exp = 0; |       this.#exp = 0; | ||||||
|       this.#blood = 0; |       this.#blood = 0; | ||||||
|       this.#itemsPurloined = 0; |       this.#itemsPurloined = 0; | ||||||
| @@ -39,6 +49,38 @@ export class PlayerProgress { | |||||||
|       this.#thrallsDeliveredItem = []; |       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() { |   applyEndOfTurn() { | ||||||
| @@ -332,7 +374,7 @@ export function initPlayerProgress( | |||||||
|   asSuccessor: SuccessorOption, |   asSuccessor: SuccessorOption, | ||||||
|   withWish: Wish | null, |   withWish: Wish | null, | ||||||
| ) { | ) { | ||||||
|   active = new PlayerProgress(asSuccessor, withWish); |   active = new PlayerProgress({asSuccessor:asSuccessor, withWish:withWish}); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getPlayerProgress(): PlayerProgress { | export function getPlayerProgress(): PlayerProgress { | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								src/save.ts
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								src/save.ts
									
									
									
									
									
								
							| @@ -1,44 +1,14 @@ | |||||||
| import { getPlayerProgress } from "./playerprogress" | import { getPlayerProgress } from "./playerprogress" | ||||||
| import { getStateManager } from "./statemanager"; | import { getStateManager } from "./statemanager"; | ||||||
| import { getThralls } from "./thralls"; | import { getThralls } from "./thralls"; | ||||||
|  | import {SaveFileV1, StatCounterV1} from "./saveformat"; | ||||||
|  |  | ||||||
| export interface SaveFile { | export interface SaveFile { | ||||||
|     version: string; |     version: string; | ||||||
|     revision: number; |     revision: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface StatCounterV1{ | type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2"; | ||||||
|     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 | /// 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. | /// it unchanged if it is, or throws an error if it's not valid. | ||||||
|   | |||||||
							
								
								
									
										180
									
								
								src/saveformat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/saveformat.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user