M3: модуль сохранения
По ARCHITECTURE.md: реестр независимых секций, JSON с ключом на секцию. Ключи — явные строковые константы, а не имена классов: релизная сборка минифицирует имена и молча потеряла бы все сохранения. - ISaveSection, SaveManager, IStorage + MemoryStorage - CocosStorage: единственный файл модуля, знающий про движок; при отказе хранилища уходит в память, а не роняет игру - StatsSection: сыграно/побед/поражений/ничьих, текущая и лучшая серия - терпимая загрузка: битый JSON, чужая версия, сломанная секция или отвалившееся хранилище не ломают остальное - проверка чистоты расширена на modules/ со списком адаптеров Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { ISaveSection } from './ISaveSection';
|
||||
|
||||
export type MatchOutcome = 'win' | 'loss' | 'draw';
|
||||
|
||||
interface StatsData {
|
||||
played: number;
|
||||
won: number;
|
||||
lost: number;
|
||||
drawn: number;
|
||||
streak: number;
|
||||
bestStreak: number;
|
||||
}
|
||||
|
||||
function emptyStats(): StatsData {
|
||||
return { played: 0, won: 0, lost: 0, drawn: 0, streak: 0, bestStreak: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* How the player has been doing. The only section iteration 1 ships; currency
|
||||
* and level progress will join it later without touching `SaveManager`.
|
||||
*/
|
||||
export class StatsSection implements ISaveSection {
|
||||
static readonly KEY = 'stats';
|
||||
|
||||
readonly key = StatsSection.KEY;
|
||||
|
||||
private _data: StatsData = emptyStats();
|
||||
|
||||
get played(): number { return this._data.played; }
|
||||
get won(): number { return this._data.won; }
|
||||
get lost(): number { return this._data.lost; }
|
||||
get drawn(): number { return this._data.drawn; }
|
||||
/** Wins in a row right now. A loss resets it; a draw leaves it alone. */
|
||||
get streak(): number { return this._data.streak; }
|
||||
get bestStreak(): number { return this._data.bestStreak; }
|
||||
|
||||
recordResult(outcome: MatchOutcome): void {
|
||||
this._data.played += 1;
|
||||
switch (outcome) {
|
||||
case 'win':
|
||||
this._data.won += 1;
|
||||
this._data.streak += 1;
|
||||
this._data.bestStreak = Math.max(this._data.bestStreak, this._data.streak);
|
||||
break;
|
||||
case 'loss':
|
||||
this._data.lost += 1;
|
||||
this._data.streak = 0;
|
||||
break;
|
||||
case 'draw':
|
||||
this._data.drawn += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
save(): unknown {
|
||||
return { ...this._data };
|
||||
}
|
||||
|
||||
load(data: unknown): void {
|
||||
const source = (typeof data === 'object' && data !== null ? data : {}) as Record<string, unknown>;
|
||||
const defaults = emptyStats();
|
||||
this._data = {
|
||||
played: count(source.played, defaults.played),
|
||||
won: count(source.won, defaults.won),
|
||||
lost: count(source.lost, defaults.lost),
|
||||
drawn: count(source.drawn, defaults.drawn),
|
||||
streak: count(source.streak, defaults.streak),
|
||||
bestStreak: count(source.bestStreak, defaults.bestStreak),
|
||||
};
|
||||
// A save edited by hand should not be able to show a nonsense best.
|
||||
this._data.bestStreak = Math.max(this._data.bestStreak, this._data.streak);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._data = emptyStats();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a non-negative whole number, or falls back. */
|
||||
function count(value: unknown, fallback: number): number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
||||
? Math.floor(value)
|
||||
: fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user