refactor: 勇士状态
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled

This commit is contained in:
unanmed 2026-04-16 22:31:39 +08:00
parent c22a51829d
commit 92ff489811
15 changed files with 310 additions and 193 deletions

View File

@ -3,8 +3,8 @@ import {
FaceDirection, FaceDirection,
getFaceMovement, getFaceMovement,
HeroAnimateDirection, HeroAnimateDirection,
IHeroState, IHeroMover,
IHeroStateHooks, IHeroMovingHooks,
nextFaceDirection nextFaceDirection
} from '@user/data-base'; } from '@user/data-base';
import { IMapLayer, state } from '@user/data-state'; import { IMapLayer, state } from '@user/data-state';
@ -54,7 +54,7 @@ export class MapHeroRenderer implements IMapHeroRenderer {
new TextureRowSplitter(); new TextureRowSplitter();
/** 勇士钩子 */ /** 勇士钩子 */
readonly controller: IHookController<IHeroStateHooks>; readonly controller: IHookController<IHeroMovingHooks>;
/** 勇士每个朝向的贴图对象 */ /** 勇士每个朝向的贴图对象 */
readonly textureMap: Map<FaceDirection, IMaterialFramedData> = new Map(); readonly textureMap: Map<FaceDirection, IMaterialFramedData> = new Map();
/** 勇士渲染实体,与 `entities[0]` 同引用 */ /** 勇士渲染实体,与 `entities[0]` 同引用 */
@ -72,7 +72,7 @@ export class MapHeroRenderer implements IMapHeroRenderer {
constructor( constructor(
readonly renderer: IMapRenderer, readonly renderer: IMapRenderer,
readonly layer: IMapLayer, readonly layer: IMapLayer,
readonly hero: IHeroState readonly hero: IHeroMover
) { ) {
this.controller = hero.addHook(new MapHeroHook(this)); this.controller = hero.addHook(new MapHeroHook(this));
this.controller.load(); this.controller.load();
@ -106,7 +106,7 @@ export class MapHeroRenderer implements IMapHeroRenderer {
private addHeroMoving( private addHeroMoving(
renderer: IMapRenderer, renderer: IMapRenderer,
layer: IMapLayer, layer: IMapLayer,
hero: IHeroState hero: IHeroMover
) { ) {
if (isNil(hero.image)) { if (isNil(hero.image)) {
logger.warn(88); logger.warn(88);
@ -450,7 +450,7 @@ export class MapHeroRenderer implements IMapHeroRenderer {
} }
} }
class MapHeroHook implements Partial<IHeroStateHooks> { class MapHeroHook implements Partial<IHeroMovingHooks> {
constructor(readonly hero: MapHeroRenderer) {} constructor(readonly hero: MapHeroRenderer) {}
onSetImage(image: ImageIds): void { onSetImage(image: ImageIds): void {

View File

@ -1,4 +1,4 @@
import { IHeroState } from '@user/data-base'; import { IHeroMover } from '@user/data-base';
import { IMapLayer } from '@user/data-state'; import { IMapLayer } from '@user/data-state';
import { import {
IMapDoorRenderer, IMapDoorRenderer,
@ -14,7 +14,7 @@ import { IOnMapTextRenderer } from './types';
export class MapExtensionManager implements IMapExtensionManager { export class MapExtensionManager implements IMapExtensionManager {
/** 勇士状态至勇士渲染器的映射 */ /** 勇士状态至勇士渲染器的映射 */
readonly heroMap: Map<IHeroState, IMapHeroRenderer> = new Map(); readonly heroMap: Map<IHeroMover, IMapHeroRenderer> = new Map();
/** 地图图层到门渲染器的映射 */ /** 地图图层到门渲染器的映射 */
readonly doorMap: Map<IMapLayer, IMapDoorRenderer> = new Map(); readonly doorMap: Map<IMapLayer, IMapDoorRenderer> = new Map();
/** 单例的文字渲染拓展(独立图层) */ /** 单例的文字渲染拓展(独立图层) */
@ -22,7 +22,7 @@ export class MapExtensionManager implements IMapExtensionManager {
constructor(readonly renderer: IMapRenderer) {} constructor(readonly renderer: IMapRenderer) {}
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null { addHero(state: IHeroMover, layer: IMapLayer): IMapHeroRenderer | null {
if (this.heroMap.has(state)) { if (this.heroMap.has(state)) {
logger.error(45, 'hero renderer'); logger.error(45, 'hero renderer');
return null; return null;
@ -32,7 +32,7 @@ export class MapExtensionManager implements IMapExtensionManager {
return heroRenderer; return heroRenderer;
} }
removeHero(state: IHeroState): void { removeHero(state: IHeroMover): void {
const renderer = this.heroMap.get(state); const renderer = this.heroMap.get(state);
if (!renderer) return; if (!renderer) return;
renderer.destroy(); renderer.destroy();

View File

@ -2,7 +2,7 @@ import { ITexture, Font } from '@motajs/render';
import { import {
FaceDirection, FaceDirection,
HeroAnimateDirection, HeroAnimateDirection,
IHeroState IHeroMover
} from '@user/data-base'; } from '@user/data-base';
import { IMapLayer } from '@user/data-state'; import { IMapLayer } from '@user/data-state';
@ -10,7 +10,7 @@ import { IMapRenderResult } from '../types';
export interface IMapExtensionManager { export interface IMapExtensionManager {
/** 勇士状态至勇士渲染器的映射 */ /** 勇士状态至勇士渲染器的映射 */
readonly heroMap: Map<IHeroState, IMapHeroRenderer>; readonly heroMap: Map<IHeroMover, IMapHeroRenderer>;
/** 地图图层到门渲染器的映射 */ /** 地图图层到门渲染器的映射 */
readonly doorMap: Map<IMapLayer, IMapDoorRenderer>; readonly doorMap: Map<IMapLayer, IMapDoorRenderer>;
/** 单例的文字渲染拓展(独立图层) */ /** 单例的文字渲染拓展(独立图层) */
@ -21,13 +21,13 @@ export interface IMapExtensionManager {
* @param state * @param state
* @param layer * @param layer
*/ */
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null; addHero(state: IHeroMover, layer: IMapLayer): IMapHeroRenderer | null;
/** /**
* *
* @param state * @param state
*/ */
removeHero(state: IHeroState): void; removeHero(state: IHeroMover): void;
/** /**
* *

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './utils';

View File

@ -0,0 +1,18 @@
export const enum FaceDirection {
Unknown,
Left,
Up,
Right,
Down,
LeftUp,
RightUp,
LeftDown,
RightDown
}
export interface IFaceData {
/** 图块数字 */
readonly identifier: number;
/** 图块朝向 */
readonly face: FaceDirection;
}

View File

@ -30,7 +30,7 @@ export function getFaceMovement(dir: FaceDirection): Loc {
/** /**
* *
* @param dir * @param dir
* @param unknown `FaceDirection.Unknown` * @param unknown `FaceDirection.Unknown`
*/ */
export function degradeFace( export function degradeFace(
dir: FaceDirection, dir: FaceDirection,

View File

@ -1,4 +1,4 @@
export * from './attribute'; export * from './attribute';
export * from './mover';
export * from './state'; export * from './state';
export * from './types'; export * from './types';
export * from './utils';

View File

@ -0,0 +1,138 @@
import { Hookable, HookController, IHookController } from '@motajs/common';
import { isNil } from 'lodash-es';
import { getFaceMovement, nextFaceDirection } from '../common/utils';
import { IHeroFollower, IHeroMover, IHeroMovingHooks } from './types';
import { FaceDirection } from '../common';
const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png';
export class HeroMover
extends Hookable<IHeroMovingHooks>
implements IHeroMover
{
x: number = 0;
y: number = 0;
direction: FaceDirection = FaceDirection.Down;
image: ImageIds = DEFAULT_HERO_IMAGE;
/** 当前勇士是否正在移动 */
moving: boolean = false;
alpha: number = 1;
readonly followers: IHeroFollower[] = [];
protected createController(
hook: Partial<IHeroMovingHooks>
): IHookController<IHeroMovingHooks> {
return new HookController(this, hook);
}
setPosition(x: number, y: number): void {
this.x = x;
this.y = y;
this.forEachHook(hook => {
hook.onSetPosition?.(x, y);
});
}
turn(direction?: FaceDirection): void {
const next = isNil(direction)
? nextFaceDirection(this.direction)
: direction;
this.direction = next;
this.forEachHook(hook => {
hook.onTurnHero?.(next);
});
}
startMove(): void {
this.moving = true;
this.forEachHook(hook => {
hook.onStartMove?.();
});
}
async move(dir: FaceDirection, time: number = 100): Promise<void> {
await Promise.all(
this.forEachHook(hook => {
return hook.onMoveHero?.(dir, time);
})
);
const { x, y } = getFaceMovement(dir);
this.x += x;
this.y += y;
}
async endMove(): Promise<void> {
if (!this.moving) return;
await Promise.all(
this.forEachHook(hook => {
return hook.onEndMove?.();
})
);
this.moving = false;
}
async jumpHero(
x: number,
y: number,
time: number = 500,
waitFollower: boolean = false
): Promise<void> {
await Promise.all(
this.forEachHook(hook => {
return hook.onJumpHero?.(x, y, time, waitFollower);
})
);
this.x = x;
this.y = y;
}
setImage(image: ImageIds): void {
this.image = image;
this.forEachHook(hook => {
hook.onSetImage?.(image);
});
}
setAlpha(alpha: number): void {
this.alpha = alpha;
this.forEachHook(hook => {
hook.onSetAlpha?.(alpha);
});
}
setFollowerAlpha(identifier: string, alpha: number): void {
const follower = this.followers.find(v => v.identifier === identifier);
if (!follower) return;
follower.alpha = alpha;
this.forEachHook(hook => {
hook.onSetFollowerAlpha?.(identifier, alpha);
});
}
addFollower(follower: number, identifier: string): void {
this.followers.push({ num: follower, identifier, alpha: 1 });
this.forEachHook(hook => {
hook.onAddFollower?.(follower, identifier);
});
}
removeFollower(identifier: string, animate: boolean = false): void {
const index = this.followers.findIndex(
v => v.identifier === identifier
);
if (index === -1) return;
this.followers.splice(index, 1);
this.forEachHook(hook => {
hook.onRemoveFollower?.(identifier, animate);
});
}
removeAllFollowers(): void {
this.followers.length = 0;
this.forEachHook(hook => {
hook.onRemoveAllFollowers?.();
});
}
}

View File

@ -1,139 +1,37 @@
import { Hookable, HookController, IHookController } from '@motajs/common';
import { isNil } from 'lodash-es';
import { getFaceMovement, nextFaceDirection } from './utils';
import { import {
FaceDirection, IHeroAttribute,
IHeroFollower, IHeroMover,
IHeroState, IHeroState,
IHeroStateHooks IReadonlyHeroAttribute
} from './types'; } from './types';
const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png'; export class HeroState<THero> implements IHeroState<THero> {
constructor(
public mover: IHeroMover,
public attribute: IHeroAttribute<THero>
) {}
export class HeroState extends Hookable<IHeroStateHooks> implements IHeroState { attachMover(mover: IHeroMover): void {
x: number = 0; this.mover = mover;
y: number = 0;
direction: FaceDirection = FaceDirection.Down;
image: ImageIds = DEFAULT_HERO_IMAGE;
/** 当前勇士是否正在移动 */
moving: boolean = false;
alpha: number = 1;
readonly followers: IHeroFollower[] = [];
protected createController(
hook: Partial<IHeroStateHooks>
): IHookController<IHeroStateHooks> {
return new HookController(this, hook);
} }
setPosition(x: number, y: number): void { attachAttribute(attribute: IHeroAttribute<THero>): void {
this.x = x; this.attribute = attribute;
this.y = y;
this.forEachHook(hook => {
hook.onSetPosition?.(x, y);
});
} }
turn(direction?: FaceDirection): void { getHeroMover(): IHeroMover {
const next = isNil(direction) return this.mover;
? nextFaceDirection(this.direction)
: direction;
this.direction = next;
this.forEachHook(hook => {
hook.onTurnHero?.(next);
});
} }
startMove(): void { getModifiableAttribute(): IHeroAttribute<THero> {
this.moving = true; return this.attribute;
this.forEachHook(hook => {
hook.onStartMove?.();
});
} }
async move(dir: FaceDirection, time: number = 100): Promise<void> { getAttribute(): IReadonlyHeroAttribute<THero> {
await Promise.all( return this.attribute;
this.forEachHook(hook => {
return hook.onMoveHero?.(dir, time);
})
);
const { x, y } = getFaceMovement(dir);
this.x += x;
this.y += y;
} }
async endMove(): Promise<void> { getIsolatedAttribute(): IHeroAttribute<THero> {
if (!this.moving) return; return this.attribute.clone();
await Promise.all(
this.forEachHook(hook => {
return hook.onEndMove?.();
})
);
this.moving = false;
}
async jumpHero(
x: number,
y: number,
time: number = 500,
waitFollower: boolean = false
): Promise<void> {
await Promise.all(
this.forEachHook(hook => {
return hook.onJumpHero?.(x, y, time, waitFollower);
})
);
this.x = x;
this.y = y;
}
setImage(image: ImageIds): void {
this.image = image;
this.forEachHook(hook => {
hook.onSetImage?.(image);
});
}
setAlpha(alpha: number): void {
this.alpha = alpha;
this.forEachHook(hook => {
hook.onSetAlpha?.(alpha);
});
}
setFollowerAlpha(identifier: string, alpha: number): void {
const follower = this.followers.find(v => v.identifier === identifier);
if (!follower) return;
follower.alpha = alpha;
this.forEachHook(hook => {
hook.onSetFollowerAlpha?.(identifier, alpha);
});
}
addFollower(follower: number, identifier: string): void {
this.followers.push({ num: follower, identifier, alpha: 1 });
this.forEachHook(hook => {
hook.onAddFollower?.(follower, identifier);
});
}
removeFollower(identifier: string, animate: boolean = false): void {
const index = this.followers.findIndex(
v => v.identifier === identifier
);
if (index === -1) return;
this.followers.splice(index, 1);
this.forEachHook(hook => {
hook.onRemoveFollower?.(identifier, animate);
});
}
removeAllFollowers(): void {
this.followers.length = 0;
this.forEachHook(hook => {
hook.onRemoveAllFollowers?.();
});
} }
} }

View File

@ -1,4 +1,5 @@
import { IHookBase, IHookable } from '@motajs/common'; import { IHookBase, IHookable } from '@motajs/common';
import { FaceDirection } from '@user/data-state';
//#region 勇士属性 //#region 勇士属性
@ -41,7 +42,7 @@ export interface IHeroModifier<T = unknown, V = unknown> {
clone(): IHeroModifier<T, V>; clone(): IHeroModifier<T, V>;
} }
export interface IHeroAttribute<THero> { export interface IReadonlyHeroAttribute<THero> {
/** /**
* Buff * Buff
* @param name * @param name
@ -54,6 +55,26 @@ export interface IHeroAttribute<THero> {
*/ */
getFinalAttribute<K extends keyof THero>(name: K): THero[K]; getFinalAttribute<K extends keyof THero>(name: K): THero[K];
/**
*
* @param name
*/
markDirty(name: keyof THero): void;
/**
*
* @param modifier
*/
markModifierDirty(modifier: IHeroModifier): void;
/**
*
* @param cloneModifier
*/
clone(cloneModifier?: boolean): IHeroAttribute<THero>;
}
export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
/** /**
* *
* @param name * @param name
@ -80,52 +101,11 @@ export interface IHeroAttribute<THero> {
name: K, name: K,
modifier: IHeroModifier<THero[K], unknown> modifier: IHeroModifier<THero[K], unknown>
): void; ): void;
/**
*
* @param name
*/
markDirty(name: keyof THero): void;
/**
*
* @param modifier
*/
markModifierDirty(modifier: IHeroModifier): void;
/**
*
* @param cloneModifier
*/
clone(cloneModifier?: boolean): IHeroAttribute<THero>;
} }
//#endregion //#endregion
//#region 朝向 //#region 勇士移动
export const enum FaceDirection {
Unknown,
Left,
Up,
Right,
Down,
LeftUp,
RightUp,
LeftDown,
RightDown
}
export interface IFaceData {
/** 图块数字 */
readonly identifier: number;
/** 图块朝向 */
readonly face: FaceDirection;
}
//#endregion
//#region 勇士状态
export const enum HeroAnimateDirection { export const enum HeroAnimateDirection {
/** 正向播放动画 */ /** 正向播放动画 */
@ -143,7 +123,7 @@ export interface IHeroFollower {
alpha: number; alpha: number;
} }
export interface IHeroStateHooks extends IHookBase { export interface IHeroMovingHooks extends IHookBase {
/** /**
* *
* @param x * @param x
@ -227,7 +207,7 @@ export interface IHeroStateHooks extends IHookBase {
onSetFollowerAlpha(identifier: string, alpha: number): void; onSetFollowerAlpha(identifier: string, alpha: number): void;
} }
export interface IHeroState extends IHookable<IHeroStateHooks> { export interface IHeroMover extends IHookable<IHeroMovingHooks> {
/** 勇士横坐标 */ /** 勇士横坐标 */
readonly x: number; readonly x: number;
/** 勇士纵坐标 */ /** 勇士纵坐标 */
@ -328,3 +308,46 @@ export interface IHeroState extends IHookable<IHeroStateHooks> {
} }
//#endregion //#endregion
//#region 勇士状态
export interface IHeroState<THero> {
/** 勇士移动对象 */
readonly mover: IHeroMover;
/** 勇士属性对象 */
readonly attribute: IReadonlyHeroAttribute<THero>;
/**
*
* @param mover
*/
attachMover(mover: IHeroMover): void;
/**
*
*/
getHeroMover(): IHeroMover;
/**
*
* @param attribute
*/
attachAttribute(attribute: IHeroAttribute<THero>): void;
/**
*
*/
getModifiableAttribute(): IHeroAttribute<THero>;
/**
*
*/
getAttribute(): IReadonlyHeroAttribute<THero>;
/**
*
*/
getIsolatedAttribute(): IHeroAttribute<THero>;
}
//#endregion

View File

@ -1,3 +1,4 @@
export * from './common';
export * from './enemy'; export * from './enemy';
export * from './hero'; export * from './hero';
export * from './load'; export * from './load';

View File

@ -5,11 +5,13 @@ import {
DamageSystem, DamageSystem,
EnemyContext, EnemyContext,
EnemyManager, EnemyManager,
HeroState, HeroMover,
IHeroState,
IEnemyContext, IEnemyContext,
IEnemyManager, IEnemyManager,
MapDamage MapDamage,
HeroAttribute,
HeroState,
IHeroState
} from '@user/data-base'; } from '@user/data-base';
import { IEnemyAttributes } from './enemy/types'; import { IEnemyAttributes } from './enemy/types';
import { import {
@ -21,21 +23,22 @@ import {
MainMapDamageReducer, MainMapDamageReducer,
registerSpecials registerSpecials
} from './enemy'; } from './enemy';
import { TILE_HEIGHT, TILE_WIDTH } from './shared'; import { HERO_DEFAULT_ATTRIBUTE, TILE_HEIGHT, TILE_WIDTH } from './shared';
import { IHeroAttributeObject } from './hero';
export class CoreState implements ICoreState { export class CoreState implements ICoreState {
readonly layer: ILayerState; readonly layer: ILayerState;
readonly hero: IHeroState;
readonly roleFace: IRoleFaceBinder; readonly roleFace: IRoleFaceBinder;
readonly idNumberMap: Map<string, number>; readonly idNumberMap: Map<string, number>;
readonly numberIdMap: Map<number, string>; readonly numberIdMap: Map<number, string>;
readonly hero: IHeroState<IHeroAttributeObject>;
readonly enemyManager: IEnemyManager<IEnemyAttributes>; readonly enemyManager: IEnemyManager<IEnemyAttributes>;
readonly enemyContext: IEnemyContext<IEnemyAttributes>; readonly enemyContext: IEnemyContext<IEnemyAttributes>;
constructor() { constructor() {
this.layer = new LayerState(); this.layer = new LayerState();
this.hero = new HeroState();
this.roleFace = new RoleFaceBinder(); this.roleFace = new RoleFaceBinder();
this.idNumberMap = new Map(); this.idNumberMap = new Map();
this.numberIdMap = new Map(); this.numberIdMap = new Map();
@ -67,18 +70,27 @@ export class CoreState implements ICoreState {
this.enemyContext = enemyContext; this.enemyContext = enemyContext;
//#endregion //#endregion
//#region 勇士初始化
const heroMover = new HeroMover();
const heroAttribute = new HeroAttribute(HERO_DEFAULT_ATTRIBUTE);
const heroState = new HeroState(heroMover, heroAttribute);
this.hero = heroState;
//#endregion
} }
saveState(): IStateSaveData { saveState(): IStateSaveData {
return structuredClone({ return structuredClone({
followers: this.hero.followers followers: this.hero.mover.followers
}); });
} }
loadState(data: IStateSaveData): void { loadState(data: IStateSaveData): void {
this.hero.removeAllFollowers(); this.hero.mover.removeAllFollowers();
data.followers.forEach(v => { data.followers.forEach(v => {
this.hero.addFollower(v.num, v.identifier); this.hero.mover.addFollower(v.num, v.identifier);
}); });
} }
} }

View File

@ -17,6 +17,10 @@ export interface IHeroAttributeObject {
mana: number; mana: number;
/** 勇士魔法上限 */ /** 勇士魔法上限 */
manamax: number; manamax: number;
/** 勇士拥有的金币 */
money: number;
/** 勇士拥有的经验 */
exp: number;
} }
//#endregion //#endregion

View File

@ -1,7 +1,27 @@
import { IHeroAttributeObject } from './hero';
/** 每个地图的默认宽度 */ /** 每个地图的默认宽度 */
export const TILE_WIDTH = 13; export const TILE_WIDTH = 13;
/** 每个地图的默认高度 */ /** 每个地图的默认高度 */
export const TILE_HEIGHT = 13; export const TILE_HEIGHT = 13;
//#region 勇士相关
/** 默认的勇士图片 */ /** 默认的勇士图片 */
export const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png'; export const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png';
/** 勇士的初始属性,数值填多少目前都无所谓,因为最终会从旧样板读取,但是必须得填 */
export const HERO_DEFAULT_ATTRIBUTE: IHeroAttributeObject = {
name: '',
hp: 1,
hpmax: 0,
atk: 0,
def: 0,
mdef: 0,
mana: 0,
manamax: 0,
money: 0,
exp: 0
};
//#endregion

View File

@ -7,6 +7,7 @@ import {
IHeroState IHeroState
} from '@user/data-base'; } from '@user/data-base';
import { IEnemyAttributes } from './enemy/types'; import { IEnemyAttributes } from './enemy/types';
import { IHeroAttributeObject } from './hero';
export interface IGameDataState { export interface IGameDataState {
/** 怪物管理器 */ /** 怪物管理器 */
@ -22,7 +23,7 @@ export interface ICoreState {
/** 地图状态 */ /** 地图状态 */
readonly layer: ILayerState; readonly layer: ILayerState;
/** 勇士状态 */ /** 勇士状态 */
readonly hero: IHeroState; readonly hero: IHeroState<IHeroAttributeObject>;
/** 朝向绑定 */ /** 朝向绑定 */
readonly roleFace: IRoleFaceBinder; readonly roleFace: IRoleFaceBinder;
/** id 到图块数字的映射 */ /** id 到图块数字的映射 */