From 9af9c7867a0ba56ce34e7c0d4dcc67ca35c7eef1 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Sat, 7 Feb 2026 20:47:55 +0300 Subject: [PATCH] save/load to localStorage --- src/model/calendar.ts | 34 ++++++++++++--- src/model/game.ts | 28 ++++++++++++ src/model/owner.ts | 30 ++++++++++--- src/model/progress.ts | 56 +++++++++++++++++++----- src/model/school.ts | 74 +++++++++++++++++++++++++------- src/model/student.ts | 35 ++++++++++++--- src/model/time.ts | 28 ++++++++++++ src/model/upgrade.ts | 31 ++++++++++--- src/prefabs/upgrades.ts | 4 +- src/{model => types}/unit.ts | 2 +- src/utils/gameClock.ts | 43 +++++++++++++++---- src/utils/mobx/externalSignal.ts | 21 +++++++++ src/views copy/UnitListView.tsx | 22 ++++++++++ src/views/DebugView.tsx | 70 +++++++++++++++++++++++------- src/views/OwnerUnitView.tsx | 3 +- src/views/StatsView.tsx | 6 +-- src/views/UnitListView.tsx | 8 ++-- src/views/UpgradeListView.tsx | 4 +- 18 files changed, 409 insertions(+), 90 deletions(-) create mode 100644 src/model/game.ts create mode 100644 src/model/time.ts rename src/{model => types}/unit.ts (72%) create mode 100644 src/utils/mobx/externalSignal.ts create mode 100644 src/views copy/UnitListView.tsx diff --git a/src/model/calendar.ts b/src/model/calendar.ts index 42b135f..4dcc272 100644 --- a/src/model/calendar.ts +++ b/src/model/calendar.ts @@ -1,24 +1,44 @@ import { makeAutoObservable } from 'mobx'; -import { school } from './school'; -import { Progress } from './progress'; +import { Progress, ProgressDto } from './progress'; +import { game } from './game'; + +export type CalendarDto = { + day: number, + progress: ProgressDto, +} export class Calendar { public day: number = 1; - - public progress = new Progress(24, 1, true, () => this.handleProgress()); + + public progress = new Progress(24, 1, true, this.handleProgress.bind(this)); constructor() { makeAutoObservable(this); } - private handleProgress(): boolean { - this.day++; - school.collectStudentGold(); + private handleProgress(times: number): boolean { + this.day += times; + game.school.collectStudentGold(times); return true; } public get dayFraction(): number { return Math.floor(this.progress.value); } + + public serialize(): CalendarDto { + return { + day: this.day, + progress: this.progress.serialize(), + }; + } + + public static deserialize(data: CalendarDto): Calendar { + const calendar = new Calendar(); + + calendar.progress = Progress.deserialize(data.progress, true, calendar.handleProgress.bind(calendar)); + + return calendar; + } } \ No newline at end of file diff --git a/src/model/game.ts b/src/model/game.ts new file mode 100644 index 0000000..74627b3 --- /dev/null +++ b/src/model/game.ts @@ -0,0 +1,28 @@ +import { makeAutoObservable } from 'mobx'; +import { School, SchoolDto } from './school'; +import { GameClock } from '../utils/gameClock'; + +export class Game { + + public school = new School(); + + constructor() { + makeAutoObservable(this); + } + + public save(): void { + const data = this.school.serialize(); + localStorage.setItem('save', JSON.stringify(data)) + } + + public load(): void { + const json = localStorage.getItem('save'); + if (!json) + return; + + const data: SchoolDto = JSON.parse(json); + this.school = School.deserialize(data); + } +} + +export const game = new Game(); \ No newline at end of file diff --git a/src/model/owner.ts b/src/model/owner.ts index 49f03b2..08307f7 100644 --- a/src/model/owner.ts +++ b/src/model/owner.ts @@ -1,21 +1,39 @@ -import { Progress } from './progress'; -import { school } from './school'; -import { IUnit } from './unit'; +import { game } from './game'; +import { Progress, ProgressDto } from './progress'; +import { IUnit } from '../types/unit'; import { makeAutoObservable } from 'mobx'; +export type OwnerDto = { + progress: ProgressDto, +} + export class Owner implements IUnit { public readonly id = 'owner'; - public progress = new Progress(2, 1, false, () => this.handleProgress()); + public progress = new Progress(2, 1, false, this.handleProgress.bind(this)); constructor() { makeAutoObservable(this); } - private handleProgress(): boolean { - school.addReputation(1); + private handleProgress(times: number): boolean { + game.school.addReputation(times); return true; } + + public serialize(): OwnerDto { + return { + progress: this.progress.serialize(), + } + } + + public static deserialize(data: OwnerDto): Owner { + const owner = new Owner(); + + owner.progress = Progress.deserialize(data.progress, false, owner.handleProgress.bind(owner)); + + return owner; + } } diff --git a/src/model/progress.ts b/src/model/progress.ts index ccc514d..594f433 100644 --- a/src/model/progress.ts +++ b/src/model/progress.ts @@ -1,5 +1,11 @@ import { makeAutoObservable } from 'mobx'; +export type ProgressDto = { + length: number; + speed: number; + value: number; +} + export class Progress { public value: number = 0; @@ -8,7 +14,7 @@ export class Progress { public length: number, public speed: number, public readonly autoRun: boolean, - private callback: () => boolean, + private callback: (times: number) => void, ) { makeAutoObservable(this); } @@ -26,19 +32,49 @@ export class Progress { } public tick(time: number): void { - if (this.isFull && this.autoRun) { - this.run(); - } + const newValue = this.value + time / this.speed; - this.value = Math.min(this.length, this.value + time / this.speed); - if (this.autoRun) - this.run(); + if (this.autoRun) { + const rem = newValue % this.length; + const times = (newValue - rem) / this.length; + this.value = rem; + + if (times > 0) + this.run(times); + } + else + this.value = Math.min(this.length, newValue); } - public run(): void { + private run(times: number): void { + this.callback(times) + } + + public manualRun(): void { + if (this.autoRun) + return; + if (this.isFull) { - if (this.callback()) - this.reset(); + this.run(1) + this.reset(); } } + + public serialize(): ProgressDto { + return { + length: this.length, + speed: this.speed, + value: this.value, + }; + } + + public static deserialize( + data: ProgressDto, + autoRun: boolean, + callback: (times: number) => void, + ): Progress { + const inst = new Progress(data.length, data.speed, autoRun, callback); + inst.value = data.value; + return inst; + } } diff --git a/src/model/school.ts b/src/model/school.ts index 91e78f6..cb065be 100644 --- a/src/model/school.ts +++ b/src/model/school.ts @@ -1,22 +1,34 @@ import { makeAutoObservable } from 'mobx'; -import { Owner } from './owner'; -import { Student } from './student'; -import { IUnit } from './unit'; +import { Owner, OwnerDto } from './owner'; +import { Student, StudentDto } from './student'; +import { IUnit } from '../types/unit'; import { Upgrades } from './upgrades'; import { PartialResourceSet, ResourceSet } from '../types/resources'; -import { addResourceSets, fullResourceSet, mapResourceSet, multiplyResourceSet, roundResourceSet, subtractResourceSets } from '../utils/resources'; +import { addResourceSets, fullResourceSet, multiplyResourceSet, roundResourceSet, subtractResourceSets } from '../utils/resources'; import { v7 as uuid } from 'uuid'; import { GameClock } from '../utils/gameClock'; -import { Calendar } from './calendar'; +import { Calendar, CalendarDto } from './calendar'; import { getRandomHumanName } from '../utils/humanNames'; +import { Time as Time } from './time'; +import { UpgradeDto } from './upgrade'; const resourceScale = 100; -export class School { +export type SchoolDto = { + lastTime: number, + calendar: CalendarDto, + resources: ResourceSet, + units: { + owner: OwnerDto, + students: StudentDto[], + }, + upgrades: UpgradeDto[], +} - public timeScale: number = 1; +export class School { public tps: number = 0; + public time = new Time(); public calendar = new Calendar(); // units @@ -38,7 +50,7 @@ export class School { } public tick(time: number) { - const scaledTime = time * this.timeScale; + const scaledTime = time; this.calendar.progress.tick(scaledTime); @@ -48,10 +60,6 @@ export class School { this.tps = GameClock.tps; } - public setTimeScale(value: number) { - this.timeScale = value; - } - public get units(): IUnit[] { return [this.owner, ...this.students] } @@ -81,9 +89,43 @@ export class School { this.scaledResources = subtractResourceSets(this.scaledResources, multiplyResourceSet(resources, resourceScale)); } - public collectStudentGold(): void { - this.addGold(this.students.length); + public collectStudentGold(times: number): void { + this.addGold(this.students.length * times); + } + + public serialize(): SchoolDto { + return { + lastTime: GameClock.lastTime, + + calendar: this.calendar.serialize(), + resources: this.scaledResources, + + units: { + owner: this.owner.serialize(), + students: this.students.map((s) => s.serialize()), + }, + + upgrades: this.upgrades.allItems.map((up) => up.serialize()), + } + } + + public static deserialize(data: SchoolDto): School { + const inst = new School(); + + GameClock.lastTime = data.lastTime; + + inst.calendar = Calendar.deserialize(data.calendar); + inst.scaledResources = data.resources; + + inst.owner = Owner.deserialize(data.units.owner); + inst.students = data.units.students.map((s) => Student.deserialize(s)); + + for (let up of inst.upgrades.allItems) { + let upgradeData = data.upgrades.find((u) => u.id === up.id); + if (upgradeData) + up.deserialize(upgradeData); + } + + return inst; } } - -export const school = new School(); diff --git a/src/model/student.ts b/src/model/student.ts index 1506fb4..9910a03 100644 --- a/src/model/student.ts +++ b/src/model/student.ts @@ -1,11 +1,18 @@ -import { Progress } from './progress'; -import { school } from './school'; -import { IUnit } from './unit'; +import { game } from './game'; +import { Progress, ProgressDto } from './progress'; +import { School } from './school'; +import { IUnit } from '../types/unit'; import { makeAutoObservable } from 'mobx'; +export type StudentDto = { + id: string, + name: string, + progress: ProgressDto, +} + export class Student implements IUnit { - public progress = new Progress(10, 1, true, () => this.handleProgress()); + public progress = new Progress(10, 1, true, this.handleProgress.bind(this)); constructor( public readonly id: string, @@ -14,9 +21,25 @@ export class Student implements IUnit { makeAutoObservable(this); } - private handleProgress(): boolean { - school.addReputation(0.1); + private handleProgress(times: number): boolean { + game.school.addReputation(0.1 * times); return true; } + + public serialize(): StudentDto { + return { + id: this.id, + name: this.name, + progress: this.progress.serialize(), + } + } + + public static deserialize(data: StudentDto): Student { + const student = new Student(data.id, data.name); + + student.progress = Progress.deserialize(data.progress, true, student.handleProgress.bind(student)); + + return student; + } } diff --git a/src/model/time.ts b/src/model/time.ts new file mode 100644 index 0000000..ac42e9f --- /dev/null +++ b/src/model/time.ts @@ -0,0 +1,28 @@ +import { makeAutoObservable } from 'mobx'; +import { GameClock } from '../utils/gameClock'; + +export class Time { + + public timeScale: number = 1; + + constructor() { + makeAutoObservable(this); + } + + public setTimeScale(value: number) { + this.timeScale = value; + } + + public get paused(): boolean { + GameClock.pausedSignal.reportObserved(); + return GameClock.paused; + } + + public pause() { + GameClock.pause(); + } + + public resume() { + GameClock.resume(); + } +} \ No newline at end of file diff --git a/src/model/upgrade.ts b/src/model/upgrade.ts index dff3a6d..f195a57 100644 --- a/src/model/upgrade.ts +++ b/src/model/upgrade.ts @@ -1,9 +1,14 @@ -import { UpgradePrefab } from "../prefabs/upgrades"; +import { UpgradePrefab, upgrades } from "../prefabs/upgrades"; import { addResourceSets, fullResourceSet, isResourceSetGte } from "../utils/resources"; import { evalLevelValue } from "../utils/upgrades"; -import { school } from "./school"; import { ResourceSet } from "../types/resources"; import { makeAutoObservable } from "mobx"; +import { game } from "./game"; + +export type UpgradeDto = { + id: string, + level: number, +} export class Upgrade { public readonly id: string; @@ -83,20 +88,32 @@ export class Upgrade { let cost = this.costToBuy; this.prefab.execute(this.level); - school.removeResources(cost); - + game.school.removeResources(cost); + this.increaseLevel(); } public isVisible(): boolean { - return isResourceSetGte(school.resources, this.costToView); + return isResourceSetGte(game.school.resources, this.costToView); } public isOpen(): boolean { - return isResourceSetGte(school.resources, this.costToOpen); + return isResourceSetGte(game.school.resources, this.costToOpen); } public isAffordable(): boolean { - return isResourceSetGte(school.resources, this.costToBuy); + return isResourceSetGte(game.school.resources, this.costToBuy); + } + + public serialize(): UpgradeDto { + return { + id: this.id, + level: this.level, + } + } + + public deserialize(data: UpgradeDto): void { + + this.level = data.level; } } \ No newline at end of file diff --git a/src/prefabs/upgrades.ts b/src/prefabs/upgrades.ts index d220d0f..43bd79e 100644 --- a/src/prefabs/upgrades.ts +++ b/src/prefabs/upgrades.ts @@ -1,4 +1,4 @@ -import { school } from "../model/school"; +import { game } from "../model/game"; import { PartialResourceSet, Resource } from "../types/resources"; import { LevelValue, MaybeLevelValue } from "../types/upgrades"; import { fullResourceSet } from "../utils/resources"; @@ -23,7 +23,7 @@ export const upgrades: UpgradePrefab[] = [ costToOpen: (level) => ({ reputation: 20 * (level / 10 + 1) }), costToBuy: (level) => ({ gold: 10 }), execute: (level) => { - school.increaseStudentCount(); + game.school.increaseStudentCount(); }, }, { diff --git a/src/model/unit.ts b/src/types/unit.ts similarity index 72% rename from src/model/unit.ts rename to src/types/unit.ts index d3aad92..afe6dab 100644 --- a/src/model/unit.ts +++ b/src/types/unit.ts @@ -1,5 +1,5 @@ import { makeAutoObservable } from 'mobx'; -import { Progress } from './progress'; +import { Progress } from '../model/progress'; export interface IUnit { get id(): string; diff --git a/src/utils/gameClock.ts b/src/utils/gameClock.ts index 5905335..af9009c 100644 --- a/src/utils/gameClock.ts +++ b/src/utils/gameClock.ts @@ -1,8 +1,13 @@ -import { school } from "../model/school"; +import { game } from "../model/game"; +import { ExternalSignal } from "./mobx/externalSignal"; export class GameClock { + public static lastTime = 0; private static frames = new Array(); + public static paused: boolean = false; + + public static readonly pausedSignal = new ExternalSignal('GameClock.paused'); // public static async start(): Promise { // let lastTime = performance.now(); @@ -18,17 +23,18 @@ export class GameClock { // } public static start(): void { - let lastTime = performance.now(); - const loop = (now: number) => { - const delta = now - lastTime; - lastTime = now; + let delta = now - GameClock.lastTime; + GameClock.lastTime = now; - GameClock.frames.push(lastTime); - while (GameClock.frames[0] < lastTime - 1000) + if (GameClock.paused) + delta *= 0; + + GameClock.frames.push(now); + while (GameClock.frames[0] < now - 1000) GameClock.frames.shift(); - school.tick(delta / 1000); + GameClock.tick(delta / 1000 * game.school.time.timeScale); requestAnimationFrame(loop); }; @@ -36,6 +42,10 @@ export class GameClock { requestAnimationFrame(loop); } + public static tick(time: number): void { + game.school.tick(time); + } + public static get tps(): number { if (GameClock.frames.length < 2) return 0; @@ -44,4 +54,21 @@ export class GameClock { return Math.round(GameClock.frames.length / timeSpan * 1000); } + + public static skipTime(skipTime: number) { + if (skipTime < 0) + return; + + GameClock.tick(skipTime); + } + + public static pause() { + GameClock.paused = true; + GameClock.pausedSignal.trigger(); + } + + public static resume() { + GameClock.paused = false; + GameClock.pausedSignal.trigger(); + } } diff --git a/src/utils/mobx/externalSignal.ts b/src/utils/mobx/externalSignal.ts new file mode 100644 index 0000000..5ff16d9 --- /dev/null +++ b/src/utils/mobx/externalSignal.ts @@ -0,0 +1,21 @@ +import { createAtom, IAtom } from "mobx"; + +export class ExternalSignal { + private readonly atom: IAtom; + + constructor(name: string) { + this.atom = createAtom( + name, + () => { }, // onBecomeObserved + () => { } // onBecomeUnobserved + ) + } + + reportObserved() { + this.atom.reportObserved(); + } + + trigger() { + this.atom.reportChanged(); + } +} diff --git a/src/views copy/UnitListView.tsx b/src/views copy/UnitListView.tsx new file mode 100644 index 0000000..27a782b --- /dev/null +++ b/src/views copy/UnitListView.tsx @@ -0,0 +1,22 @@ +import { observer } from "mobx-react-lite"; +import { school } from "../model/school"; +import { Student } from "../model/student"; +import { Owner } from "../model/owner"; +import { OwnerUnitView } from "./OwnerUnitView"; +import { IUnit } from "../types/unit"; + +export const UnitListView = observer(function () { + + function renderUnit(unit: IUnit) { + if (unit instanceof Owner) + return + // // else if (unit instanceof Student) + // // return StudentView({ unit }); + else + return '???'; + } + + return
    { + school.units.map((unit) =>
  • {renderUnit(unit)}
  • ) + }
+}); diff --git a/src/views/DebugView.tsx b/src/views/DebugView.tsx index 8d7992c..de882e9 100644 --- a/src/views/DebugView.tsx +++ b/src/views/DebugView.tsx @@ -1,27 +1,65 @@ import { observer } from "mobx-react-lite"; -import { school } from "../model/school"; -import { ChangeEvent } from "react"; +import { ChangeEvent, useState } from "react"; +import { GameClock } from "../utils/gameClock"; +import { game } from "../model/game"; export const DebugView = observer(function () { + const [skipTime, setSkipTime] = useState(5); + function handleTimeScaleChange(event: ChangeEvent): void { - school.setTimeScale(Number(event.target.value)); + game.school.time.setTimeScale(Number(event.target.value)); + } + + function handleSkipTime(): void { + GameClock.skipTime(skipTime); + } + + function handleSaveClick(): void { + game.save(); + } + + function handleLoadClick(): void { + game.school.time.pause(); + game.load(); + } + + function handleTickClick(): void { + GameClock.pause(); + GameClock.tick(0.1); } return
-
- Масштаб времени - - x{school.timeScale} +
+ Время +
+ Масштаб + + x{game.school.time.timeScale} +
+ { + game.school.time.paused + ? + : + } + +
-
TPS: {school.tps}
+
+ + +
+
TPS: {game.school.tps}
}); diff --git a/src/views/OwnerUnitView.tsx b/src/views/OwnerUnitView.tsx index 9d4c70e..1a41c20 100644 --- a/src/views/OwnerUnitView.tsx +++ b/src/views/OwnerUnitView.tsx @@ -1,6 +1,5 @@ import { observer } from "mobx-react-lite"; import { Owner } from "../model/owner"; -import { school } from "../model/school"; type Props = { unit: Owner; @@ -9,7 +8,7 @@ type Props = { export const OwnerUnitView = observer(function ({ unit }: Props) { function handleButtonClick(): void { if (unit.progress.isFull) { - unit.progress.run(); + unit.progress.manualRun(); } } diff --git a/src/views/StatsView.tsx b/src/views/StatsView.tsx index 342c224..9a85624 100644 --- a/src/views/StatsView.tsx +++ b/src/views/StatsView.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react-lite"; -import { school } from "../model/school"; import { ResourceStringView } from "./ResourceStringView"; +import { game } from "../model/game"; export const StatsView = observer(function () { return
- -
День {school.calendar.day} : { school.calendar.dayFraction }
+ +
День {game.school.calendar.day} : { game.school.calendar.dayFraction }
}); diff --git a/src/views/UnitListView.tsx b/src/views/UnitListView.tsx index c9686a0..3b5dd5a 100644 --- a/src/views/UnitListView.tsx +++ b/src/views/UnitListView.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; -import { school } from "../model/school"; import { Student } from "../model/student"; import { Owner } from "../model/owner"; import { OwnerUnitView } from "./OwnerUnitView"; -import { IUnit } from "../model/unit"; +import { IUnit } from "../types/unit"; import { StudentUnitView } from "./StudentUnitView"; +import { game } from "../model/game"; export const UnitListView = observer(function () { @@ -19,10 +19,10 @@ export const UnitListView = observer(function () { return
- +
- {school.students.map((unit) => )} + {game.school.students.map((unit) => )}
}); diff --git a/src/views/UpgradeListView.tsx b/src/views/UpgradeListView.tsx index 2f543dd..fac64fb 100644 --- a/src/views/UpgradeListView.tsx +++ b/src/views/UpgradeListView.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; -import { school } from "../model/school"; import { UpgradeView } from "./UpgradeView"; +import { game } from "../model/game"; export const UpgradeListView = observer(function () { return
{ - school.upgrades.visibleItems.map((upgrade) => ) + game.school.upgrades.visibleItems.map((upgrade) => ) }
; });