feat: state 及其序列化

This commit is contained in:
unanmed 2024-08-04 16:19:21 +08:00
parent 1df2bc66f4
commit c1c5d29e89
6 changed files with 255 additions and 71 deletions

75
src/common/struct.ts Normal file
View File

@ -0,0 +1,75 @@
import { EventEmitter } from 'eventemitter3';
interface MonoStoreEvent<T> {
change: [before: T | undefined, after: T | undefined];
}
/**
* 使
*
*/
export class MonoStore<T> extends EventEmitter<MonoStoreEvent<T>> {
list: Map<string, T> = new Map();
using?: T;
usingId?: string;
/**
* 使id的数据
* @param id 使id
*/
use(id: string) {
const before = this.using;
this.using = this.list.get(id);
this.usingId = id;
this.emit('change', before, this.using);
}
static toJSON(data: MonoStore<any>) {
return JSON.stringify({
now: data.usingId,
data: [...data.list]
});
}
static fromJSON<T>(data: string): MonoStore<T> {
const d = JSON.parse(data);
const arr: [string, T][] = d.data;
const store = new MonoStore<T>();
arr.forEach(([key, value]) => {
store.list.set(key, value);
});
if (d.now) store.use(d.now);
return store;
}
}
export namespace SerializeUtils {
interface StructSerializer<T> {
toJSON(data: T): string;
fromJSON(data: string): T;
}
/**
* Map键值对序列化函数使Map作为state使
*/
export const mapSerializer: StructSerializer<Map<any, any>> = {
toJSON(data) {
return JSON.stringify([...data]);
},
fromJSON(data) {
return new Map(JSON.parse(data));
}
};
/**
* Set集合序列化函数使Set作为state使
*/
export const setSerializer: StructSerializer<Set<any>> = {
toJSON(data) {
return JSON.stringify([...data]);
},
fromJSON(data) {
return new Set(JSON.parse(data));
}
};
}

View File

@ -9,6 +9,7 @@ import * as battle from './enemy/battle';
import * as hero from './state/hero'; import * as hero from './state/hero';
import * as miscMechanism from './mechanism/misc'; import * as miscMechanism from './mechanism/misc';
import * as study from './mechanism/study'; import * as study from './mechanism/study';
import { registerPresetState } from './state/preset';
// ----- 类注册 // ----- 类注册
Mota.register('class', 'DamageEnemy', damage.DamageEnemy); Mota.register('class', 'DamageEnemy', damage.DamageEnemy);
@ -37,3 +38,5 @@ main.loading = loading;
loading.once('coreInit', () => { loading.once('coreInit', () => {
Mota.Plugin.init(); Mota.Plugin.init();
}); });
registerPresetState();

View File

@ -1,8 +1,9 @@
import { logger } from '@/core/common/logger'; import { logger } from '@/core/common/logger';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { cloneDeep, isNil } from 'lodash-es'; import { cloneDeep, isNil } from 'lodash-es';
import { GameState, ISerializable } from './state'; import { GameState } from './state';
import { ItemState } from './item'; import { ItemState } from './item';
import { MonoStore } from '@/common/struct';
/** /**
* *
@ -128,7 +129,11 @@ interface HeroStateEvent {
set: [key: string | number | symbol, value: any]; set: [key: string | number | symbol, value: any];
} }
type HeroStatusCalculate<T> = <K extends keyof T>(key: K, value: T[K]) => T[K]; type HeroStatusCalculate = (
hero: HeroState<any>,
key: string | number | symbol,
value: any
) => any;
export class HeroState< export class HeroState<
T extends object = IHeroStatusDefault T extends object = IHeroStatusDefault
@ -139,7 +144,7 @@ export class HeroState<
readonly buffable: Set<keyof T> = new Set(); readonly buffable: Set<keyof T> = new Set();
readonly buffMap: Map<keyof T, number> = new Map(); readonly buffMap: Map<keyof T, number> = new Map();
private cal: HeroStatusCalculate<T> = (_, value) => value; private static cal: HeroStatusCalculate = (_0, _1, value) => value;
constructor(init: T) { constructor(init: T) {
super(); super();
@ -220,7 +225,7 @@ export class HeroState<
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') { if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
logger.warn( logger.warn(
13, 13,
`Cannot set buff non-number status. Key: ${String(key)}.` `Cannot set buff of non-number status. Key: ${String(key)}.`
); );
return false; return false;
} }
@ -238,7 +243,7 @@ export class HeroState<
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') { if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
logger.warn( logger.warn(
13, 13,
`Cannot set buff non-number status. Key: ${String(key)}.` `Cannot set buff of non-number status. Key: ${String(key)}.`
); );
return false; return false;
} }
@ -254,11 +259,11 @@ export class HeroState<
if (key === void 0) { if (key === void 0) {
for (const [key, value] of Object.entries(this.status)) { for (const [key, value] of Object.entries(this.status)) {
// @ts-ignore // @ts-ignore
this.computedStatus[key] = this.cal(key, value); this.computedStatus[key] = HeroState.cal(this, key, value);
} }
return true; return true;
} }
this.computedStatus[key] = this.cal(key, this.status[key]); this.computedStatus[key] = HeroState.cal(this, key, this.status[key]);
return true; return true;
} }
@ -268,7 +273,11 @@ export class HeroState<
*/ */
refreshBuffable(): boolean { refreshBuffable(): boolean {
for (const key of this.buffable) { for (const key of this.buffable) {
this.computedStatus[key] = this.cal(key, this.status[key]); this.computedStatus[key] = HeroState.cal(
this,
key,
this.status[key]
);
} }
return true; return true;
} }
@ -277,7 +286,7 @@ export class HeroState<
* *
* @param fn key表示属性名value表示属性值 * @param fn key表示属性名value表示属性值
*/ */
overrideCalculate(fn: HeroStatusCalculate<T>) { static overrideCalculate(fn: HeroStatusCalculate) {
this.cal = fn; this.cal = fn;
} }
} }
@ -353,23 +362,23 @@ interface HeroEvent {
export class Hero<T extends object = IHeroStatusDefault> export class Hero<T extends object = IHeroStatusDefault>
extends EventEmitter<HeroEvent> extends EventEmitter<HeroEvent>
implements IHeroItem, ISerializable implements IHeroItem
{ {
x: number; x: number;
y: number; y: number;
floorId: FloorIds; floorId: FloorIds;
id: string;
readonly items: Map<AllIdsOf<'items'>, number> = new Map();
readonly id: string;
state: HeroState<T>; state: HeroState<T>;
items: Map<AllIdsOf<'items'>, number> = new Map();
constructor( constructor(
id: string, id: string,
x: number, x: number,
y: number, y: number,
floorId: FloorIds, floorId: FloorIds,
state: HeroState<T>, state: HeroState<T>
gameState: GameState
) { ) {
super(); super();
this.id = id; this.id = id;
@ -378,11 +387,11 @@ export class Hero<T extends object = IHeroStatusDefault>
this.floorId = floorId; this.floorId = floorId;
this.state = state; this.state = state;
const list = gameState.state.hero.list; // const list = gameState.get<MonoStore<Hero<any>>>('hero')!.list;
if (list.has(id)) { // if (list.has(id)) {
logger.warn(11, `Repeated hero: ${id}.`); // logger.warn(11, `Repeated hero: ${id}.`);
} // }
list.set(id, this); // list.set(id, this);
} }
/** /**
@ -467,8 +476,4 @@ export class Hero<T extends object = IHeroStatusDefault>
hasItem(item: AllIdsOf<'items'>): boolean { hasItem(item: AllIdsOf<'items'>): boolean {
return this.itemCount(item) > 0; return this.itemCount(item) > 0;
} }
toJSON(): string {
return '';
}
} }

View File

@ -1,10 +1,10 @@
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import type { Hero } from './hero'; import type { Hero } from './hero';
import { GameState, gameStates, IGameState } from './state'; import { GameState, gameStates } from './state';
import { loading } from '../game'; import { loading } from '../game';
type EffectFn = (state: IGameState, hero: Hero<any>) => void; type EffectFn = (state: GameState, hero: Hero<any>) => void;
type CanUseEffectFn = (state: IGameState, hero: Hero<any>) => boolean; type CanUseEffectFn = (state: GameState, hero: Hero<any>) => boolean;
interface ItemStateEvent { interface ItemStateEvent {
use: [hero: Hero<any>]; use: [hero: Hero<any>];
@ -91,16 +91,17 @@ export class ItemState<
/** /**
* 使 * 使
* @param state * @param hero 使
* @param num 使tools和items有效
*/ */
use(hero: Hero<any>): boolean { use(hero: Hero<any>): boolean {
if (!this.canUse(hero)) return false; if (!this.canUse(hero)) return false;
if (!gameStates.now) return false; if (!gameStates.now) return false;
const state = gameStates.now.state; const state = gameStates.now;
this.useItemEffectFn?.(state, hero); this.useItemEffectFn?.(state, hero);
if (this.useItemEvent) core.insertAction(this.useItemEvent); if (this.useItemEvent) core.insertAction(this.useItemEvent);
if (!this.noRoute) state.route.push(`item:${this.id}`); if (!this.noRoute) {
state.get<string[]>('route')!.push(`item:${this.id}`);
}
hero.addItem(this.id, -1); hero.addItem(this.id, -1);
this.emit('use', hero); this.emit('use', hero);
@ -116,7 +117,7 @@ export class ItemState<
if (num <= 0) return false; if (num <= 0) return false;
if (hero.itemCount(this.id) < num) return false; if (hero.itemCount(this.id) < num) return false;
if (!gameStates.now) return false; if (!gameStates.now) return false;
return !!this.canUseItemEffectFn?.(gameStates.now.state, hero); return !!this.canUseItemEffectFn?.(gameStates.now, hero);
} }
/** /**

64
src/game/state/preset.ts Normal file
View File

@ -0,0 +1,64 @@
import { MonoStore } from '@/common/struct';
import { GameState } from './state';
import { Hero, HeroState } from './hero';
export function registerPresetState() {
GameState.register<MonoStore<Hero<any>>>('hero', heroToJSON, heroFromJSON);
}
interface HeroSave {
x: number;
y: number;
floorId: FloorIds;
id: string;
items: [AllIdsOf<'items'>, number][];
state: {
status: any;
buffable: (string | number | symbol)[];
buffMap: [string | number | symbol, number][];
};
}
interface HeroSerializable {
now: string | null;
saves: HeroSave[];
}
function heroToJSON(data: MonoStore<Hero<any>>): string {
const now = data.usingId ?? null;
const saves: HeroSave[] = [...data.list.values()].map(v => {
return {
x: v.x,
y: v.y,
floorId: v.floorId,
id: v.id,
items: [...v.items],
state: {
status: v.state.status,
buffable: [...v.state.buffable],
buffMap: [...v.state.buffMap]
}
};
});
const obj: HeroSerializable = {
now,
saves
};
return JSON.stringify(obj);
}
function heroFromJSON(data: string): MonoStore<Hero<any>> {
const obj: HeroSerializable = JSON.parse(data);
const store = new MonoStore<Hero<any>>();
const saves: [string, Hero<any>][] = obj.saves.map(v => {
const state = new HeroState(v.state.status);
v.state.buffable.forEach(v => state.buffable.add(v));
v.state.buffMap.forEach(v => state.buffMap.set(v[0], v[1]));
const hero = new Hero(v.id, v.x, v.y, v.floorId, state);
v.items.forEach(v => hero.items.set(v[0], v[1]));
return [hero.id, hero];
});
store.list = new Map(saves);
if (obj.now) store.use(obj.now);
return store;
}

View File

@ -1,53 +1,89 @@
import { Undoable } from '@/core/interface'; import { Undoable } from '@/core/interface';
import { Hero, HeroState } from './hero'; import { EventEmitter } from 'eventemitter3';
import EventEmitter from 'eventemitter3'; import { logger } from '@/core/common/logger';
export interface ISerializable { type ToJSONFunction<T> = (data: T) => string;
toJSON(): string; type FromJSONFunction<T> = (data: string) => T;
}
export interface IGameState { export class GameState {
hero: MonoStore<Hero<any>>; state: Map<string, any> = new Map();
route: string[];
}
export class GameState implements ISerializable { private static states: Set<string> = new Set();
state: IGameState; private static toJSONFn: Map<string, ToJSONFunction<any>> = new Map();
private static fromJSONFn: Map<string, FromJSONFunction<any>> = new Map();
constructor(state: IGameState) {
this.state = state;
}
/**
*
*/
toJSON() { toJSON() {
return ''; const obj: Record<string, string> = {};
this.state.forEach((v, k) => {
const to = GameState.toJSONFn.get(k);
if (to) obj[k] = to(v);
else obj[k] = JSON.stringify(v);
});
return JSON.stringify(obj);
} }
static loadState(state: GameState) {} /**
*
static fromJSON(json: string) {} * @param key
*/
static loadStateFromJSON(json: string) {} get<T>(key: string): T | undefined {
} return this.state.get(key);
interface MonoStoreEvent<T> {
change: [before: T | undefined, after: T | undefined];
}
export class MonoStore<T extends ISerializable>
extends EventEmitter<MonoStoreEvent<T>>
implements ISerializable
{
list: Map<string, T> = new Map();
using?: T;
use(id: string) {
const before = this.using;
this.using = this.list.get(id);
this.emit('change', before, this.using);
} }
toJSON() { /**
return ''; *
* @param key
* @param data
*/
set(key: string, data: any) {
this.state.set(key, data);
}
/**
*
* @param key
* @param toJSON
* 使JSON.stringify进行序列化
* @param fromJSON
* 使JSON.parse进行反序列化
*/
static register<T>(
key: string,
toJSON?: ToJSONFunction<T>,
fromJSON?: FromJSONFunction<T>
) {
if (this.states.has(key)) {
logger.warn(16, `Override repeated state key: ${key}.`);
}
if (toJSON) {
this.toJSONFn.set(key, toJSON);
} else {
this.toJSONFn.delete(key);
}
if (fromJSON) {
this.fromJSONFn.set(key, fromJSON);
} else {
this.fromJSONFn.delete(key);
}
}
/**
*
* @param json
*/
static fromJSON(json: string) {
const obj: Record<string, string> = JSON.parse(json);
const state = new GameState();
for (const [key, data] of Object.entries(obj)) {
const from = this.fromJSONFn.get(key);
if (from) state.set(key, from(data));
else state.set(key, JSON.parse(data));
}
return state;
} }
} }