Save system: ceremonial PR #42
| @@ -6,7 +6,6 @@ import { addButton } from "./button.ts"; | |||||||
| import { getPlayerProgress } from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
| import { getStateManager } from "./statemanager.ts"; | import { getStateManager } from "./statemanager.ts"; | ||||||
| import { getCheckModal } from "./checkmodal.ts"; | import { getCheckModal } from "./checkmodal.ts"; | ||||||
| import { saveGame } from "./save.ts"; |  | ||||||
|  |  | ||||||
| type Button = { | type Button = { | ||||||
|   label: string; |   label: string; | ||||||
| @@ -113,7 +112,6 @@ export class Hotbar { | |||||||
|       }, |       }, | ||||||
|       () => { |       () => { | ||||||
|         getStateManager().advance(); |         getStateManager().advance(); | ||||||
|         saveGame(); |  | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ import { hostGame } from "./engine/internal/host.ts"; | |||||||
| import { game } from "./game.ts"; | import { game } from "./game.ts"; | ||||||
| import { getStateManager } from "./statemanager.ts"; | import { getStateManager } from "./statemanager.ts"; | ||||||
|  |  | ||||||
| getStateManager().startFirstGame(); | getStateManager().startOrLoadFirstGame(); | ||||||
| hostGame(game); | hostGame(game); | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ export class PlayerProgress { | |||||||
|   #thrallsDeliveredItem: number[]; |   #thrallsDeliveredItem: number[]; | ||||||
|  |  | ||||||
|   constructor(args: NewRoundConfig | SaveFileV1) { |   constructor(args: NewRoundConfig | SaveFileV1) { | ||||||
|     if ("asSuccesor" in args) { |     if ("asSuccessor" in args) { | ||||||
|       //asSuccessor: SuccessorOption, withWish: Wish | null) { |       //asSuccessor: SuccessorOption, withWish: Wish | null) { | ||||||
|       const config = args as NewRoundConfig; |       const config = args as NewRoundConfig; | ||||||
|       const asSuccessor = config.asSuccessor; |       const asSuccessor = config.asSuccessor; | ||||||
| @@ -381,6 +381,10 @@ export function initPlayerProgress( | |||||||
|   active = new PlayerProgress({ asSuccessor: asSuccessor, withWish: withWish }); |   active = new PlayerProgress({ asSuccessor: asSuccessor, withWish: withWish }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function rehydratePlayerProgress(savefile: SaveFileV1) { | ||||||
|  |   active = new PlayerProgress(savefile); | ||||||
|  | } | ||||||
|  |  | ||||||
| export function getPlayerProgress(): PlayerProgress { | export function getPlayerProgress(): PlayerProgress { | ||||||
|   if (active == null) { |   if (active == null) { | ||||||
|     throw new Error( |     throw new Error( | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								src/save.ts
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/save.ts
									
									
									
									
									
								
							| @@ -2,6 +2,7 @@ import { getPlayerProgress } from "./playerprogress"; | |||||||
| import { getStateManager } from "./statemanager"; | import { getStateManager } from "./statemanager"; | ||||||
| import { getThralls } from "./thralls"; | import { getThralls } from "./thralls"; | ||||||
| import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat"; | import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat"; | ||||||
|  | import { getHuntMode } from "./huntmode.ts"; | ||||||
|  |  | ||||||
| export interface SaveFile { | export interface SaveFile { | ||||||
|   version: string; |   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 | /// 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 | /// are present, `file` and `error` will both be absent. If an invalid save | ||||||
| /// file is discovered, `error` will refer to the issue(s) detected. | /// 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 from1 = readFromSlot("FLEDGLING_SLOT_1"); | ||||||
|   const from2 = readFromSlot("FLEDGLING_SLOT_2"); |   const from2 = readFromSlot("FLEDGLING_SLOT_2"); | ||||||
|   if (from1.file && from2.file) { |   if (from1.file && from2.file) { | ||||||
| @@ -96,6 +97,7 @@ export function saveGame() { | |||||||
| function extractCurrentState(): SaveFileV1 { | function extractCurrentState(): SaveFileV1 { | ||||||
|   const progress = getPlayerProgress(); |   const progress = getPlayerProgress(); | ||||||
|   const stateManager = getStateManager(); |   const stateManager = getStateManager(); | ||||||
|  |   const huntMode = getHuntMode(); | ||||||
|   var thrallDamage: number[] = []; |   var thrallDamage: number[] = []; | ||||||
|   const nThralls = getThralls().length; |   const nThralls = getThralls().length; | ||||||
|   for (let i = 0; i < nThralls; ++i) { |   for (let i = 0; i < nThralls; ++i) { | ||||||
| @@ -115,10 +117,10 @@ function extractCurrentState(): SaveFileV1 { | |||||||
|       psi: progress.getStat("PSI"), |       psi: progress.getStat("PSI"), | ||||||
|     }, |     }, | ||||||
|     talents: { |     talents: { | ||||||
|       agi: progress.getStat("AGI"), |       agi: progress.getTalent("AGI"), | ||||||
|       int: progress.getStat("INT"), |       int: progress.getTalent("INT"), | ||||||
|       cha: progress.getStat("CHA"), |       cha: progress.getTalent("CHA"), | ||||||
|       psi: progress.getStat("PSI"), |       psi: progress.getTalent("PSI"), | ||||||
|     }, |     }, | ||||||
|     isInPenance: progress.isInPenance, |     isInPenance: progress.isInPenance, | ||||||
|     wishId: progress.getWish()?.id ?? -1, |     wishId: progress.getWish()?.id ?? -1, | ||||||
| @@ -131,6 +133,7 @@ function extractCurrentState(): SaveFileV1 { | |||||||
|     thrallDamage: thrallDamage, |     thrallDamage: thrallDamage, | ||||||
|     thrallsObtainedItem: progress.getThrallObtainedItemIds(), |     thrallsObtainedItem: progress.getThrallObtainedItemIds(), | ||||||
|     thrallsDeliveredItem: progress.getThrallDeliveredItemIds(), |     thrallsDeliveredItem: progress.getThrallDeliveredItemIds(), | ||||||
|  |     depth: huntMode.getDepth(), | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ export interface SaveFileV1 { | |||||||
|   thrallDamage: number[]; // 0: thrall is absent or undamaged |   thrallDamage: number[]; // 0: thrall is absent or undamaged | ||||||
|   thrallsObtainedItem: number[]; |   thrallsObtainedItem: number[]; | ||||||
|   thrallsDeliveredItem: number[]; |   thrallsDeliveredItem: number[]; | ||||||
|  |   depth: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| /// 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 | ||||||
| @@ -69,6 +70,7 @@ export function mustBeSaveFileV1(obj: unknown): SaveFileV1 { | |||||||
|     thrallDamage: mustGetNumberArray(obj, "thrallDamage"), |     thrallDamage: mustGetNumberArray(obj, "thrallDamage"), | ||||||
|     thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), |     thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"), | ||||||
|     thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), |     thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"), | ||||||
|  |     depth: mustGetNumber(obj, "depth"), | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts"; | ||||||
| import { getVNModal } from "./vnmodal.ts"; | import { getVNModal } from "./vnmodal.ts"; | ||||||
| import { getScorer } from "./scorer.ts"; | import { getScorer } from "./scorer.ts"; | ||||||
| @@ -11,6 +15,7 @@ import { generateName } from "./namegen.ts"; | |||||||
| import { photogenicThralls } from "./thralls.ts"; | import { photogenicThralls } from "./thralls.ts"; | ||||||
| import { choose } from "./utils.ts"; | import { choose } from "./utils.ts"; | ||||||
| import { SaveFileV1 } from "./saveformat.ts"; | import { SaveFileV1 } from "./saveformat.ts"; | ||||||
|  | import { readBestSave, saveGame } from "./save.ts"; | ||||||
|  |  | ||||||
| const N_TURNS: number = 9; | const N_TURNS: number = 9; | ||||||
|  |  | ||||||
| @@ -32,6 +37,23 @@ export class StateManager { | |||||||
|     return this.#revision; |     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() { |   startFirstGame() { | ||||||
|     getVNModal().play([ |     getVNModal().play([ | ||||||
|       ...openingScene, |       ...openingScene, | ||||||
| @@ -65,7 +87,17 @@ export class StateManager { | |||||||
|     sndSleep.play({ bgm: true }); |     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() { |   advance() { | ||||||
|  |     saveGame(); | ||||||
|     if (this.#turn + 1 <= N_TURNS) { |     if (this.#turn + 1 <= N_TURNS) { | ||||||
|       this.#turn += 1; |       this.#turn += 1; | ||||||
|       getPlayerProgress().applyEndOfTurn(); |       getPlayerProgress().applyEndOfTurn(); | ||||||
|   | |||||||
| @@ -1,8 +1,13 @@ | |||||||
| import { D, I } from "./engine/public.ts"; | 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 { withCamera } from "./layout.ts"; | ||||||
| import { VNScene, VNSceneMessage, VNScenePart } from "./vnscene.ts"; | import { VNScene, VNSceneMessage, VNScenePart } from "./vnscene.ts"; | ||||||
| import { C } from "./colors.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 WIDTH = 384; | ||||||
| const HEIGHT = 384; | const HEIGHT = 384; | ||||||
| @@ -90,6 +95,8 @@ function createCathexis(part: VNScenePart): SceneCathexis { | |||||||
|     case "callback": |     case "callback": | ||||||
|       part?.callback(); |       part?.callback(); | ||||||
|       return new SkipCathexis(); |       return new SkipCathexis(); | ||||||
|  |     case "saveGameScreen": | ||||||
|  |       return new SaveGameCathexis(part.file, part.error); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -112,7 +119,6 @@ class SceneMessageCathexis { | |||||||
|     let firstFrame = !this.#gotOneFrame; |     let firstFrame = !this.#gotOneFrame; | ||||||
|     this.#gotOneFrame = true; |     this.#gotOneFrame = true; | ||||||
|  |  | ||||||
|     // TODO: SFX |  | ||||||
|     if (!firstFrame && I.isMouseClicked("leftMouse")) { |     if (!firstFrame && I.isMouseClicked("leftMouse")) { | ||||||
|       this.#done = true; |       this.#done = true; | ||||||
|     } |     } | ||||||
| @@ -152,3 +158,87 @@ class SkipCathexis { | |||||||
|     throw new Error("shouldn't ever be drawn"); |     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(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { Sound } from "./sound.ts"; | import { Sound } from "./sound.ts"; | ||||||
|  | import { SaveFileV1 } from "./saveformat.ts"; | ||||||
|  |  | ||||||
| export type VNSceneMessage = { | export type VNSceneMessage = { | ||||||
|   type: "message"; |   type: "message"; | ||||||
| @@ -11,9 +12,18 @@ export type VNSceneCallback = { | |||||||
|   callback: () => void; |   callback: () => void; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export type VNSceneSaveGameScreen = { | ||||||
|  |   type: "saveGameScreen"; | ||||||
|  |   file: SaveFileV1 | null; | ||||||
|  |   error: string | null; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export type VNSceneBasisPart = string | VNSceneMessage | VNSceneCallback; | export type VNSceneBasisPart = string | VNSceneMessage | VNSceneCallback; | ||||||
| export type VNSceneBasis = VNSceneBasisPart[]; | export type VNSceneBasis = VNSceneBasisPart[]; | ||||||
| export type VNScenePart = VNSceneMessage | VNSceneCallback; | export type VNScenePart = | ||||||
|  |   | VNSceneMessage | ||||||
|  |   | VNSceneCallback | ||||||
|  |   | VNSceneSaveGameScreen; | ||||||
| export type VNScene = VNScenePart[]; | export type VNScene = VNScenePart[]; | ||||||
|  |  | ||||||
| export function compile(basis: VNSceneBasis): VNScene { | export function compile(basis: VNSceneBasis): VNScene { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user