refactor: hero

This commit is contained in:
unanmed 2026-09-29 16:41:44 +08:00
parent bbdea04393
commit 122567b050
8 changed files with 40 additions and 103 deletions

View File

@ -199,10 +199,9 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
name: K,
index: number
): IHeroModifier<THero[K]> | null {
const arr = this.modifier.get(name);
const arr = this.modifier.get(name) as IHeroModifier<THero[K]>[];
if (!arr) return null;
const modifier = arr[index] as IHeroModifier<THero[K]> | undefined;
// 越界与负索引一律不删除任何修饰器,避免旧 splice 语义下误删末尾元素
const modifier = arr[index];
if (!modifier) return null;
// 删除簿记统一由 deleteModifier 承担,故需先取出修饰器再委托以保留返回值
this.deleteModifier(name, modifier);

View File

@ -23,6 +23,8 @@ import {
} from './types';
import { isNil } from 'lodash-es';
// TODO: 挪到渲染端
export class HeroFollower implements IHeroFollower {
readonly num: number;
readonly state: IDataCommon;

View File

@ -13,13 +13,11 @@ import {
IHeroItemsSave,
IHeroItemState
} from './types';
import { logger } from '@motajs/common';
export class HeroItems<THero> implements IHeroItems<THero> {
/** 永久道具 */
private readonly constants: Map<number, IHeroItemState<THero>> = new Map();
/** 消耗道具 */
private readonly consumables: Map<number, IHeroItemState<THero>> =
new Map();
/** 当前勇士拥有的所有道具 */
private readonly items: Map<number, IHeroItemState<THero>> = new Map();
readonly equipment: HeroEquipsStore<THero>;
@ -27,30 +25,6 @@ export class HeroItems<THero> implements IHeroItems<THero> {
this.equipment = new HeroEquipsStore<THero>(state);
}
/**
* 将 item 参数解析为 num,字符串 id 通过 tileStore 转换为 num
* @param item 道具图块数字或 id
*/
private resolveNum(item: number | string): number | undefined {
if (typeof item === 'number') return item;
return this.state.tileStore.idToNumber(item);
}
/**
* 根据道具分类获取对应的分表
* @param category 道具分类枚举
*/
private getMap(category: ItemCategory): Map<number, IHeroItemState<THero>> {
switch (category) {
case ItemCategory.Constant:
return this.constants;
case ItemCategory.Consumable:
return this.consumables;
default:
return this.constants;
}
}
/**
* 获取指定道具的内部状态,返回可修改引用供内部逻辑使用
* @param item 道具图块数字或 id
@ -58,12 +32,9 @@ export class HeroItems<THero> implements IHeroItems<THero> {
private internalGetItemState(
item: number | string
): IHeroItemState<THero> | null {
const num = this.resolveNum(item);
const num = this.state.tileStore.num(item);
if (isNil(num)) return null;
const category = this.state.itemStore.getCategory(num);
const map = this.getMap(category);
return map.get(num) ?? null;
return this.items.get(num) ?? null;
}
getItemState(
@ -78,8 +49,8 @@ export class HeroItems<THero> implements IHeroItems<THero> {
@shouldReplay('Adding item to hero should be replayed.')
addItem(item: number | string, count: number = 1): void {
const num = this.resolveNum(item);
if (isNil(num)) return;
const [num, id] = this.state.tileStore.identity(item);
if (isNil(num) || isNil(id)) return;
const raw = this.state.itemStore.getData(num);
if (!raw) return;
@ -98,23 +69,18 @@ export class HeroItems<THero> implements IHeroItems<THero> {
return;
}
const map = this.getMap(raw.category);
const existing = map.get(num);
const existing = this.items.get(num);
if (existing) {
existing.count += count;
if (existing.count <= 0) {
map.delete(num);
this.items.delete(num);
}
} else if (count > 0) {
const store = this.state.tileStore;
map.set(num, { id: store.id(raw.num)!, num: raw.num, raw, count });
const consumable = raw.category === ItemCategory.Consumable;
this.items.set(num, { id, num, raw, count, consumable });
}
}
getItem(item: number | string): void {
this.addItem(item, 1);
}
@shouldReplay('Using item should be replayed.')
useItem(item: number | string): boolean {
const state = this.internalGetItemState(item);
@ -136,10 +102,10 @@ export class HeroItems<THero> implements IHeroItems<THero> {
raw.effect.useEffect?.(raw, this.state);
if (raw.category === ItemCategory.Consumable) {
if (state.consumable) {
state.count--;
if (state.count <= 0) {
this.consumables.delete(raw.num);
this.items.delete(raw.num);
}
}
@ -173,19 +139,24 @@ export class HeroItems<THero> implements IHeroItems<THero> {
const raw = this.state.itemStore.getData(save.num);
if (!raw) continue;
const store = this.state.tileStore;
const id = store.id(raw.num);
if (isNil(id)) {
logger.warn(193, raw.num.toString());
continue;
}
map.set(save.num, {
id: store.id(raw.num)!,
id,
num: raw.num,
raw,
count: save.count
count: save.count,
consumable: raw.category === ItemCategory.Consumable
});
}
}
saveState(compression: SaveCompression): IHeroItemsSave<THero> {
return {
constants: this.mapToSave(this.constants),
consumables: this.mapToSave(this.consumables),
items: this.mapToSave(this.items),
equipStore: this.equipment.saveState(compression)
};
}
@ -194,10 +165,8 @@ export class HeroItems<THero> implements IHeroItems<THero> {
state: IHeroItemsSave<THero>,
compression: SaveCompression
): void {
this.constants.clear();
this.consumables.clear();
this.loadMap(this.constants, state.constants);
this.loadMap(this.consumables, state.consumables);
this.items.clear();
this.loadMap(this.items, state.items);
this.equipment.loadState(state.equipStore, compression);
}
}

View File

@ -6,6 +6,8 @@ import {
IHeroRenderingSave
} from './types';
// TODO: 挪到渲染端
export class HeroRendering
extends Hookable<IHeroRenderingHooks>
implements IHeroRendering

View File

@ -1,16 +1,12 @@
import { HeroEquipment } from './equipment';
import { HeroFollowersController } from './follower';
import { HeroItems } from './items';
import { HeroLocation } from './location';
import { HeroRendering } from './rendering';
import {
IHeroAttribute,
IHeroFollowersController,
IHeroEquipment,
IHeroItems,
IHeroLocation,
IHeroModifier,
IHeroRendering,
IHeroState,
IHeroStateSave,
IReadonlyHeroAttribute,
@ -35,8 +31,6 @@ export class HeroState<THero>
implements IHeroState<THero>
{
readonly location: IHeroLocation;
readonly rendering: IHeroRendering;
readonly followers: IHeroFollowersController;
readonly items: IHeroItems<THero>;
readonly equip: IHeroEquipment<THero>;
@ -46,18 +40,12 @@ export class HeroState<THero>
public readonly attribute: IHeroAttribute<THero>
) {
super();
this.rendering = new HeroRendering(state);
const defaultLoc: IFacedTileLocator = {
x: 0,
y: 0,
direction: FaceDirection.Down
};
this.location = new HeroLocation(state, defaultLoc, faceHandler);
this.followers = new HeroFollowersController(
state,
this.location,
faceHandler
);
this.items = new HeroItems(state);
this.equip = new HeroEquipment(this.items.equipment, this.attribute);
}
@ -125,15 +113,9 @@ export class HeroState<THero>
}
saveState(compression: SaveCompression): IHeroStateSave<THero> {
const followerSaves = this.followers
.getAllFollowers()
.map(v => v.saveState(compression));
return {
attribute: this.attribute.saveState(compression),
location: this.location.saveState(compression),
rendering: this.rendering.saveState(compression),
followers: followerSaves,
items: this.items.saveState(compression),
equip: this.equip.saveState(compression)
};
@ -146,10 +128,7 @@ export class HeroState<THero>
// 属性原地读档:不替换实例,使装备与战斗侧持有的引用跨读档始终有效
this.attribute.loadState(state.attribute, compression);
this.location.loadState(state.location, compression);
this.rendering.loadState(state.rendering, compression);
this.items.loadState(state.items, compression);
this.equip.loadState(state.equip, compression);
// 跟随者保留式恢复:同索引同图块数字的实例原地读档,其余按存档替换或删除
this.followers.restoreFollowers(state.followers, compression);
}
}

View File

@ -579,9 +579,7 @@ export interface IHeroItemSave {
export interface IHeroItemsSave<THero> {
/** 永久道具存档 */
readonly constants: readonly IHeroItemSave[];
/** 消耗道具存档 */
readonly consumables: readonly IHeroItemSave[];
readonly items: readonly IHeroItemSave[];
/** 装备实例仓库存档 */
readonly equipStore: IHeroEquipsStoreSave<THero>;
}
@ -593,6 +591,8 @@ export interface IHeroItemState<THero> {
readonly num: number;
/** 道具原始定义数据引用 */
readonly raw: IItemRawData<THero>;
/** 是否为可消耗道具 */
readonly consumable: boolean;
/** 道具持有数量 */
count: number;
}
@ -609,13 +609,6 @@ export interface IHeroItems<THero>
*/
addItem(item: number | string, count?: number): void;
/**
* 勇士获取指定道具,当道具为 `Pick` 类型时会立刻执行其效果,否则使背包中的数量加一。
* 是 `addItem(item, 1)` 的另一种写法。
* @param item 带锯图块数字或 id
*/
getItem(item: number | string): void;
/**
* 获取指定道具的状态
* @param item 道具图块数字或字符串 id
@ -640,12 +633,6 @@ export interface IHeroItems<THero>
//#region 勇士装备
/**
* 装备实例的存档。装备自身的数值与百分比加成是加成的唯一事实源,
* 存档持久化的正是这两个表:无压缩档全量存储、压缩档只存与原始定义的差异;
* 读档也由它们重建装备修饰器。故运行时要改变装备加成,必须改装备自身属性,
* 而不是修改它的修饰器
*/
export interface IEquipmentStateSave<THero> {
/** 装备实例 uid */
readonly uid: number;
@ -914,10 +901,6 @@ export interface IHeroStateSave<THero> {
readonly attribute: IHeroAttributeSave<THero>;
/** 勇士当前位置 */
readonly location: IHeroLocationSave;
/** 勇士渲染状态 */
readonly rendering: IHeroRenderingSave;
/** 勇士当前的跟随者 */
readonly followers: readonly IHeroFollowerSave[];
/** 勇士道具背包状态 */
readonly items: IHeroItemsSave<THero>;
/** 勇士装备状态 */
@ -932,10 +915,6 @@ export interface IHeroState<THero>
readonly location: IHeroLocation;
/** 勇士属性对象 */
readonly attribute: IReadonlyHeroAttribute<THero>;
/** 勇士跟随者对象 */
readonly followers: IHeroFollowersController;
/** 勇士的渲染对象,包含一些必要渲染信息,存在于数据端,并非渲染端 */
readonly rendering: IHeroRendering;
/** 勇士道具背包 */
readonly items: IHeroItems<THero>;
/** 勇士装备系统 */

View File

@ -110,6 +110,12 @@ export interface ITileStore {
* @param token 图块数字或 id
*/
id(token: number | string): string | undefined;
/**
* 传入图块数字或 id,返回图块数字和 id
* @param token 图块数字或 id
*/
identity(token: number | string): [number, string] | [];
}
//#endregion

View File

@ -275,6 +275,7 @@
"190": "Cannot flatten autotile since its width or height cannot be divided evenly. Autotile frame size: [$1,$2].",
"191": "Cannot cache autotile since no its frame or type data stored.",
"192": "A FaceGroup.Dir8 face handler registration is expected.",
"193": "Cannot load item state since no item id registration for item num $1.",
"1001": "Event(setBlock): Unknown tile '$1' when setting block."
}
}