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 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();

View File

@ -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<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<
T extends object = IHeroStatusDefault
@ -139,7 +144,7 @@ export class HeroState<
readonly buffable: Set<keyof T> = new Set();
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) {
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<T>) {
static overrideCalculate(fn: HeroStatusCalculate) {
this.cal = fn;
}
}
@ -353,23 +362,23 @@ interface HeroEvent {
export class Hero<T extends object = IHeroStatusDefault>
extends EventEmitter<HeroEvent>
implements IHeroItem, ISerializable
implements IHeroItem
{
x: number;
y: number;
floorId: FloorIds;
id: string;
readonly items: Map<AllIdsOf<'items'>, number> = new Map();
readonly id: string;
state: HeroState<T>;
items: Map<AllIdsOf<'items'>, number> = new Map();
constructor(
id: string,
x: number,
y: number,
floorId: FloorIds,
state: HeroState<T>,
gameState: GameState
state: HeroState<T>
) {
super();
this.id = id;
@ -378,11 +387,11 @@ export class Hero<T extends object = IHeroStatusDefault>
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<MonoStore<Hero<any>>>('hero')!.list;
// if (list.has(id)) {
// logger.warn(11, `Repeated hero: ${id}.`);
// }
// list.set(id, this);
}
/**
@ -467,8 +476,4 @@ export class Hero<T extends object = IHeroStatusDefault>
hasItem(item: AllIdsOf<'items'>): boolean {
return this.itemCount(item) > 0;
}
toJSON(): string {
return '';
}
}

View File

@ -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<any>) => void;
type CanUseEffectFn = (state: IGameState, hero: Hero<any>) => boolean;
type EffectFn = (state: GameState, hero: Hero<any>) => void;
type CanUseEffectFn = (state: GameState, hero: Hero<any>) => boolean;
interface ItemStateEvent {
use: [hero: Hero<any>];
@ -91,16 +91,17 @@ export class ItemState<
/**
* 使
* @param state
* @param num 使tools和items有效
* @param hero 使
*/
use(hero: Hero<any>): 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<string[]>('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);
}
/**

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 { 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<T> = (data: T) => string;
type FromJSONFunction<T> = (data: string) => T;
export interface IGameState {
hero: MonoStore<Hero<any>>;
route: string[];
}
export class GameState {
state: Map<string, any> = new Map();
export class GameState implements ISerializable {
state: IGameState;
constructor(state: IGameState) {
this.state = state;
}
private static states: Set<string> = new Set();
private static toJSONFn: Map<string, ToJSONFunction<any>> = new Map();
private static fromJSONFn: Map<string, FromJSONFunction<any>> = new Map();
/**
*
*/
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) {}
static loadStateFromJSON(json: string) {}
}
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);
/**
*
* @param key
*/
get<T>(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<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;
}
}