refactor: 删除 data-state 中的 legacy 文件夹

This commit is contained in:
unanmed 2026-09-17 19:37:30 +08:00
parent 5f97352f94
commit e9e9939bd8
21 changed files with 23 additions and 1374 deletions

View File

@ -1,5 +1,6 @@
import { KeyCode } from '@motajs/client-base';
import { Hotkey, HotkeyData } from '@motajs/system';
// @ts-expect-error 需要重构
import { HeroMover, IMoveController } from '@user/data-state';
import { Ticker } from 'mutate-animate';
import { mainScope } from './hotkey';

View File

@ -25,6 +25,7 @@ import {
RightStatusBar
} from './statusBar';
import { ReplayingStatus } from './toolbar';
// @ts-expect-error 需要重构
import { getHeroStatusOn, state } from '@user/data-state';
import { hook } from '@user/data-base';
import { mainUIController } from './controller';

View File

@ -145,7 +145,7 @@ function createItem(
/** 构造一个装配勇士装备对象的测试环境 */
function createEnv(): TestEnv {
const tileStore = new TileStore();
const itemStore = new ItemStore<IHeroAttr, unknown>();
const itemStore = new ItemStore<IHeroAttr>();
// 录像系统桩,用于满足装备/卸下时的 route.add 记录与临时禁用录像
const replaySystem = createReplaySystem();
const state = { tileStore, itemStore, replaySystem } as never;

View File

@ -57,7 +57,7 @@ interface ItemFixture {
/** 构造一个装配勇士道具对象的测试环境 */
function createEnv(): TestEnv {
const tileStore = new TileStore();
const itemStore = new ItemStore<IHeroAttr, unknown>();
const itemStore = new ItemStore<IHeroAttr>();
// 录像系统桩,仅用于满足道具使用时的 route.add 记录
const replaySystem = { route: { add: vi.fn() } };
const state = { tileStore, itemStore, replaySystem } as never;

View File

@ -74,7 +74,7 @@ interface EquipEnv {
/** 极E<E69E81><45>一个含图块、E<E38081><45>具存储与假录像系统的公共层假对象 */
function createState(): IDataCommon {
const tileStore = new TileStore();
const itemStore = new ItemStore<IHeroAttr, unknown>();
const itemStore = new ItemStore<IHeroAttr>();
const route = { add: vi.fn() };
return {
tileStore,

View File

@ -1,18 +1,9 @@
import { logger } from '@motajs/common';
import {
IItemLegacyConverter,
IItemRawData,
IItemStore,
ItemCategory
} from './types';
import { IItemRawData, IItemStore, ItemCategory } from './types';
export class ItemStore<THero, TLegacy> implements IItemStore<THero, TLegacy> {
export class ItemStore<THero> implements IItemStore<THero> {
/** 以道具图块数字为键的原始道具定义表 */
private readonly dataMap: Map<number, IItemRawData<THero>> = new Map();
/** 当前挂载的旧样板道具转换器 */
private converter: IItemLegacyConverter<THero, TLegacy> | null = null;
getData(num: number): IItemRawData<THero> | null {
return this.dataMap.get(num) ?? null;
}
@ -24,21 +15,4 @@ export class ItemStore<THero, TLegacy> implements IItemStore<THero, TLegacy> {
addItem(data: IItemRawData<THero>): void {
this.dataMap.set(data.num, data);
}
attachLegacyConverter(
converter: IItemLegacyConverter<THero, TLegacy>
): void {
this.converter = converter;
}
fromLegacy(num: number, legacy: TLegacy): IItemRawData<THero> {
const converter = this.converter;
if (!converter) {
logger.error(57);
throw new Error('Expected a item legacy converter.');
}
const data = converter.fromLegacy(num, legacy);
this.addItem(data);
return data;
}
}

View File

@ -1,13 +1,8 @@
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import {
ITileLegacyConverter,
ITileRawData,
ITileStore,
TileType
} from './types';
import { ITileRawData, ITileStore, TileType } from './types';
export class TileStore<TLegacy = unknown> implements ITileStore<TLegacy> {
export class TileStore implements ITileStore {
/** 以图块数字为键的原始图块定义表 */
private readonly dataMap: Map<number, ITileRawData> = new Map();
@ -20,9 +15,6 @@ export class TileStore<TLegacy = unknown> implements ITileStore<TLegacy> {
/** 由图块数字保存的默认事件映射 */
private readonly eventMap: Map<number, Map<number, string>> = new Map();
/** 当前挂载的旧样板图块转换器 */
private legacyConverter: ITileLegacyConverter<TLegacy> | null = null;
getData(num: number): ITileRawData | null {
return this.dataMap.get(num) ?? null;
}
@ -83,21 +75,6 @@ export class TileStore<TLegacy = unknown> implements ITileStore<TLegacy> {
}
}
attachLegacyConverter(converter: ITileLegacyConverter<TLegacy>): void {
this.legacyConverter = converter;
}
fromLegacy(num: number, legacy: TLegacy): ITileRawData {
const converter = this.legacyConverter;
if (!converter) {
logger.error(56);
throw new Error('Expected a tile legacy converter.');
}
const data = converter.fromLegacy(num, legacy);
this.addTile(data);
return data;
}
/** 删除一组旧的图块定义及其双向索引 */
private deleteBy(num: number, id: string): void {
this.dataMap.delete(num);

View File

@ -62,16 +62,7 @@ export interface ITileRawData {
readonly eventPass: boolean;
}
export interface ITileLegacyConverter<TLegacy> {
/**
* 将旧样板图块定义转换为新的图块原始数据
* @param num 图块数字
* @param legacy 旧样板图块定义
*/
fromLegacy(num: number, legacy: TLegacy): ITileRawData;
}
export interface ITileStore<TLegacy = unknown> {
export interface ITileStore {
/**
* 获取指定图块数字对应的完整原始定义
* @param num 图块数字
@ -119,19 +110,6 @@ export interface ITileStore<TLegacy = unknown> {
* @param token 图块数字或 id
*/
id(token: number | string): string | undefined;
/**
* 挂载一个旧样板转换器
* @param converter 旧样板转换器
*/
attachLegacyConverter(converter: ITileLegacyConverter<TLegacy>): void;
/**
* 使用当前转换器转换并写入一个旧样板图块定义
* @param num 图块数字
* @param legacy 旧样板图块定义
*/
fromLegacy(num: number, legacy: TLegacy): ITileRawData;
}
//#endregion
@ -208,16 +186,7 @@ export interface IItemRawData<THero> {
readonly equip: IItemEquipData<THero>;
}
export interface IItemLegacyConverter<THero, TLegacy> {
/**
* 将旧样板道具定义转换为新的道具原始数据
* @param num 道具图块数字
* @param legacy 旧样板道具定义
*/
fromLegacy(num: number, legacy: TLegacy): IItemRawData<THero>;
}
export interface IItemStore<THero, TLegacy> {
export interface IItemStore<THero> {
/**
* 获取指定图块数字对应的道具原始数据
* @param num 道具图块数字
@ -235,21 +204,6 @@ export interface IItemStore<THero, TLegacy> {
* @param data 道具原始数据
*/
addItem(data: IItemRawData<THero>): void;
/**
* 挂载一个旧样板转换器
* @param converter 旧样板转换器
*/
attachLegacyConverter(
converter: IItemLegacyConverter<THero, TLegacy>
): void;
/**
* 使用当前转换器转换并写入一个旧样板道具定义
* @param num 道具图块数字
* @param legacy 旧样板道具定义
*/
fromLegacy(num: number, legacy: TLegacy): IItemRawData<THero>;
}
//#endregion

View File

@ -45,9 +45,9 @@ export interface IHeroAttr {
export interface IDataCommon {
/** 图块定义存储 */
readonly tileStore: ITileStore<MapDataOf<keyof NumberToId>>;
readonly tileStore: ITileStore;
/** 道具定义存储 */
readonly itemStore: IItemStore<IHeroAttr, Item<AllIdsOf<'items'>>>;
readonly itemStore: IItemStore<IHeroAttr>;
/** 地图定义存储 */
readonly mapStore: IMapStore;
/** 游戏事件存储 */

View File

@ -35,8 +35,6 @@ import {
FlagSystem,
IMotaDataLoader,
MotaDataLoader,
loading,
IReadonlyEnemy,
IMapState,
MapState
} from '@user/data-base';
@ -62,23 +60,7 @@ import {
registerSpecials,
MainEnemyComparer
} from './enemy';
import {
BG2_ZINDEX,
BG_ZINDEX,
EVENT_ZINDEX,
FG2_ZINDEX,
FG_ZINDEX,
HERO_DEFAULT_ATTRIBUTE,
TILE_HEIGHT,
TILE_WIDTH
} from './shared';
import {
ItemLegacyBridge,
LegacyItemData,
LegacyTileData,
TileLegacyBridge
} from './legacy';
import { isNil } from 'lodash-es';
import { HERO_DEFAULT_ATTRIBUTE, TILE_HEIGHT, TILE_WIDTH } from './shared';
import { DefaultHeroMoveTopImpl, DefaultPassPredicateImpl } from './hero';
import { createEventRegistrations } from './event/registrations';
import {
@ -93,8 +75,8 @@ export class CoreState implements ICoreState {
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
readonly roleFace: IRoleFaceBinder;
readonly faceManager: IFaceManager;
readonly tileStore: ITileStore<LegacyTileData>;
readonly itemStore: IItemStore<IHeroAttr, LegacyItemData>;
readonly tileStore: ITileStore;
readonly itemStore: IItemStore<IHeroAttr>;
readonly mapStore: IMapStore;
readonly eventStore: IGameEventStore;
readonly directionMapper: IDirectionMapper;
@ -139,19 +121,13 @@ export class CoreState implements ICoreState {
this.faceManager.registerById('dir8', dir8);
// 图块
const tileStore = new TileStore<LegacyTileData>();
tileStore.attachLegacyConverter(new TileLegacyBridge());
this.tileStore = tileStore;
this.tileStore = new TileStore();
// 道具
const itemStore = new ItemStore<IHeroAttr, LegacyItemData>();
itemStore.attachLegacyConverter(new ItemLegacyBridge(this));
this.itemStore = itemStore;
this.itemStore = new ItemStore<IHeroAttr>();
// 地图
const mapStore = new MapStore();
this.mapStore = mapStore;
this.mapStore = new MapStore();
// 游戏事件
const eventStore = new GameEventStore();
this.eventStore = eventStore;
this.eventStore = new GameEventStore();
this.directionMapper = new DirectionMapper();
//#endregion
@ -162,7 +138,7 @@ export class CoreState implements ICoreState {
this.flags = new FlagSystem();
// 地图
this.maps = new MapState(tileStore, this);
this.maps = new MapState(this.tileStore, this);
// 勇士
const heroAttribute = new HeroAttribute(HERO_DEFAULT_ATTRIBUTE);
@ -237,17 +213,6 @@ export class CoreState implements ICoreState {
this.addSaveableContent('@system/enemy', this.enemyManager);
this.addSaveableContent('@system/replay', this.replaySystem);
// 加载初始化,先使用兼容层实现
loading.once('loaded', () => {
this.initTileStore(core.maps.blocksInfo);
this.initItemStore(core.items.items);
this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80);
this.initMapState(
core.floorIds,
core.floors as Record<FloorIds, ResolvedFloor>
);
});
// 勇士顶层初始化
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this);
this.hero.location.mover.useTopImplementation(heroMoveTopImpl);
@ -285,168 +250,6 @@ export class CoreState implements ICoreState {
replay.registerCommand(ReplayCode.Unequip, unequip);
}
/**
* 初始化图块存储对象
* @param data 旧样板图块定义对象
*/
private initTileStore(data: typeof core.maps.blocksInfo) {
const entries = Object.entries(data);
for (const [key, block] of entries) {
this.tileStore.fromLegacy(Number(key), block);
}
for (const [key, block] of entries) {
if (!block.faceIds) continue;
const { down, up, left, right } = block.faceIds;
const downNum = this.tileStore.idToNumber(down);
if (downNum !== Number(key)) continue;
const upNum = this.tileStore.idToNumber(up);
const leftNum = this.tileStore.idToNumber(left);
const rightNum = this.tileStore.idToNumber(right);
this.roleFace.malloc(downNum, FaceDirection.Down);
if (!isNil(upNum)) {
this.roleFace.bind(upNum, downNum, FaceDirection.Up);
}
if (!isNil(leftNum)) {
this.roleFace.bind(leftNum, downNum, FaceDirection.Left);
}
if (!isNil(rightNum)) {
this.roleFace.bind(rightNum, downNum, FaceDirection.Right);
}
}
}
/**
* 初始化道具存储对象
* @param data 旧样板道具定义对象
*/
private initItemStore(data: typeof core.items.items) {
const entries = Object.entries(data);
for (const [id, legacy] of entries) {
const num = this.tileStore.idToNumber(id);
if (isNil(num)) {
logger.warn(145, id);
continue;
}
this.itemStore.fromLegacy(num, legacy);
}
}
/**
* 初始化怪物管理器对象
* @param data 旧样板怪物存储对象
*/
private initEnemyManager(data: Record<EnemyIds, Enemy>) {
const manager = this.enemyManager;
const reference = new Map<number, IReadonlyEnemy<IEnemyAttr>>();
for (const [id, enemy] of Object.entries(structuredClone(data))) {
const num = this.tileStore.idToNumber(id);
if (isNil(num)) continue;
if (enemy.faceIds) {
// 有 faceId 的要把其他的也映射到当前怪物
const { left, up, right, down } = enemy.faceIds;
const leftCode = this.tileStore.idToNumber(left)!;
const upCode = this.tileStore.idToNumber(up)!;
const rightCode = this.tileStore.idToNumber(right)!;
const downCode = this.tileStore.idToNumber(down)!;
const prefab = manager.fromLegacyEnemy(downCode, enemy);
reference.set(downCode, prefab);
manager.addPrefab(prefab);
this.roleFace.malloc(downCode, FaceDirection.Down);
this.roleFace.bind(leftCode, downCode, FaceDirection.Left);
this.roleFace.bind(upCode, downCode, FaceDirection.Up);
this.roleFace.bind(rightCode, downCode, FaceDirection.Down);
manager.reusePrefab(num, leftCode, left);
manager.reusePrefab(num, upCode, up);
manager.reusePrefab(num, rightCode, right);
} else {
const prefab = manager.fromLegacyEnemy(num, enemy);
reference.set(num, prefab);
manager.addPrefab(prefab);
}
}
manager.compareWith(reference);
}
/**
* 初始化地图状态
* @param floors 楼层列表
* @param data 每个楼层对应的旧版地图信息
*/
private initMapState(
floors: FloorIds[],
data: Record<FloorIds, ResolvedFloor>
) {
const reference = new Map<string, Map<number, Uint32Array>>();
for (const id of floors) {
const floor = data[id];
const state = this.maps.createMap(id, floor.width, floor.height);
const bg = state.addLayer();
const bg2 = state.addLayer();
const event = state.addLayer();
const fg = state.addLayer();
const fg2 = state.addLayer();
bg.setZIndex(BG_ZINDEX);
bg2.setZIndex(BG2_ZINDEX);
event.setZIndex(EVENT_ZINDEX);
fg.setZIndex(FG_ZINDEX);
fg2.setZIndex(FG2_ZINDEX);
state.setLayerAlias(bg, 'bg');
state.setLayerAlias(bg2, 'bg2');
state.setLayerAlias(event, 'event');
state.setEventLayer(event);
state.setLayerAlias(fg, 'fg');
state.setLayerAlias(fg2, 'fg2');
state.setActiveStatus(false);
const size = floor.width * floor.height;
const ref = new Map<number, Uint32Array>();
if (floor.bgmap && floor.bgmap.length > 0) {
const arr = new Uint32Array(floor.bgmap.flat());
bg.setMapRef(arr);
ref.set(BG_ZINDEX, new Uint32Array(arr));
} else {
ref.set(BG_ZINDEX, new Uint32Array(size));
}
if (floor.bg2map && floor.bg2map.length > 0) {
const arr = new Uint32Array(floor.bg2map.flat());
bg2.setMapRef(arr);
ref.set(BG2_ZINDEX, new Uint32Array(arr));
} else {
ref.set(BG2_ZINDEX, new Uint32Array(size));
}
if (floor.map && floor.map.length > 0) {
const arr = new Uint32Array(floor.map.flat());
event.setMapRef(arr);
ref.set(EVENT_ZINDEX, new Uint32Array(arr));
} else {
ref.set(EVENT_ZINDEX, new Uint32Array(size));
}
if (floor.fgmap && floor.fgmap.length > 0) {
const arr = new Uint32Array(floor.fgmap.flat());
fg.setMapRef(arr);
ref.set(FG_ZINDEX, new Uint32Array(arr));
} else {
ref.set(FG_ZINDEX, new Uint32Array(size));
}
if (floor.fg2map && floor.fg2map.length > 0) {
const arr = new Uint32Array(floor.fg2map.flat());
fg2.setMapRef(arr);
ref.set(FG2_ZINDEX, new Uint32Array(arr));
} else {
ref.set(FG2_ZINDEX, new Uint32Array(size));
}
reference.set(id, ref);
}
this.maps.compareWith(reference);
}
//#endregion
//#region 存档方法

View File

@ -3,7 +3,6 @@ import {
defineNonePropertySpecial,
IEnemyManager
} from '@user/data-base';
import { getHeroStatusOn } from '../legacy/hero';
import { IEnemyAttr } from '@user/data-common';
//#region 复合属性值类型
@ -176,6 +175,7 @@ export function registerSpecials(manager: IEnemyManager<IEnemyAttr>): void {
const { vampire, add } = special.value;
return (
`战斗前,怪物首先吸取角色的${vampire}%生命` +
// @ts-expect-error 需要重构
`(约${Math.floor((vampire / 100) * getHeroStatusOn('hp'))}点)作为伤害` +
(add ? `,并把伤害数值加到自身生命上。` : `。`)
);

View File

@ -1,7 +1,6 @@
export * from './enemy';
export * from './event';
export * from './hero';
export * from './legacy';
export * from './replay';
export * from './core';

View File

@ -1,115 +0,0 @@
/**
* 获取勇士在某一点的属性
* @param name 要获取的勇士属性
* @param floorId 勇士所在楼层
*/
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
export function getHeroStatusOn(
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOn<K extends keyof HeroStatus>(
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOn(
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
// @ts-expect-error 暂时无法推导
return getHeroStatusOf(core.status.hero, name, floorId);
}
/**
* 获取一定状态下的勇士在某一点的属性
* @param status 勇士的状态
* @param name 要获取的勇士属性
* @param floorId 勇士所在楼层
*/
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: 'all',
floorId?: FloorIds
): HeroStatus;
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOf<K extends keyof HeroStatus>(
status: Partial<HeroStatus>,
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOf(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
return getRealStatus(status, name, floorId);
}
function getRealStatus(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId: FloorIds = core.status.floorId
): any {
if (name instanceof Array) {
const res: any = {};
name.forEach(v => {
res[v] = getRealStatus(status, v, floorId);
});
return res;
}
if (name === 'all') {
const res: any = {};
for (const [key, value] of Object.entries(core.status.hero)) {
if (typeof value === 'number') {
res[key] = getRealStatus(
status,
key as keyof HeroStatus,
floorId
);
} else {
res[key] = value;
}
}
return res;
}
let s = (status[name] ?? core.status.hero[name]) as number;
if (s === null || s === void 0) {
throw new ReferenceError(
`Wrong hero status property name is delivered: ${name}`
);
}
if (typeof s !== 'number') return s;
// buff
s *= core.status.hero.buff[name] ?? 1;
s = Math.floor(s);
// 衰弱效果
if ((name === 'atk' || name === 'def') && flags.weak) {
const weak = core.values.weakValue;
if (weak < 1) {
// 百分比衰弱
s *= 1 - weak;
} else {
s -= weak;
}
}
return s;
}
// 下面的内容暂时无用
export interface IHeroStatusDefault {
atk: number;
def: number;
hp: number;
}

View File

@ -1,6 +0,0 @@
export * from './hero';
export * from './interface';
export * from './item';
export * from './move';
export * from './tile';
export * from './utils';

View File

@ -1,4 +0,0 @@
export interface IStateDamageable {
/** 生命值 */
hp: number;
}

View File

@ -1,139 +0,0 @@
import {
IHeroAttr,
IItemEffect,
IItemEquipData,
IItemLegacyConverter,
IItemRawData,
ItemCategory
} from '@user/data-common';
import { IStateSystem } from '@user/data-system';
export type LegacyItemData = Item<AllIdsOf<'items'>>;
type LegacyItemEffectFn = (
state: IStateSystem,
item: IItemRawData<IHeroAttr>
) => void;
type LegacyItemCanUseFn = (
state: IStateSystem,
item: IItemRawData<IHeroAttr>
) => boolean;
class LegacyItemEffect implements IItemEffect<IHeroAttr> {
pickEvent: unknown;
useEvent: unknown;
/** 道具使用效果 */
private readonly useFn: LegacyItemEffectFn | null = null;
/** 是否能够使用道具 */
private readonly canUseFn: LegacyItemCanUseFn | null = null;
/** 全局状态对象 */
private readonly state: IStateSystem;
constructor(legacy: LegacyItemData, state: IStateSystem) {
this.state = state;
this.pickEvent = undefined;
this.useEvent = legacy.useItemEvent;
if (legacy.cls === 'items') {
if (legacy.itemEffect) {
this.useFn = new Function(
'state',
'item',
legacy.itemEffect
) as LegacyItemEffectFn;
}
} else if (legacy.cls === 'constants' || legacy.cls === 'tools') {
if (legacy.useItemEffect) {
this.useFn = new Function(
'state',
'item',
legacy.useItemEffect
) as LegacyItemEffectFn;
}
if (typeof legacy.canUseItemEffect === 'string') {
this.canUseFn = new Function(
'state',
'item',
legacy.canUseItemEffect
) as LegacyItemCanUseFn;
} else {
this.canUseFn = () => !!legacy.canUseItemEffect;
}
}
}
useEffect(item: IItemRawData<IHeroAttr>): void {
this.useFn?.(this.state, item);
}
canUse(item: IItemRawData<IHeroAttr>): boolean {
if (!this.canUseFn) return true;
else return this.canUseFn(this.state, item);
}
}
export class ItemLegacyBridge implements IItemLegacyConverter<
IHeroAttr,
LegacyItemData
> {
constructor(private readonly state: IStateSystem) {}
fromLegacy(num: number, legacy: LegacyItemData): IItemRawData<IHeroAttr> {
const effect = new LegacyItemEffect(legacy, this.state);
return {
num,
id: legacy.id,
category: this.mapCategory(legacy.cls),
name: legacy.name,
text: legacy.text ?? '',
hideInToolbox: legacy.hideInToolBox,
effect,
equip: this.convertEquip(legacy.equip)
};
}
/**
* 将旧样板装备信息转换为新样板接口
* @param legacy 旧样板格式的装备信息
*/
private convertEquip(legacy: Equip): IItemEquipData<IHeroAttr> {
const valueMap = new Map<SelectKey<IHeroAttr, number>, number>();
const perMap = new Map<SelectKey<IHeroAttr, number>, number>();
for (const [key, value] of Object.entries(legacy.value)) {
valueMap.set(key as SelectKey<IHeroAttr, number>, value);
}
for (const [key, value] of Object.entries(legacy.percentage)) {
perMap.set(key as SelectKey<IHeroAttr, number>, value);
}
return {
slots: [legacy.type],
animate: legacy.animate,
value: valueMap,
percentage: perMap,
loadEvent: legacy.equipEvent,
unloadEvent: legacy.unequipEvent
};
}
/**
* 从旧样板道具类型获取新接口对应的类型
* @param cls 旧样板道具类型
*/
private mapCategory(cls: ItemCls): ItemCategory {
switch (cls) {
case 'constants':
return ItemCategory.Constant;
case 'tools':
return ItemCategory.Consumable;
case 'equips':
return ItemCategory.Equipment;
case 'items':
return ItemCategory.Pick;
default:
return ItemCategory.Unknown;
}
}
}

View File

@ -1,523 +0,0 @@
import EventEmitter from 'eventemitter3';
import { backDir, toDir } from './utils';
import { loading } from '@user/data-base';
import type { HeroKeyMover } from '@user/client-modules';
import { sleep } from '@motajs/common';
import { fromDirectionString } from '@user/data-common';
function getDataStateMover() {
if (typeof Mota === 'undefined') return null;
return Mota.require('@user/data-state').state.hero.location.mover;
}
// todo: 转身功能
interface MoveStepDir {
type: 'dir';
value: Move2;
}
interface MoveStepSpeed {
type: 'speed';
value: number;
}
export type MoveStep = MoveStepDir | MoveStepSpeed;
export interface IMoveController {
/** 本次移动是否完成 */
done: boolean;
/** 当前移动队列 */
queue: MoveStep[];
/** 当本次移动完成时兑现 */
onEnd: Promise<void>;
/**
* 停止本次移动,停止后将不再能继续移动
*/
stop(): Promise<void>;
/**
* 向队列末尾添加移动信息
* @param step 移动信息
*/
push(...step: MoveStep[]): void;
}
interface EObjectMovingEvent {
stepEnd: [step: MoveStep];
moveEnd: [];
moveStart: [queue: MoveStep[]];
}
export abstract class ObjectMoverBase extends EventEmitter<EObjectMovingEvent> {
/** 移动队列 */
private moveQueue: MoveStep[] = [];
/** 当前的移动速度 */
moveSpeed: number = 100;
/** 当前的移动方向 */
moveDir: Dir2 = 'down';
/** 当前是否正在移动 */
moving: boolean = false;
/** 面朝方向 */
faceDir: Dir2 = 'down';
/** 当前的控制对象 */
controller?: IMoveController;
/**
* 当本次移动开始时执行的函数
* @param controller 本次移动的控制对象
*/
protected abstract onMoveStart(controller: IMoveController): Promise<void>;
/**
* 当本次移动结束时执行的函数
* @param controller 本次移动的控制对象
*/
protected abstract onMoveEnd(controller: IMoveController): Promise<void>;
/**
* 当这一步开始移动时执行的函数,可以用来播放移动动画等
* @param step 这一步的移动信息
* @param controller 本次移动的控制对象
* @returns 一个Promise,其值表示本次移动的代码,并将其值传入这一步移动后的操作,即{@link onStepEnd}
*/
protected abstract onStepStart(
step: MoveStepDir,
controller: IMoveController
): Promise<number>;
/**
* 当这一步移动结束后执行的函数(即{@link onStepStart}成功兑现后),可以用于设置物体坐标等
* @param step 这一步的移动信息
* @param code 本步移动的代码,即onMoveStart的返回值
* @param controller 本次移动的控制对象
* @returns 一个Promise,当其兑现时,本步移动全部完成,将会开始下一步的移动
*/
protected abstract onStepEnd(
step: MoveStepDir,
code: number,
controller: IMoveController
): Promise<void>;
/**
* 当移动速度被设置时执行的函数
* @param speed 设置为的移动速度
* @param controller 本次移动的控制对象
*/
protected abstract onSetMoveSpeed(
speed: number,
controller: IMoveController
): void;
private getMoveDir(move: Move2): Dir2 {
if (move === 'forward') return this.moveDir;
if (move === 'backward') return backDir(this.faceDir);
return move;
}
private getFaceDir(move: Move2): Dir2 {
if (move === 'forward' || move === 'backward') return this.faceDir;
return move;
}
/**
* 设置当前面朝方向,移动中设置无效
* @param dir 面朝方向
*/
setFaceDir(dir: Dir2) {
if (!this.moving) this.faceDir = dir;
}
/**
* 设置当前移动方向,移动中设置无效
* @param dir 移动方向
*/
setMoveDir(dir: Dir2) {
if (!this.moving) this.moveDir = dir;
}
/**
* 开始移动,这个操作后,本次移动将不能通过{@link insertMove}添加移动步骤,
* 只能通过{@link IMoveController.push}添加,而通过前者添加的移动步骤会在下次移动生效
*/
startMove(): IMoveController | null {
if (this.moving) return null;
const queue = this.moveQueue.slice();
this.clearMoveQueue();
this.moving = true;
let stopMove = false;
const start = async () => {
// 等待宏任务执行完成,不然controller会在首次调用中未定义
await new Promise<void>(res => res());
await this.onMoveStart(controller);
this.emit('moveStart', queue);
while (queue.length > 0) {
if (stopMove || !this.moving) break;
const step = queue.shift();
if (!step) break;
if (step.type === 'dir') {
this.moveDir = this.getMoveDir(step.value);
this.faceDir = this.getFaceDir(step.value);
const code = await this.onStepStart(step, controller);
await this.onStepEnd(step, code, controller);
} else {
const speed = step.value;
this.moveSpeed = speed;
this.onSetMoveSpeed(speed, controller);
}
this.emit('stepEnd', step);
}
this.moving = false;
this.emit('moveEnd');
await this.onMoveEnd(controller);
};
const onEnd = start().then(() => {
this.controller = void 0;
controller.done = true;
});
const controller: IMoveController = {
done: false,
onEnd,
queue,
push(...step) {
queue.push(...step);
},
stop() {
stopMove = true;
return onEnd;
}
};
this.controller = controller;
return controller;
}
/**
* 向移动队列末尾插入移动实例
* @param move 移动实例
*/
insertMove(...move: MoveStep[]) {
this.moveQueue.push(...move);
}
/**
* 清空移动队列
*/
clearMoveQueue() {
this.moveQueue = [];
}
/**
* 向移动队列末尾插入一个移动操作
* @param step 移动方向
*/
oneStep(step: Move2) {
this.moveQueue.push({ type: 'dir', value: step });
}
/**
* 按照传入的数组移动物体,插入至移动队列末尾
* @param steps 移动步骤
*/
moveAs(steps: MoveStep[]) {
this.moveQueue.push(...steps);
}
}
// todo: refactor
interface CanMoveStatus {
/** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */
canMove: boolean;
/** 由图块的noPass计算出的信息,不可移动时会触发触发器 */
noPass: boolean;
}
const enum HeroMoveCode {
Step,
Stop,
/** 不能移动,并撞击前面一格的图块,触发其触发器 */
Hit,
/** 不能移动,同时当前格有CannotOut,或目标格有CannotIn,不会触发前面一格的触发器 */
CannotMove
}
export class HeroMover extends ObjectMoverBase {
/** 当前移动是否忽略地形 */
private ignoreTerrain: boolean = false;
/** 当前移动是否不计入录像 */
private noRoute: boolean = false;
/** 当前移动是否是在lockControl条件下开始的 */
private inLockControl: boolean = false;
/** 是否会在特殊时刻进行自动存档 */
private autoSave: boolean = false;
/** 本次移动开始时的移动速度 */
private beforeMoveSpeed: number = 100;
override startMove(
ignoreTerrain: boolean = false,
noRoute: boolean = false,
inLockControl: boolean = false,
autoSave: boolean = false
): IMoveController | null {
if (this.moving) return null;
this.ignoreTerrain = ignoreTerrain;
this.noRoute = noRoute;
this.inLockControl = inLockControl;
this.autoSave = autoSave;
return super.startMove();
}
private checkAutoSave(_x: number, _y: number, _nx: number, _ny: number) {
// const index = `${x},${y}`;
// const nIndex = `${nx},${ny}`;
// const map = core.status.thisMap.enemy.mapDamage;
// const dam = map[index];
// const nextDam = map[nIndex];
// if (!dam || !nextDam) return;
// 可以在这里判断地图伤害,并进行自动存档,例如在进入或离开地图伤害时存档
// if (dam.damage > 0 || nextDam.damage > 0) {
// core.autosave()
// }
}
protected async onMoveStart(controller: IMoveController): Promise<void> {
this.beforeMoveSpeed = this.moveSpeed;
if (!core.isReplaying() || core.status.replay.speed <= 12) {
getDataStateMover()?.start();
}
// 这里要检查前面那一格能不能走,不能走则不触发平滑视角,以避免撞墙上视角卡住
if (!this.ignoreTerrain) {
const { x, y } = core.status.hero.loc;
const firstDir = controller.queue.find(
v => v.type === 'dir'
)?.value;
if (firstDir && firstDir !== 'backward' && firstDir !== 'forward') {
const data = this.checkCanMove(x, y, toDir(firstDir as Dir));
if (data.canMove && !data.noPass) {
// viewport.sync('startMove');
}
}
} else {
// viewport.sync('startMove');
}
}
protected async onMoveEnd(controller: IMoveController): Promise<void> {
this.moveSpeed = this.beforeMoveSpeed;
this.onSetMoveSpeed(this.moveSpeed, controller);
// viewport.sync('endMove');
core.clearContinueAutomaticRoute();
core.stopAutomaticRoute();
}
protected async onStepStart(
_step: MoveStepDir,
controller: IMoveController
): Promise<HeroMoveCode> {
const showDir = toDir(this.faceDir);
const dir4Move = toDir(this.moveDir);
core.setHeroLoc('direction', showDir);
const { x, y } = core.status.hero.loc;
const { x: nx, y: ny } = this.nextLoc(x, y, this.moveDir);
if (this.autoSave && !this.ignoreTerrain) {
this.checkAutoSave(x, y, nx, ny);
}
if (!this.inLockControl && core.status.lockControl) {
controller.stop();
return HeroMoveCode.Stop;
}
if (!this.ignoreTerrain || !this.noRoute) {
this.moveDir = dir4Move;
}
const dir = this.moveDir;
if (!this.ignoreTerrain) {
const { noPass, canMove } = this.checkCanMove(x, y, dir4Move);
if (!canMove) {
return HeroMoveCode.CannotMove;
}
// 不能移动
if (noPass) {
return HeroMoveCode.Hit;
}
}
// 可以移动,显示移动动画
await this.moveAnimate(nx, ny, showDir, dir);
return HeroMoveCode.Step;
}
protected async onStepEnd(
_step: MoveStepDir,
code: HeroMoveCode,
controller: IMoveController
): Promise<void> {
const { x, y } = core.status.hero.loc;
const { x: nx, y: ny } = this.nextLoc(x, y, this.moveDir);
const showDir = toDir(this.faceDir);
// 前方不能移动
if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Hit) {
controller.stop();
this.onCannotMove(showDir);
if (code === HeroMoveCode.Hit) {
core.trigger(nx, ny);
}
core.checkRouteFolding();
return;
}
// 本次移动停止
if (code === HeroMoveCode.Stop) {
core.clearContinueAutomaticRoute();
core.stopAutomaticRoute();
controller.stop();
return;
}
// 本次移动正常完成
if (code === HeroMoveCode.Step) {
core.setHeroLoc('x', nx, true);
core.setHeroLoc('y', ny, true);
if (!this.ignoreTerrain) {
const direction = core.getHeroLoc('direction');
core.control._moveAction_popAutomaticRoute();
if (!this.noRoute) core.status.route.push(direction);
// 中毒处理
if (core.hasFlag('poison')) {
core.status.hero.hp -= core.values.poisonDamage;
if (core.status.hero.hp <= 0) {
core.status.hero.hp = 0;
core.events.lose();
}
core.updateStatusBar();
}
core.moveOneStep();
core.checkRouteFolding();
}
}
}
protected onSetMoveSpeed(
speed: number,
_controller: IMoveController
): void {
this.moveSpeed = speed;
}
/**
* 移动动画
* @param x 目标横坐标
* @param y 目标纵坐标
* @param _showDir 显示方向
* @param moveDir 移动方向
*/
private async moveAnimate(
x: number,
y: number,
_showDir: Dir,
moveDir: Dir2
) {
// const viewport = HeroMover.viewport;
// if (!viewport) return;
const replay = core.status.replay.speed;
const speed = replay === 24 ? 1 : this.moveSpeed / replay;
// viewport.all('moveTo', x, y, speed * 1.6);
const replaying = core.isReplaying();
if (replaying) {
if (core.status.replay.speed > 12) {
await sleep(speed);
getDataStateMover()?.setPos(x, y);
} else {
const mover = getDataStateMover();
mover?.step(fromDirectionString(moveDir));
await mover?.start()?.onEnd;
}
} else {
const mover = getDataStateMover();
mover?.step(fromDirectionString(moveDir));
await mover?.start()?.onEnd;
}
}
/**
* 当前方一格不能走时,执行的函数
* @param dir 移动方向
*/
private onCannotMove(dir: Dir) {
if (!this.noRoute) core.status.route.push(dir);
core.status.automaticRoute.moveStepBeforeStop = [];
core.status.automaticRoute.lastDirection = dir;
if (core.status.automaticRoute.moveStepBeforeStop.length === 0) {
core.clearContinueAutomaticRoute();
core.stopAutomaticRoute();
}
}
/**
* 检查前方是否可以移动
* @param x 当前横坐标
* @param y 当前纵坐标
* @param dir 移动方向
*/
private checkCanMove(x: number, y: number, dir: Dir): CanMoveStatus {
// 如果是循环式地图
const { x: nx, y: ny } = this.nextLoc(x, y, dir);
const noPass = core.noPass(nx, ny);
const canMove = core.canMoveHero(x, y, dir);
return { noPass, canMove };
}
/**
* 获取前方一格的坐标
* @param x 当前横坐标
* @param y 当前纵坐标
* @param dir 移动方向
*/
private nextLoc(x: number, y: number, dir: Dir2): Loc {
const { x: dx, y: dy } = core.utils.scan2[dir];
const nx = x + dx;
const ny = y + dy;
return { x: nx, y: ny };
}
}
interface HeroMoveCollection {
mover: HeroMover;
keyMover?: HeroKeyMover;
}
// 初始化基础勇士移动实例
const heroMover = new HeroMover();
export const heroMoveCollection: HeroMoveCollection = {
mover: heroMover
};
loading.once('coreInit', () => {
// 注册按键操作
Mota.r(() => {
const { HeroKeyMover } = Mota.require('@user/client-modules');
const { gameKey } = Mota.require('@motajs/system');
const keyMover = new HeroKeyMover(gameKey, heroMover);
heroMoveCollection.keyMover = keyMover;
});
});

View File

@ -1,123 +0,0 @@
import {
ITileLegacyConverter,
ITilePassData,
ITileRawData,
PassBit,
TileType
} from '@user/data-common';
interface ILegacyTileEventData {
/** 旧样板图块携带的默认事件映射 */
readonly events?: Record<number, string>;
}
export type LegacyTileData = MapDataOf<keyof NumberToId> & ILegacyTileEventData;
export class TileLegacyBridge implements ITileLegacyConverter<LegacyTileData> {
private getTileType(num: number, legacy: LegacyTileData): TileType {
if (num === 0) return TileType.None;
switch (legacy.cls) {
case 'terrains':
return TileType.Terrain;
case 'autotile':
return TileType.Autotile;
case 'animates':
return TileType.Animate;
case 'items':
return TileType.Item;
case 'enemys':
case 'enemy48':
return TileType.Enemy;
case 'npcs':
case 'npc48':
return TileType.Npc;
// @ts-expect-error 动态类型声明导致的错误,忽略即可
case 'tileset':
return TileType.Tileset;
default:
return TileType.Unknown;
}
}
private getPass(num: number, legacy: LegacyTileData): ITilePassData {
if (num === 0) {
// 空图块
return {
onlyEvents: true,
inPass: 0b1111,
outPass: 0b1111
};
} else if (num === 17) {
// 空气墙
return {
onlyEvents: false,
inPass: 0b0000,
outPass: 0b0000
};
} else {
// 正常图块
if (legacy.cannotIn && legacy.cannotOut) {
let inPass = 0b1111;
let outPass = 0b1111;
if (legacy.cannotIn.includes('up')) {
inPass &= ~PassBit.Up;
}
if (legacy.cannotIn.includes('right')) {
inPass &= ~PassBit.Right;
}
if (legacy.cannotIn.includes('down')) {
inPass &= ~PassBit.Down;
}
if (legacy.cannotIn.includes('left')) {
inPass &= ~PassBit.Left;
}
if (legacy.cannotOut.includes('up')) {
outPass &= ~PassBit.Up;
}
if (legacy.cannotOut.includes('right')) {
outPass &= ~PassBit.Right;
}
if (legacy.cannotOut.includes('down')) {
outPass &= ~PassBit.Down;
}
if (legacy.cannotOut.includes('left')) {
outPass &= ~PassBit.Left;
}
return {
onlyEvents: false,
inPass,
outPass
};
} else {
return {
onlyEvents: true,
inPass: 0b1111,
outPass: 0b1111
};
}
}
}
/** 将旧样板事件输入复制为新的默认事件对象 */
private getEvents(legacy: LegacyTileData): Record<number, string> {
const events: Record<number, string> = {};
for (const [priority, event] of Object.entries(legacy.events ?? {})) {
const priorityNum = Number(priority);
if (Number.isInteger(priorityNum) && typeof event === 'string') {
events[priorityNum] = event;
}
}
return events;
}
fromLegacy(num: number, legacy: LegacyTileData): ITileRawData {
return {
num,
id: legacy.id,
events: this.getEvents(legacy),
type: this.getTileType(num, legacy),
pass: this.getPass(num, legacy),
eventPass: !legacy.noPass
};
}
}

View File

@ -1,107 +0,0 @@
const dirMap: Record<Dir2, Dir> = {
down: 'down',
left: 'left',
leftdown: 'left',
leftup: 'left',
right: 'right',
rightdown: 'right',
rightup: 'right',
up: 'up'
};
/**
* 将八方向转换为四方向,一般用于斜角移动时的方向显示
* @param dir 八方向
*/
export function toDir(dir: Dir2): Dir {
return dirMap[dir];
}
const backDirMap: Record<Dir2, Dir2> = {
up: 'down',
down: 'up',
left: 'right',
right: 'left',
leftup: 'rightdown',
rightup: 'leftdown',
leftdown: 'rightup',
rightdown: 'leftup'
};
export function backDir(dir: Dir): Dir;
export function backDir(dir: Dir2): Dir2;
export function backDir(dir: Dir2): Dir2 {
return backDirMap[dir];
}
export function locInMap(x: number, y: number, floorId: FloorIds) {
const { width, height } = core.status.maps[floorId];
return x >= 0 && y >= 0 && x < width && y < height;
}
/**
* 广义检查能否移动,指定两个点以及行走方向,返回能否执行上述移动。
* 可以传入地图之外的点,视为可以随意移动。仅检查 cannotIn 和 cannotOut,不检查 noPass
* @param fx 出点横坐标
* @param fy 出点纵坐标
* @param tx 入点横坐标
* @param ty 入点纵坐标
* @param dir 行走方向
* @param fromFloorId 出点楼层id
* @param toFloorId 入点楼层id
*/
export function checkCanMoveExtended(
fx: number,
fy: number,
tx: number,
ty: number,
dir: Dir,
fromFloorId: FloorIds = core.status.floorId,
toFloorId: FloorIds = core.status.floorId
) {
const fromInMap = locInMap(fx, fy, fromFloorId);
const toInMap = locInMap(tx, ty, toFloorId);
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
if (fromInMap) {
const cannotMove = core.floors[fromFloorId].cannotMove;
const fromIndex: LocString = `${fx},${fy}`;
// 检查当前点是否有不可出
if (cannotMove[fromIndex]?.includes(dir)) return false;
const blocks = getBlockForLoc(fx, fy, fromFloorId);
const can = blocks.some(v => {
if (v === 0) return false;
const out = map[v as Exclude<AllNumbers, 0>]?.cannotOut;
return out?.includes(dir);
});
if (can) return false;
}
if (toInMap) {
const cannotMoveIn = core.floors[toFloorId].cannotMoveIn;
const toIndex: LocString = `${tx},${ty}`;
const back = backDir(dir);
// 检查目标点是否有不可入
if (cannotMoveIn[toIndex]?.includes(back)) return false;
const blocks = getBlockForLoc(tx, ty, toFloorId);
const can = blocks.some(v => {
if (v === 0) return false;
const out = map[v as Exclude<AllNumbers, 0>]?.cannotIn;
return out?.includes(back);
});
if (can) return false;
}
return true;
}
function getBlockForLoc(x: number, y: number, floorId: FloorIds) {
const map = core.status.maps[floorId];
const floor = core.floors[floorId];
return [
floor.bgmap[y][x],
floor.bg2map[y][x],
map.map[y][x],
floor.bgmap[y][x],
floor.bg2map[y][x]
];
}

View File

@ -1,44 +0,0 @@
import { describe, expect, it } from 'vitest';
import { TileStore } from '@user/data-common';
import { LegacyTileData, TileLegacyBridge } from '../src/legacy/tile';
describe('TileLegacyBridge events-map contract', () => {
// 验证旧样板事件对象会转换为 TileStore 可消费的默认事件映射
it('converts legacy events into the TileStore event map', () => {
const bridge = new TileLegacyBridge();
const store = new TileStore<LegacyTileData>();
store.attachLegacyConverter(bridge);
const legacy: LegacyTileData = {
id: 'yellowWall',
cls: 'terrains',
events: { 10: 'onEnter', 20: 'onTouch' },
noPass: true
};
const converted = store.fromLegacy(1, legacy);
expect(converted.events).toEqual({ 10: 'onEnter', 20: 'onTouch' });
expect(store.getEvent(1)).toEqual(
new Map([
[10, 'onEnter'],
[20, 'onTouch']
])
);
expect(converted.eventPass).toBe(false);
expect(converted).not.toHaveProperty('trigger');
});
// 验证旧样板缺少事件输入时仍生成合法的空事件映射
it('uses an empty events map when legacy input has no events', () => {
const bridge = new TileLegacyBridge();
const legacy: LegacyTileData = {
id: 'whiteWall',
cls: 'terrains'
};
const converted = bridge.fromLegacy(2, legacy);
expect(converted.events).toEqual({});
expect(converted).not.toHaveProperty('trigger');
});
});

View File

@ -62,6 +62,7 @@ export function getNowStatus(nowEquip?: Equip, onCol: boolean = false) {
'hpmax',
'money'
] as (keyof SelectType<HeroStatus, number>)[];
// @ts-expect-error 需要重构
const { getHeroStatusOn } = Mota.require('@user/data-state');
return (