From c1c5d29e898ca3028f59d5e86dc6c6d2dbf44cc3 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 4 Aug 2024 16:19:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20state=20=E5=8F=8A=E5=85=B6=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/struct.ts | 75 +++++++++++++++++++++++++ src/game/index.ts | 3 + src/game/state/hero.ts | 51 +++++++++-------- src/game/state/item.ts | 17 +++--- src/game/state/preset.ts | 64 +++++++++++++++++++++ src/game/state/state.ts | 116 +++++++++++++++++++++++++-------------- 6 files changed, 255 insertions(+), 71 deletions(-) create mode 100644 src/common/struct.ts create mode 100644 src/game/state/preset.ts diff --git a/src/common/struct.ts b/src/common/struct.ts new file mode 100644 index 0000000..c58c20f --- /dev/null +++ b/src/common/struct.ts @@ -0,0 +1,75 @@ +import { EventEmitter } from 'eventemitter3'; + +interface MonoStoreEvent { + change: [before: T | undefined, after: T | undefined]; +} + +/** + * 一个可序列化存储结构,存储多个数据,但是同时只能有一个数据正在使用,适合那些需要切换的场景, + * 例如多勇士,多摄像机等,就可以通过此结构进行存储,然后同时只能操作一个勇士、摄像机等。 + */ +export class MonoStore extends EventEmitter> { + list: Map = 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) { + return JSON.stringify({ + now: data.usingId, + data: [...data.list] + }); + } + + static fromJSON(data: string): MonoStore { + const d = JSON.parse(data); + const arr: [string, T][] = d.data; + const store = new MonoStore(); + arr.forEach(([key, value]) => { + store.list.set(key, value); + }); + if (d.now) store.use(d.now); + return store; + } +} + +export namespace SerializeUtils { + interface StructSerializer { + toJSON(data: T): string; + fromJSON(data: string): T; + } + + /** + * Map键值对序列化函数,如果使用Map作为state,那么需要使用这里面的函数作为序列化与反序列化函数 + */ + export const mapSerializer: StructSerializer> = { + toJSON(data) { + return JSON.stringify([...data]); + }, + fromJSON(data) { + return new Map(JSON.parse(data)); + } + }; + + /** + * Set集合序列化函数,如果使用Set作为state,那么需要使用这里面的函数作为序列化与反序列化函数 + */ + export const setSerializer: StructSerializer> = { + toJSON(data) { + return JSON.stringify([...data]); + }, + fromJSON(data) { + return new Set(JSON.parse(data)); + } + }; +} diff --git a/src/game/index.ts b/src/game/index.ts index 001818d..1326535 100644 --- a/src/game/index.ts +++ b/src/game/index.ts @@ -9,6 +9,7 @@ import * as battle from './enemy/battle'; import * as hero from './state/hero'; import * as miscMechanism from './mechanism/misc'; import * as study from './mechanism/study'; +import { registerPresetState } from './state/preset'; // ----- 类注册 Mota.register('class', 'DamageEnemy', damage.DamageEnemy); @@ -37,3 +38,5 @@ main.loading = loading; loading.once('coreInit', () => { Mota.Plugin.init(); }); + +registerPresetState(); diff --git a/src/game/state/hero.ts b/src/game/state/hero.ts index a0fa5db..4daf8f4 100644 --- a/src/game/state/hero.ts +++ b/src/game/state/hero.ts @@ -1,8 +1,9 @@ import { logger } from '@/core/common/logger'; import { EventEmitter } from 'eventemitter3'; import { cloneDeep, isNil } from 'lodash-es'; -import { GameState, ISerializable } from './state'; +import { GameState } from './state'; import { ItemState } from './item'; +import { MonoStore } from '@/common/struct'; /** * 获取勇士在某一点的属性 @@ -128,7 +129,11 @@ interface HeroStateEvent { set: [key: string | number | symbol, value: any]; } -type HeroStatusCalculate = (key: K, value: T[K]) => T[K]; +type HeroStatusCalculate = ( + hero: HeroState, + key: string | number | symbol, + value: any +) => any; export class HeroState< T extends object = IHeroStatusDefault @@ -139,7 +144,7 @@ export class HeroState< readonly buffable: Set = new Set(); readonly buffMap: Map = new Map(); - private cal: HeroStatusCalculate = (_, value) => value; + private static cal: HeroStatusCalculate = (_0, _1, value) => value; constructor(init: T) { super(); @@ -220,7 +225,7 @@ export class HeroState< if (!this.buffable.has(key) || typeof this.status[key] !== 'number') { logger.warn( 13, - `Cannot set buff non-number status. Key: ${String(key)}.` + `Cannot set buff of non-number status. Key: ${String(key)}.` ); return false; } @@ -238,7 +243,7 @@ export class HeroState< if (!this.buffable.has(key) || typeof this.status[key] !== 'number') { logger.warn( 13, - `Cannot set buff non-number status. Key: ${String(key)}.` + `Cannot set buff of non-number status. Key: ${String(key)}.` ); return false; } @@ -254,11 +259,11 @@ export class HeroState< if (key === void 0) { for (const [key, value] of Object.entries(this.status)) { // @ts-ignore - this.computedStatus[key] = this.cal(key, value); + this.computedStatus[key] = HeroState.cal(this, key, value); } return true; } - this.computedStatus[key] = this.cal(key, this.status[key]); + this.computedStatus[key] = HeroState.cal(this, key, this.status[key]); return true; } @@ -268,7 +273,11 @@ export class HeroState< */ refreshBuffable(): boolean { 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; } @@ -277,7 +286,7 @@ export class HeroState< * 复写属性计算函数,默认函数不进行计算,直接将原属性返回 * @param fn 计算函数,传入两个参数,key表示属性名,value表示属性值,返回值表示计算结果 */ - overrideCalculate(fn: HeroStatusCalculate) { + static overrideCalculate(fn: HeroStatusCalculate) { this.cal = fn; } } @@ -353,23 +362,23 @@ interface HeroEvent { export class Hero extends EventEmitter - implements IHeroItem, ISerializable + implements IHeroItem { x: number; y: number; floorId: FloorIds; - id: string; + + readonly items: Map, number> = new Map(); + readonly id: string; state: HeroState; - items: Map, number> = new Map(); constructor( id: string, x: number, y: number, floorId: FloorIds, - state: HeroState, - gameState: GameState + state: HeroState ) { super(); this.id = id; @@ -378,11 +387,11 @@ export class Hero this.floorId = floorId; this.state = state; - const list = gameState.state.hero.list; - if (list.has(id)) { - logger.warn(11, `Repeated hero: ${id}.`); - } - list.set(id, this); + // const list = gameState.get>>('hero')!.list; + // if (list.has(id)) { + // logger.warn(11, `Repeated hero: ${id}.`); + // } + // list.set(id, this); } /** @@ -467,8 +476,4 @@ export class Hero hasItem(item: AllIdsOf<'items'>): boolean { return this.itemCount(item) > 0; } - - toJSON(): string { - return ''; - } } diff --git a/src/game/state/item.ts b/src/game/state/item.ts index 33969a1..c826534 100644 --- a/src/game/state/item.ts +++ b/src/game/state/item.ts @@ -1,10 +1,10 @@ import EventEmitter from 'eventemitter3'; import type { Hero } from './hero'; -import { GameState, gameStates, IGameState } from './state'; +import { GameState, gameStates } from './state'; import { loading } from '../game'; -type EffectFn = (state: IGameState, hero: Hero) => void; -type CanUseEffectFn = (state: IGameState, hero: Hero) => boolean; +type EffectFn = (state: GameState, hero: Hero) => void; +type CanUseEffectFn = (state: GameState, hero: Hero) => boolean; interface ItemStateEvent { use: [hero: Hero]; @@ -91,16 +91,17 @@ export class ItemState< /** * 使用这个物品 - * @param state 游戏状态 - * @param num 使用的数量,仅对tools和items有效 + * @param hero 使用物品的勇士 */ use(hero: Hero): boolean { if (!this.canUse(hero)) return false; if (!gameStates.now) return false; - const state = gameStates.now.state; + const state = gameStates.now; this.useItemEffectFn?.(state, hero); if (this.useItemEvent) core.insertAction(this.useItemEvent); - if (!this.noRoute) state.route.push(`item:${this.id}`); + if (!this.noRoute) { + state.get('route')!.push(`item:${this.id}`); + } hero.addItem(this.id, -1); this.emit('use', hero); @@ -116,7 +117,7 @@ export class ItemState< if (num <= 0) return false; if (hero.itemCount(this.id) < num) return false; if (!gameStates.now) return false; - return !!this.canUseItemEffectFn?.(gameStates.now.state, hero); + return !!this.canUseItemEffectFn?.(gameStates.now, hero); } /** diff --git a/src/game/state/preset.ts b/src/game/state/preset.ts new file mode 100644 index 0000000..92a90b5 --- /dev/null +++ b/src/game/state/preset.ts @@ -0,0 +1,64 @@ +import { MonoStore } from '@/common/struct'; +import { GameState } from './state'; +import { Hero, HeroState } from './hero'; + +export function registerPresetState() { + GameState.register>>('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>): 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> { + const obj: HeroSerializable = JSON.parse(data); + const store = new MonoStore>(); + const saves: [string, Hero][] = 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; +} diff --git a/src/game/state/state.ts b/src/game/state/state.ts index bdb041e..2598579 100644 --- a/src/game/state/state.ts +++ b/src/game/state/state.ts @@ -1,53 +1,89 @@ 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 { - toJSON(): string; -} +type ToJSONFunction = (data: T) => string; +type FromJSONFunction = (data: string) => T; -export interface IGameState { - hero: MonoStore>; - route: string[]; -} +export class GameState { + state: Map = new Map(); -export class GameState implements ISerializable { - state: IGameState; - - constructor(state: IGameState) { - this.state = state; - } + private static states: Set = new Set(); + private static toJSONFn: Map> = new Map(); + private static fromJSONFn: Map> = new Map(); + /** + * 序列化游戏状态,可直接用于存储等操作 + */ toJSON() { - return ''; + const obj: Record = {}; + 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) {} - - static loadStateFromJSON(json: string) {} -} - -interface MonoStoreEvent { - change: [before: T | undefined, after: T | undefined]; -} - -export class MonoStore - extends EventEmitter> - implements ISerializable -{ - list: Map = new Map(); - using?: T; - - use(id: string) { - const before = this.using; - this.using = this.list.get(id); - this.emit('change', before, this.using); + /** + * 获取某个游戏状态 + * @param key 要获取的状态名称 + */ + get(key: string): T | undefined { + return this.state.get(key); } - 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( + key: string, + toJSON?: ToJSONFunction, + fromJSON?: FromJSONFunction + ) { + 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 = 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; } }