mirror of
https://github.com/unanmed/HumanBreak.git
synced 2026-11-15 03:04:30 +08:00
refactor: src/game 全部移入 monorepo
This commit is contained in:
1 parent
f7c6434aba
commit
ef8d3e9e2e
54 files changed
+722
-254
No files matched your search
@@ -1,282 +0,0 @@
|
||||
import { DamageEnemy, ensureFloorDamage, getSingleEnemy } from './damage';
|
||||
import { findDir, has } from '../../plugin/game/utils';
|
||||
import { hook, loading } from '../game';
|
||||
import { HeroSkill, NightSpecial } from '../mechanism/misc';
|
||||
|
||||
export interface CurrentEnemy {
|
||||
enemy: DamageEnemy;
|
||||
// 这个是干啥的?
|
||||
onMapEnemy: DamageEnemy[];
|
||||
}
|
||||
|
||||
export function getEnemy(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds = core.status.floorId
|
||||
) {
|
||||
const enemy = core.status.maps[floorId].enemy.get(x, y);
|
||||
return enemy;
|
||||
}
|
||||
|
||||
function init() {
|
||||
core.enemys.canBattle = function canBattle(
|
||||
x: number | DamageEnemy,
|
||||
y: number,
|
||||
floorId: FloorIds = core.status.floorId
|
||||
) {
|
||||
const enemy = typeof x === 'number' ? getEnemy(x, y, floorId) : x;
|
||||
if (!enemy) {
|
||||
throw new Error(
|
||||
`Cannot get enemy on x:${x}, y:${y}, floor: ${floorId}`
|
||||
);
|
||||
}
|
||||
const { damage } = enemy.calDamage();
|
||||
|
||||
return damage < core.status.hero.hp;
|
||||
};
|
||||
|
||||
core.events.battle = function battle(
|
||||
x: number | DamageEnemy,
|
||||
y: number,
|
||||
force: boolean = false,
|
||||
callback?: () => void
|
||||
) {
|
||||
core.saveAndStopAutomaticRoute();
|
||||
const isLoc = typeof x === 'number';
|
||||
const enemy = isLoc ? getEnemy(x, y) : x;
|
||||
if (!enemy) {
|
||||
throw new Error(
|
||||
`Cannot battle with enemy since no enemy on ${x},${y}`
|
||||
);
|
||||
}
|
||||
// 非强制战斗
|
||||
// @ts-ignore
|
||||
if (!core.canBattle(x, y) && !force && !core.status.event.id) {
|
||||
core.stopSound();
|
||||
core.playSound('操作失败');
|
||||
core.drawTip('你打不过此怪物!', enemy!.id);
|
||||
return core.clearContinueAutomaticRoute(callback);
|
||||
}
|
||||
// 自动存档
|
||||
if (!core.status.event.id) core.autosave(true);
|
||||
// 战前事件
|
||||
// 战后事件
|
||||
core.afterBattle(enemy, isLoc ? x : enemy.x, y);
|
||||
callback?.();
|
||||
};
|
||||
|
||||
const getFacedId = (enemy: DamageEnemy) => {
|
||||
const e = enemy.enemy;
|
||||
|
||||
if (e.displayIdInBook) return e.displayIdInBook;
|
||||
if (e.faceIds) return e.faceIds.down;
|
||||
return e.id;
|
||||
};
|
||||
|
||||
core.enemys.getCurrentEnemys = function getCurrentEnemys(
|
||||
floorId = core.status.floorId
|
||||
) {
|
||||
floorId = floorId || core.status.floorId;
|
||||
const enemys: CurrentEnemy[] = [];
|
||||
const used: Record<string, DamageEnemy[]> = {};
|
||||
ensureFloorDamage(floorId);
|
||||
const floor = core.status.maps[floorId];
|
||||
floor.enemy.list.forEach(v => {
|
||||
const id = getFacedId(v);
|
||||
if (!(id in used)) {
|
||||
const e = new DamageEnemy(v.enemy);
|
||||
e.calAttribute();
|
||||
e.getRealInfo();
|
||||
e.calDamage();
|
||||
const curr: CurrentEnemy = {
|
||||
enemy: e,
|
||||
onMapEnemy: [v]
|
||||
};
|
||||
enemys.push(curr);
|
||||
used[id] = curr.onMapEnemy;
|
||||
} else {
|
||||
used[id].push(v);
|
||||
}
|
||||
});
|
||||
|
||||
return enemys.sort((a, b) => {
|
||||
const ad = a.enemy.calDamage().damage;
|
||||
const bd = b.enemy.calDamage().damage;
|
||||
return ad - bd;
|
||||
});
|
||||
};
|
||||
|
||||
core.events._sys_battle = function (data: Block, callback?: () => void) {
|
||||
// 检查战前事件
|
||||
const floor = core.floors[core.status.floorId];
|
||||
const beforeBattle: MotaEvent = [];
|
||||
const loc = `${data.x},${data.y}` as LocString;
|
||||
const enemy = getEnemy(data.x, data.y);
|
||||
|
||||
beforeBattle.push(...(floor.beforeBattle[loc] ?? []));
|
||||
beforeBattle.push(...(enemy!.enemy.beforeBattle ?? []));
|
||||
|
||||
if (beforeBattle.length > 0) {
|
||||
beforeBattle.push({ type: 'battle', x: data.x, y: data.y });
|
||||
core.clearContinueAutomaticRoute();
|
||||
|
||||
// 自动存档
|
||||
var inAction = core.status.event.id == 'action';
|
||||
if (inAction) {
|
||||
core.insertAction(beforeBattle, data.x, data.y);
|
||||
core.doAction();
|
||||
} else {
|
||||
core.autosave(true);
|
||||
core.insertAction(beforeBattle, data.x, data.y, callback);
|
||||
}
|
||||
} else {
|
||||
core.battle(data.x, data.y, false, callback);
|
||||
}
|
||||
};
|
||||
|
||||
core.events._action_battle = function (data, x, y, prefix) {
|
||||
if (data.id) {
|
||||
const enemy = getSingleEnemy(data.id as EnemyIds);
|
||||
// todo: 与不在地图上的怪物战斗
|
||||
} else {
|
||||
if (data.floorId != core.status.floorId) {
|
||||
core.doAction();
|
||||
return;
|
||||
}
|
||||
const [ex, ey] = this.__action_getLoc(
|
||||
data.loc,
|
||||
x,
|
||||
y,
|
||||
prefix
|
||||
) as LocArr;
|
||||
core.battle(ex, ey, true, core.doAction);
|
||||
}
|
||||
};
|
||||
|
||||
core.events.afterBattle = function afterBattle(
|
||||
enemy: DamageEnemy,
|
||||
x?: number,
|
||||
y?: number
|
||||
) {
|
||||
const floorId = core.status.floorId;
|
||||
const special = enemy.info.special;
|
||||
|
||||
// 播放战斗动画
|
||||
let animate: AnimationIds = 'hand';
|
||||
// 检查当前装备是否存在攻击动画
|
||||
const equipId = core.getEquip(0);
|
||||
if (equipId && (core.material.items[equipId].equip || {}).animate)
|
||||
animate = core.material.items[equipId].equip.animate;
|
||||
|
||||
// 检查该动画是否存在SE,如果不存在则使用默认音效
|
||||
if (!core.material.animates[animate]?.se) core.playSound('attack.opus');
|
||||
|
||||
// 战斗伤害
|
||||
const info = enemy.calDamage(core.status.hero);
|
||||
const damage = info.damage;
|
||||
// 判定是否致死
|
||||
if (damage >= core.status.hero.hp) {
|
||||
core.status.hero.hp = 0;
|
||||
core.updateStatusBar(false, true);
|
||||
core.events.lose('战斗失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 扣减体力值并记录统计数据
|
||||
core.status.hero.hp -= damage;
|
||||
core.status.hero.statistics.battleDamage += damage;
|
||||
core.status.hero.statistics.battle++;
|
||||
|
||||
// 智慧之源
|
||||
if (special.has(14) && flags.hard === 2) {
|
||||
core.addFlag(
|
||||
'inte_' + floorId,
|
||||
Math.ceil((core.status.hero.mdef / 10) * 0.3) * 10
|
||||
);
|
||||
core.status.hero.mdef -=
|
||||
Math.ceil((core.status.hero.mdef / 10) * 0.3) * 10;
|
||||
}
|
||||
|
||||
// 极昼永夜
|
||||
if (special.has(22)) {
|
||||
NightSpecial.addNight(floorId, -enemy.info.night!);
|
||||
}
|
||||
if (special.has(23)) {
|
||||
NightSpecial.addNight(floorId, enemy.info.day!);
|
||||
}
|
||||
|
||||
// 如果是融化怪,需要特殊标记一下
|
||||
if (special.has(25) && has(x) && has(y)) {
|
||||
flags[`melt_${floorId}`] ??= {};
|
||||
flags[`melt_${floorId}`][`${x},${y}`] = enemy.info.melt;
|
||||
}
|
||||
|
||||
// 获得金币
|
||||
const money = enemy.info.money!;
|
||||
core.status.hero.money += money;
|
||||
core.status.hero.statistics.money += money;
|
||||
|
||||
// 获得经验
|
||||
const exp = enemy.info.exp!;
|
||||
core.status.hero.exp += exp;
|
||||
core.status.hero.statistics.exp += exp;
|
||||
|
||||
const hint =
|
||||
'打败 ' + enemy.enemy.name + ',金币+' + money + ',经验+' + exp;
|
||||
core.drawTip(hint, enemy.id);
|
||||
|
||||
HeroSkill.disableSkill();
|
||||
|
||||
// 事件的处理
|
||||
const todo: MotaEvent = [];
|
||||
|
||||
// 战后事件
|
||||
if (has(core.status.floorId)) {
|
||||
const loc = `${x},${y}` as LocString;
|
||||
todo.push(
|
||||
...(core.floors[core.status.floorId].afterBattle[loc] ?? [])
|
||||
);
|
||||
}
|
||||
todo.push(...(enemy.enemy.afterBattle ?? []));
|
||||
|
||||
// 如果事件不为空,将其插入
|
||||
if (todo.length > 0) core.insertAction(todo, x, y);
|
||||
|
||||
if (has(x) && has(y)) {
|
||||
core.drawAnimate(animate, x, y);
|
||||
core.removeBlock(x, y);
|
||||
} else core.drawHeroAnimate(animate);
|
||||
|
||||
// 如果已有事件正在处理中
|
||||
if (core.status.event.id == null) core.continueAutomaticRoute();
|
||||
else core.clearContinueAutomaticRoute();
|
||||
|
||||
core.checkAutoEvents();
|
||||
|
||||
hook.emit('afterBattle', enemy, x, y);
|
||||
};
|
||||
}
|
||||
loading.once('coreInit', init);
|
||||
|
||||
declare global {
|
||||
interface Enemys {
|
||||
getCurrentEnemys(floorId: FloorIds): CurrentEnemy[];
|
||||
canBattle(enemy: DamageEnemy, _?: number, floorId?: FloorIds): boolean;
|
||||
canBattle(x: number, y: number, floorId?: FloorIds): boolean;
|
||||
}
|
||||
|
||||
interface Events {
|
||||
battle(
|
||||
enemy: DamageEnemy,
|
||||
_?: number,
|
||||
force?: boolean,
|
||||
callback?: () => void
|
||||
): void;
|
||||
battle(
|
||||
x: number,
|
||||
y?: number,
|
||||
force?: boolean,
|
||||
callback?: () => void
|
||||
): void;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,290 +0,0 @@
|
||||
import { getHeroStatusOn } from '@/game/state/hero';
|
||||
import { EnemyInfo } from './damage';
|
||||
|
||||
export interface SpecialDeclaration {
|
||||
code: number;
|
||||
name: string | ((enemy: EnemyInfo) => string);
|
||||
desc: string | ((enemy: EnemyInfo) => string);
|
||||
color: string;
|
||||
}
|
||||
|
||||
const fromFunc = (
|
||||
func: string | ((enemy: EnemyInfo) => string),
|
||||
enemy: EnemyInfo
|
||||
) => {
|
||||
return typeof func === 'string' ? func : func(enemy);
|
||||
};
|
||||
|
||||
export const specials: SpecialDeclaration[] = [
|
||||
{
|
||||
code: 0,
|
||||
name: '空',
|
||||
desc: '空',
|
||||
color: '#fff'
|
||||
},
|
||||
{
|
||||
code: 1,
|
||||
name: '致命一击',
|
||||
desc: enemy => `怪物每5回合触发一次强力攻击,造成${enemy.crit}%的伤害`,
|
||||
color: '#fc3'
|
||||
},
|
||||
{
|
||||
code: 2,
|
||||
name: '恶毒',
|
||||
desc: '怪物攻击无视勇士的防御',
|
||||
color: '#bbb0ff'
|
||||
},
|
||||
{
|
||||
code: 3,
|
||||
name: '坚固',
|
||||
desc: '怪物防御不小于勇士攻击-1',
|
||||
color: '#c0b088'
|
||||
},
|
||||
{
|
||||
code: 4,
|
||||
name: '2连击',
|
||||
desc: '怪物每回合攻击2次',
|
||||
color: '#fe7'
|
||||
},
|
||||
{
|
||||
code: 5,
|
||||
name: '3连击',
|
||||
desc: '怪物每回合攻击3次',
|
||||
color: '#fe7'
|
||||
},
|
||||
{
|
||||
code: 6,
|
||||
name: enemy => `${enemy.n ?? 4}连击`,
|
||||
desc: enemy => `怪物每回合攻击${enemy.n}次`,
|
||||
color: '#fe7'
|
||||
},
|
||||
{
|
||||
code: 7,
|
||||
name: '饥渴',
|
||||
desc: enemy =>
|
||||
`战斗前,怪物降低勇士${enemy.hungry}%的攻击,并加在自己身上`,
|
||||
color: '#b67'
|
||||
},
|
||||
{
|
||||
code: 8,
|
||||
name: '抱团',
|
||||
desc: enemy =>
|
||||
`怪物周围5×5范围内每有一个拥有该属性的怪物(不包括自身),则对方攻防就增加${enemy.together}%(线性叠加)` +
|
||||
`,受加成怪物会在右上角以绿色字体显示当前周围有多少个怪物`,
|
||||
color: '#fa4'
|
||||
},
|
||||
{
|
||||
code: 9,
|
||||
name: '绝对防御',
|
||||
desc: '怪物的奇特护甲可以让勇士的额外攻击失效,攻击变为基础攻击+额外攻击',
|
||||
color: '#80eed6'
|
||||
},
|
||||
{
|
||||
code: 10,
|
||||
name: '勇气之刃',
|
||||
desc: enemy => `怪物第一回合造成${enemy.courage}%的伤害`,
|
||||
color: '#b0c0dd'
|
||||
},
|
||||
{
|
||||
code: 11,
|
||||
name: '勇气冲锋',
|
||||
desc: enemy =>
|
||||
`怪物首先攻击,造成${enemy.charge}%的伤害,并眩晕勇士5回合`,
|
||||
color: '#ff00d2'
|
||||
},
|
||||
{
|
||||
code: 12,
|
||||
name: '追猎',
|
||||
desc: '当勇士移动到该怪物的水平或竖直方向上时,怪物向勇士移动一格',
|
||||
color: '#9e8'
|
||||
},
|
||||
{
|
||||
code: 13,
|
||||
name: '魔攻',
|
||||
desc: '怪物攻击无视勇士的防御',
|
||||
color: '#bbb0ff'
|
||||
},
|
||||
{
|
||||
code: 14,
|
||||
name: '智慧之源',
|
||||
desc: '困难难度下(简单难度没有效果),战斗后,怪物会吸取勇士30%的智慧(勇士智慧向下取整至整十)加在本层的拥有该属性的怪物攻击上',
|
||||
color: '#bbeef0'
|
||||
},
|
||||
{
|
||||
code: 15,
|
||||
name: '突刺',
|
||||
desc: enemy =>
|
||||
`勇士走到怪物怪物周围四格时,怪物对勇士造成${core.formatBigNumber(
|
||||
Math.max((enemy.value || 0) - getHeroStatusOn('def'))
|
||||
)}点伤害`,
|
||||
color: '#c677dd'
|
||||
},
|
||||
{
|
||||
code: 16,
|
||||
name: '空',
|
||||
desc: '空',
|
||||
color: '#fff'
|
||||
},
|
||||
{
|
||||
code: 17,
|
||||
name: '先攻',
|
||||
desc: '战斗时,怪物首先攻击',
|
||||
color: '#b0b666'
|
||||
},
|
||||
{
|
||||
code: 18,
|
||||
name: '阻击',
|
||||
desc: enemy =>
|
||||
`经过怪物十字范围内时怪物后退一格,同时对勇士造成${enemy.value}点伤害`,
|
||||
color: '#8888e6'
|
||||
},
|
||||
{
|
||||
code: 19,
|
||||
name: '电摇嘲讽',
|
||||
desc:
|
||||
'当勇士移动到怪物同行或同列时,勇士会直接冲向怪物,撞碎路上的所有地形和门,拾取路上的道具,与路上的怪物战斗' +
|
||||
',最后与该怪物战斗',
|
||||
color: '#ff6666'
|
||||
},
|
||||
{
|
||||
code: 20,
|
||||
name: '霜冻',
|
||||
desc: enemy =>
|
||||
`怪物寒冷的攻击使勇士动作变慢,勇士每回合对怪物造成的伤害减少${enemy.ice}%。装备杰克的衣服后可以免疫。`,
|
||||
color: 'cyan'
|
||||
},
|
||||
{
|
||||
code: 21,
|
||||
name: '冰封光环',
|
||||
desc: enemy =>
|
||||
`寒气逼人,使勇士对该怪物周围7*7范围内的怪物伤害减少${enemy.iceHalo}%(线性叠加)`,
|
||||
color: 'cyan'
|
||||
},
|
||||
{
|
||||
code: 22,
|
||||
name: '永夜',
|
||||
desc: enemy =>
|
||||
`战斗后,减少勇士${enemy.night}点攻防,增加本层所有怪物${enemy.night}点攻防,仅在本层有效`,
|
||||
color: '#d8a'
|
||||
},
|
||||
{
|
||||
code: 23,
|
||||
name: '极昼',
|
||||
desc: enemy =>
|
||||
`战斗后,减少本层所有怪物${enemy.day}点攻防,增加勇士${enemy.day}点攻防,仅在本层有效`,
|
||||
color: '#ffd'
|
||||
},
|
||||
{
|
||||
code: 24,
|
||||
name: '射击',
|
||||
desc: function () {
|
||||
return '经过怪物同行或同列的可视范围内时受到一次普通攻击的伤害';
|
||||
},
|
||||
color: '#dda0dd'
|
||||
},
|
||||
{
|
||||
code: 25,
|
||||
name: '融化',
|
||||
desc: enemy =>
|
||||
`战斗后该怪物会融化,在怪物位置产生一个3*3的范围光环,光环内怪物的攻防增加${enemy.melt}%`,
|
||||
color: '#e6e099'
|
||||
},
|
||||
{
|
||||
code: 26,
|
||||
name: '冰封之核',
|
||||
desc: enemy =>
|
||||
`怪物拥有逼人的寒气,使周围5*5范围内的怪物防御增加${enemy.iceCore}%`,
|
||||
color: '#70ffd1'
|
||||
},
|
||||
{
|
||||
code: 27,
|
||||
name: '火焰之核',
|
||||
desc: enemy =>
|
||||
`怪物拥有灼热的火焰,使周围5*5范围内的怪物攻击增加${enemy.fireCore}%`,
|
||||
color: '#ff6f0a'
|
||||
},
|
||||
{
|
||||
code: 28,
|
||||
name: '苍蓝刻',
|
||||
desc: enemy =>
|
||||
`怪物使用苍蓝之灵的力量,使自身受到的伤害减少${enemy.paleShield}%`,
|
||||
color: '#ff6f0a'
|
||||
},
|
||||
{
|
||||
code: 29,
|
||||
name: '杀戮光环',
|
||||
desc: enemy => {
|
||||
let content = '';
|
||||
enemy.specialHalo?.forEach((v, i) => {
|
||||
content +=
|
||||
' '.repeat(8) +
|
||||
`${i + 1}. <span style="color: ${
|
||||
specials[v].color
|
||||
}">${fromFunc(specials[v].name, enemy)}</span>: ${fromFunc(
|
||||
specials[v].desc,
|
||||
enemy
|
||||
)}<br />`;
|
||||
});
|
||||
return (
|
||||
`怪物周围方形${enemy.haloRange}格范围内所有怪物获得以下特殊属性(包括自身),` +
|
||||
`特殊属性数值间为${
|
||||
enemy.specialMultiply ? '相乘' : '相加'
|
||||
}关系:<br />` +
|
||||
content
|
||||
);
|
||||
},
|
||||
color: '#F721F7'
|
||||
},
|
||||
{
|
||||
code: 30,
|
||||
name: '乾坤挪移',
|
||||
desc: enemy => {
|
||||
const [dx, dy] = enemy.translation!;
|
||||
let str = `此怪物在场时,所有光环`;
|
||||
if (dx !== 0) {
|
||||
str += `向${dx < 0 ? '左' : '右'}平移${Math.abs(dx)}格,`;
|
||||
}
|
||||
if (dy !== 0) {
|
||||
str += `向${dy < 0 ? '上' : '下'}平移${Math.abs(dy)}格,`;
|
||||
}
|
||||
str += `不同怪物间为加算叠加`;
|
||||
return str;
|
||||
},
|
||||
color: '#FDCD0B'
|
||||
},
|
||||
{
|
||||
code: 31,
|
||||
name: '再生光环',
|
||||
desc: enemy =>
|
||||
`怪物周围7*7范围内的所有怪物(包括自身)生命值提高${enemy.hpHalo}%`,
|
||||
color: '#85FF99'
|
||||
},
|
||||
{
|
||||
code: 32,
|
||||
name: '同化',
|
||||
desc: enemy =>
|
||||
`怪物会获得自身周围方形${enemy.assimilateRange}格范围内怪物的特殊属性(光环类属性除外),` +
|
||||
`特殊属性数值间为${enemy.specialMultiply ? '乘算' : '加算'}关系`,
|
||||
color: '#ffd366'
|
||||
},
|
||||
{
|
||||
code: 33,
|
||||
name: '战争号角',
|
||||
desc: enemy => {
|
||||
const [hp, atk, def] = enemy.horn ?? [];
|
||||
let str = '地图上每存在一个怪物(包括自身),自身';
|
||||
if (hp) str += `生命值增加${hp}%,`;
|
||||
if (atk) str += `攻击增加${atk}%,`;
|
||||
if (def) str += `防御增加${def}%,`;
|
||||
str += '线性叠加。该怪物会在右上角以黄色字体显示当前地图有多少怪物';
|
||||
return str;
|
||||
},
|
||||
color: '#fff866'
|
||||
},
|
||||
{
|
||||
code: 34,
|
||||
name: '赏金',
|
||||
desc: `怪物没有任何能力,也没有任何战利品,也无法让勇士恢复生命值,战斗伤害恒为0`,
|
||||
color: '#faff33'
|
||||
}
|
||||
];
|
||||
@@ -1,233 +0,0 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import type { DamageEnemy, EnemyCollection } from './enemy/damage';
|
||||
|
||||
// ----- 加载事件
|
||||
interface GameLoadEvent {
|
||||
coreLoaded: [];
|
||||
autotileLoaded: [];
|
||||
coreInit: [];
|
||||
loaded: [];
|
||||
}
|
||||
|
||||
class GameLoading extends EventEmitter<GameLoadEvent> {
|
||||
private autotileLoaded: number = 0;
|
||||
private autotileNum?: number;
|
||||
private autotileListened: boolean = false;
|
||||
loaded: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.once('coreInit', () => {
|
||||
this.autotileNum = Object.keys(core.material.icons.autotile).length;
|
||||
});
|
||||
this.once('loaded', () => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
addAutotileLoaded() {
|
||||
this.autotileLoaded++;
|
||||
if (this.autotileLoaded === this.autotileNum) {
|
||||
this.emit('autotileLoaded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当自动原件加载完毕时
|
||||
* @param autotiles 自动原件数组
|
||||
*/
|
||||
onAutotileLoaded(
|
||||
autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>>
|
||||
) {
|
||||
if (this.autotileListened) return;
|
||||
this.autotileListened = true;
|
||||
this.on('autotileLoaded', () => {
|
||||
const keys = Object.keys(
|
||||
core.material.icons.autotile
|
||||
) as AllIdsOf<'autotile'>[];
|
||||
|
||||
keys.forEach(v => {
|
||||
core.material.images.autotile[v] = autotiles[v]!;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
core.maps._makeAutotileEdges();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const loading = new GameLoading();
|
||||
|
||||
export interface GameEvent {
|
||||
/** Emitted in libs/events.js resetGame. */
|
||||
reset: [];
|
||||
/** Emitted in src/App.vue setup. */
|
||||
mounted: [];
|
||||
/** Emitted in plugin/game/ui.ts updateStatusBar_update */
|
||||
statusBarUpdate: [];
|
||||
/** Emitted in core/index.ts */
|
||||
renderLoaded: [];
|
||||
/** Emitted in libs/events.js getItem */
|
||||
afterGetItem: [
|
||||
itemId: AllIdsOf<'items'>,
|
||||
x: number,
|
||||
y: number,
|
||||
isGentleClick: boolean
|
||||
];
|
||||
/** Emitted in libs/events.js _openDoor_animate */
|
||||
afterOpenDoor: [doorId: AllIdsOf<'animates'>, x: number, y: number];
|
||||
/** Emitted in project/functions.js afterChangeFloor */
|
||||
afterChangeFloor: [floorId: FloorIds];
|
||||
/** Emitted in project/functions.js moveOneStep */
|
||||
moveOneStep: [x: number, y: number, floorId: FloorIds];
|
||||
/** Emitted in src/game/enemy/battle.ts afterBattle */
|
||||
afterBattle: [enemy: DamageEnemy, x?: number, y?: number];
|
||||
/** Emitted in libs/events.js changingFloor */
|
||||
changingFloor: [floorId: FloorIds, heroLoc: Loc];
|
||||
/** Emitted in libs/maps.js setBlock */
|
||||
setBlock: [
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
newBlock: AllNumbers,
|
||||
oldBlock: AllNumbers
|
||||
];
|
||||
/** Emitted in game/enemy/damage.ts */
|
||||
enemyExtract: [col: EnemyCollection];
|
||||
/** Emitted in lib/events.js restart */
|
||||
restart: [];
|
||||
/** Emitted in lib/maps.js setBgFgBlock */
|
||||
setBgFgBlock: [
|
||||
name: 'bg' | 'fg' | 'bg2' | 'fg2',
|
||||
number: AllNumbers,
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds
|
||||
];
|
||||
/** Emitted in lib/control.js */
|
||||
replayStatus: [replaying: boolean];
|
||||
}
|
||||
|
||||
export const hook = new EventEmitter<GameEvent>();
|
||||
|
||||
interface ListenerEvent {
|
||||
// block
|
||||
hoverBlock: [block: Block, ev: MouseEvent];
|
||||
leaveBlock: [block: Block, ev: MouseEvent, leaveGame: boolean];
|
||||
clickBlock: [block: Block, ev: MouseEvent];
|
||||
// mouse
|
||||
mouseMove: [ev: MouseEvent];
|
||||
}
|
||||
|
||||
class GameListener extends EventEmitter<ListenerEvent> {
|
||||
static num: number = 0;
|
||||
|
||||
num: number = GameListener.num++;
|
||||
|
||||
mouseX: number = -1;
|
||||
mouseY: number = -1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (main.replayChecking) return;
|
||||
if (!!window.core) {
|
||||
this.init();
|
||||
} else {
|
||||
loading.once('coreInit', () => {
|
||||
this.init();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
// ----- block
|
||||
|
||||
const data = core.canvas.data.canvas;
|
||||
|
||||
const getBlockLoc = (px: number, py: number, size: number) => {
|
||||
return [
|
||||
Math.floor(((px * 32) / size + core.bigmap.offsetX) / 32),
|
||||
Math.floor(((py * 32) / size + core.bigmap.offsetY) / 32)
|
||||
];
|
||||
};
|
||||
|
||||
// hover & leave & mouseMove
|
||||
data.addEventListener('mousemove', e => {
|
||||
if (
|
||||
core.status.lockControl ||
|
||||
!core.isPlaying() ||
|
||||
!core.status.floorId
|
||||
)
|
||||
return;
|
||||
this.emit('mouseMove', e);
|
||||
const {
|
||||
x: px,
|
||||
y: py,
|
||||
size
|
||||
} = core.actions._getClickLoc(e.offsetX, e.offsetY);
|
||||
const [bx, by] = getBlockLoc(px, py, size);
|
||||
const blocks = core.getMapBlocksObj();
|
||||
if (this.mouseX !== bx || this.mouseY !== by) {
|
||||
const lastBlock = blocks[`${this.mouseX},${this.mouseY}`];
|
||||
const block = blocks[`${bx},${by}`];
|
||||
if (!!lastBlock) {
|
||||
this.emit('leaveBlock', lastBlock, e, false);
|
||||
}
|
||||
if (!!block) {
|
||||
this.emit('hoverBlock', block, e);
|
||||
this.mouseX = bx;
|
||||
this.mouseY = by;
|
||||
} else {
|
||||
this.mouseX = -1;
|
||||
this.mouseY = -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
data.addEventListener('mouseleave', e => {
|
||||
if (
|
||||
core.status.lockControl ||
|
||||
!core.isPlaying() ||
|
||||
!core.status.floorId
|
||||
)
|
||||
return;
|
||||
const blocks = core.getMapBlocksObj();
|
||||
const lastBlock = blocks[`${this.mouseX},${this.mouseY}`];
|
||||
if (!!lastBlock) {
|
||||
this.emit('leaveBlock', lastBlock, e, true);
|
||||
}
|
||||
this.mouseX = -1;
|
||||
this.mouseY = -1;
|
||||
});
|
||||
// click
|
||||
data.addEventListener('click', e => {
|
||||
if (
|
||||
core.status.lockControl ||
|
||||
!core.isPlaying() ||
|
||||
!core.status.floorId
|
||||
)
|
||||
return;
|
||||
const {
|
||||
x: px,
|
||||
y: py,
|
||||
size
|
||||
} = core.actions._getClickLoc(e.offsetX, e.offsetY);
|
||||
const [bx, by] = getBlockLoc(px, py, size);
|
||||
const blocks = core.getMapBlocksObj();
|
||||
const block = blocks[`${bx},${by}`];
|
||||
if (!!block) {
|
||||
this.emit('clickBlock', block, e);
|
||||
}
|
||||
});
|
||||
|
||||
// ----- mouse
|
||||
}
|
||||
}
|
||||
|
||||
export const gameListener = new GameListener();
|
||||
|
||||
declare global {
|
||||
interface Main {
|
||||
loading: GameLoading;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import './system';
|
||||
import '../plugin/game/index';
|
||||
import * as damage from './enemy/damage';
|
||||
import { EventEmitter, IndexedEventEmitter } from '@motajs/legacy-common';
|
||||
import { specials } from './enemy/special';
|
||||
import { gameListener, hook, loading } from './game';
|
||||
import * as battle from './enemy/battle';
|
||||
import * as hero from './state/hero';
|
||||
import * as miscMechanism from './mechanism/misc';
|
||||
import { ItemState } from './state/item';
|
||||
import {
|
||||
BlockMover,
|
||||
heroMoveCollection,
|
||||
HeroMover,
|
||||
ObjectMoverBase
|
||||
} from './state/move';
|
||||
|
||||
// ----- 类注册
|
||||
Mota.register('class', 'DamageEnemy', damage.DamageEnemy);
|
||||
Mota.register('class', 'EnemyCollection', damage.EnemyCollection);
|
||||
Mota.register('class', 'EventEmitter', EventEmitter);
|
||||
Mota.register('class', 'IndexedEventEmitter', IndexedEventEmitter);
|
||||
// ----- 函数注册
|
||||
Mota.register('fn', 'getEnemy', battle.getEnemy);
|
||||
Mota.register('fn', 'getHeroStatusOn', hero.getHeroStatusOn);
|
||||
Mota.register('fn', 'getHeroStatusOf', hero.getHeroStatusOf);
|
||||
Mota.register('fn', 'ensureFloorDamage', damage.ensureFloorDamage);
|
||||
// ----- 变量注册
|
||||
Mota.register('var', 'enemySpecials', specials);
|
||||
Mota.register('var', 'hook', hook);
|
||||
Mota.register('var', 'gameListener', gameListener);
|
||||
Mota.register('var', 'loading', loading);
|
||||
// ----- 模块注册
|
||||
Mota.register('module', 'Mechanism', {
|
||||
BluePalace: miscMechanism.BluePalace,
|
||||
NightSpecial: miscMechanism.NightSpecial,
|
||||
MiscData: miscMechanism.MiscData,
|
||||
HeroSkill: miscMechanism.HeroSkill
|
||||
});
|
||||
Mota.register('module', 'State', {
|
||||
ItemState,
|
||||
HeroMover,
|
||||
BlockMover,
|
||||
ObjectMoverBase,
|
||||
heroMoveCollection
|
||||
});
|
||||
|
||||
main.loading = loading;
|
||||
|
||||
loading.once('coreInit', () => {
|
||||
Mota.Plugin.init();
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
core.checkAutosave();
|
||||
});
|
||||
hook.on('restart', () => {
|
||||
core.checkAutosave();
|
||||
});
|
||||
@@ -1,402 +0,0 @@
|
||||
import { backDir, has } from '@/plugin/game/utils';
|
||||
import { loading } from '../game';
|
||||
import type { LayerDoorAnimate } from '@motajs/render';
|
||||
import { getSkillLevel } from '@/plugin/game/skillTree';
|
||||
|
||||
/**
|
||||
* 一些零散机制的数据
|
||||
*/
|
||||
export namespace MiscData {
|
||||
/** 循环式地图 */
|
||||
export const loopMaps: Set<FloorIds> = new Set(['tower6']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 永夜/极昼
|
||||
*/
|
||||
export namespace NightSpecial {
|
||||
const nightMap = new Map<FloorIds, number>();
|
||||
|
||||
export function getNight(floor: FloorIds) {
|
||||
return nightMap.get(floor) ?? 0;
|
||||
}
|
||||
|
||||
export function addNight(floor: FloorIds, value: number) {
|
||||
const num = nightMap.get(floor) ?? 0;
|
||||
nightMap.set(floor, num + value);
|
||||
}
|
||||
|
||||
export function clearNight(floors: Iterable<FloorIds>) {
|
||||
for (const floor of floors) {
|
||||
nightMap.delete(floor);
|
||||
}
|
||||
}
|
||||
|
||||
export function saveNight() {
|
||||
return nightMap.entries();
|
||||
}
|
||||
|
||||
export function loadNight(night: Iterable<[FloorIds, number]>) {
|
||||
nightMap.clear();
|
||||
for (const [floor, num] of night) {
|
||||
nightMap.set(floor, num);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAll() {
|
||||
return nightMap;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace HeroSkill {
|
||||
export const enum Skill {
|
||||
None,
|
||||
/** 断灭之刃 */
|
||||
Blade,
|
||||
/** 铸剑为盾 */
|
||||
Shield,
|
||||
/** 跳跃 */
|
||||
Jump
|
||||
}
|
||||
|
||||
export const Blade = Skill.Blade;
|
||||
export const Shield = Skill.Shield;
|
||||
export const Jump = Skill.Jump;
|
||||
|
||||
const skillNameMap = new Map<Skill, string>([
|
||||
[Skill.Blade, '断灭之刃'],
|
||||
[Skill.Shield, '铸剑为盾'],
|
||||
[Skill.Jump, '跳跃']
|
||||
]);
|
||||
|
||||
const skillDesc = new Map<Skill, (level: number) => string>([
|
||||
[
|
||||
Skill.Blade,
|
||||
level => `攻击上升 ${level * 10}%,防御下降 ${level * 10}%`
|
||||
],
|
||||
[
|
||||
Skill.Shield,
|
||||
level => `防御上升 ${level * 10}%,攻击下降 ${level * 10}%`
|
||||
],
|
||||
[Skill.Jump, () => `跳过前方障碍,或踢走面前的怪物`]
|
||||
]);
|
||||
|
||||
interface SkillSave {
|
||||
autoSkill: boolean;
|
||||
learned: Skill[];
|
||||
}
|
||||
|
||||
const learned = new Set<Skill>();
|
||||
let autoSkill = true;
|
||||
let enabled: Skill = Skill.None;
|
||||
|
||||
export function getLevel(skill: Skill = getEnabled()) {
|
||||
switch (skill) {
|
||||
case Blade:
|
||||
return getSkillLevel(2);
|
||||
case Jump:
|
||||
return learned.has(Jump) ? 1 : 0;
|
||||
case Shield:
|
||||
return getSkillLevel(10);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getSkillName(skill: Skill = getEnabled()) {
|
||||
return skillNameMap.get(skill) ?? '未开启技能';
|
||||
}
|
||||
|
||||
export function getSkillDesc(
|
||||
skill: Skill = getEnabled(),
|
||||
level: number = getLevel()
|
||||
) {
|
||||
return skillDesc.get(skill)?.(level) ?? '';
|
||||
}
|
||||
|
||||
export function setAutoSkill(auto: boolean) {
|
||||
autoSkill = auto;
|
||||
}
|
||||
|
||||
export function getAutoSkill() {
|
||||
return autoSkill;
|
||||
}
|
||||
|
||||
export function learnedSkill(skill: Skill) {
|
||||
return learned.has(skill);
|
||||
}
|
||||
|
||||
export function learnSkill(skill: Skill) {
|
||||
learned.add(skill);
|
||||
}
|
||||
|
||||
export function forgetSkill(skill: Skill) {
|
||||
learned.delete(skill);
|
||||
}
|
||||
|
||||
export function clearSkill() {
|
||||
learned.clear();
|
||||
}
|
||||
|
||||
export function saveSkill(): SkillSave {
|
||||
return { autoSkill, learned: [...learned] };
|
||||
}
|
||||
|
||||
export function loadSkill(skills: SkillSave) {
|
||||
learned.clear();
|
||||
for (const skill of skills.learned) {
|
||||
learned.add(skill);
|
||||
}
|
||||
autoSkill = skills.autoSkill;
|
||||
}
|
||||
|
||||
export function getAll() {
|
||||
return learned;
|
||||
}
|
||||
|
||||
export function toggleSkill(skill: Skill) {
|
||||
if (!learned.has(skill)) return;
|
||||
if (enabled !== skill) enabled = skill;
|
||||
else enabled = Skill.None;
|
||||
}
|
||||
|
||||
export function enableSkill(skill: Skill) {
|
||||
if (!learned.has(skill)) return;
|
||||
enabled = skill;
|
||||
}
|
||||
|
||||
export function disableSkill() {
|
||||
enabled = Skill.None;
|
||||
}
|
||||
|
||||
export function getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BluePalace {
|
||||
type DoorConvertInfo = [id: AllIds, x: number, y: number];
|
||||
|
||||
// ---------- 黄蓝门转换
|
||||
|
||||
export function doorConvert(
|
||||
x?: number,
|
||||
y?: number,
|
||||
floorId: FloorIds = core.status.floorId
|
||||
) {
|
||||
core.autosave();
|
||||
core.extractBlocks(floorId);
|
||||
const blocks = core.status.maps[floorId].blocks;
|
||||
|
||||
const toConvert: DoorConvertInfo[] = [];
|
||||
blocks.forEach(v => {
|
||||
if (v.id === 492) {
|
||||
core.setBlock(494, v.x, v.y, floorId);
|
||||
toConvert.push(['A492', v.x, v.y]);
|
||||
} else if (v.id === 494) {
|
||||
core.setBlock(492, v.x, v.y, floorId);
|
||||
toConvert.push(['A494', v.x, v.y]);
|
||||
}
|
||||
});
|
||||
|
||||
if (has(x) && has(y)) {
|
||||
core.removeBlock(x, y, floorId);
|
||||
}
|
||||
|
||||
if (core.isReplaying() || core.status.floorId !== floorId) {
|
||||
core.doAction();
|
||||
return;
|
||||
}
|
||||
|
||||
core.lockControl();
|
||||
core.playSound('door.opus');
|
||||
|
||||
const Adapter = Mota.require('module', 'Render').RenderAdapter;
|
||||
const adapter = Adapter.get<LayerDoorAnimate>('door-animate');
|
||||
const texture = Mota.require('module', 'Render').texture;
|
||||
if (adapter) {
|
||||
Promise.all(
|
||||
toConvert.map(v => {
|
||||
const block = core.initBlock(
|
||||
v[1],
|
||||
v[2],
|
||||
texture.idNumberMap[v[0]]
|
||||
);
|
||||
return adapter.all('openDoor', block);
|
||||
})
|
||||
).then(() => {
|
||||
core.unlockControl();
|
||||
core.doAction();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 传送门部分
|
||||
|
||||
export interface Portal {
|
||||
fx: number;
|
||||
fy: number;
|
||||
dir: Dir;
|
||||
tx: number;
|
||||
ty: number;
|
||||
toDir: Dir;
|
||||
}
|
||||
|
||||
export interface PortalTo {
|
||||
x: number;
|
||||
y: number;
|
||||
dir: Dir;
|
||||
}
|
||||
|
||||
type PortalMap = Map<FloorIds, Map<number, Partial<Record<Dir, PortalTo>>>>;
|
||||
|
||||
export const portalMap: PortalMap = new Map();
|
||||
|
||||
export const portals: Partial<Record<FloorIds, Portal[]>> = {
|
||||
MT76: [
|
||||
{ fx: 11, fy: 7, dir: 'right', tx: 4, ty: 6, toDir: 'down' },
|
||||
{ fx: 6, fy: 5, dir: 'left', tx: 8, ty: 13, toDir: 'right' }
|
||||
],
|
||||
MT77: [
|
||||
{ fx: 2, fy: 9, dir: 'up', tx: 10, ty: 13, toDir: 'right' },
|
||||
{ fx: 10, fy: 8, dir: 'right', tx: 3, ty: 0, toDir: 'down' },
|
||||
{ fx: 1, fy: 0, dir: 'down', tx: 8, ty: 1, toDir: 'left' }
|
||||
],
|
||||
MT78: [
|
||||
{ fx: 8, fy: 4, dir: 'right', tx: 8, ty: 6, toDir: 'left' },
|
||||
{ fx: 7, fy: 7, dir: 'up', tx: 1, ty: 0, toDir: 'down' }
|
||||
],
|
||||
MT79: [
|
||||
{ fx: 5, fy: 10, dir: 'right', tx: 9, ty: 7, toDir: 'left' },
|
||||
{ fx: 2, fy: 2, dir: 'up', tx: 7, ty: 5, toDir: 'down' },
|
||||
{ fx: 4, fy: 11, dir: 'up', tx: 5, ty: 7, toDir: 'right' },
|
||||
{ fx: 7, fy: 11, dir: 'down', tx: 7, ty: 9, toDir: 'up' }
|
||||
],
|
||||
MT80: [
|
||||
{ fx: 2, fy: 10, dir: 'right', tx: 1, ty: 2, toDir: 'down' },
|
||||
{ fx: 2, fy: 10, dir: 'left', tx: 13, ty: 5, toDir: 'up' }
|
||||
],
|
||||
MT81: [
|
||||
{ fx: 4, fy: 8, dir: 'right', tx: 1, ty: 11, toDir: 'down' },
|
||||
{ fx: 7, fy: 13, dir: 'right', tx: 13, ty: 5, toDir: 'up' }
|
||||
],
|
||||
MT82: [{ fx: 9, fy: 10, dir: 'left', tx: 6, ty: 5, toDir: 'left' }],
|
||||
MT83: [
|
||||
{ fx: 5, fy: 11, dir: 'left', tx: 9, ty: 11, toDir: 'right' },
|
||||
{ fx: 5, fy: 3, dir: 'left', tx: 9, ty: 3, toDir: 'right' },
|
||||
{ fx: 2, fy: 2, dir: 'up', tx: 2, ty: 12, toDir: 'down' },
|
||||
{ fx: 12, fy: 2, dir: 'up', tx: 12, ty: 12, toDir: 'down' }
|
||||
],
|
||||
MT84: [
|
||||
{ fx: 2, fy: 3, dir: 'right', tx: 12, ty: 3, toDir: 'left' },
|
||||
{ fx: 2, fy: 11, dir: 'right', tx: 12, ty: 11, toDir: 'left' }
|
||||
],
|
||||
MT94: [{ fx: 12, fy: 11, dir: 'left', tx: 5, ty: 1, toDir: 'left' }],
|
||||
MT95: [
|
||||
{ fx: 13, fy: 14, dir: 'up', tx: 7, ty: 8, toDir: 'left' },
|
||||
{ fx: 0, fy: 1, dir: 'right', tx: 14, ty: 1, toDir: 'left' },
|
||||
{ fx: 6, fy: 13, dir: 'right', tx: 6, ty: 0, toDir: 'down' }
|
||||
],
|
||||
MT96: [{ fx: 6, fy: 11, dir: 'down', tx: 4, ty: 14, toDir: 'up' }],
|
||||
MT97: [{ fx: 0, fy: 1, dir: 'right', tx: 8, ty: 9, toDir: 'right' }]
|
||||
};
|
||||
loading.once('coreInit', initPortals);
|
||||
|
||||
function generatePortalMap() {
|
||||
const delta: Record<Dir, [[number, number], [number, number]]> = {
|
||||
// 方向:[正向, 逆向]<进出>
|
||||
left: [
|
||||
[0, 0],
|
||||
[-1, 0]
|
||||
],
|
||||
down: [
|
||||
[0, 0],
|
||||
[0, 1]
|
||||
],
|
||||
right: [
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
],
|
||||
up: [
|
||||
[0, 0],
|
||||
[0, -1]
|
||||
]
|
||||
};
|
||||
for (const [floor, p] of Object.entries(portals)) {
|
||||
const width = core.floors[floor as FloorIds].width;
|
||||
const map = new Map<number, Partial<Record<Dir, PortalTo>>>();
|
||||
portalMap.set(floor as FloorIds, map);
|
||||
|
||||
// 正向映射
|
||||
p.forEach(v => {
|
||||
const [[fdx, fdy], [tdx, tdy]] = delta[v.dir];
|
||||
const [[toFdx, toFdy], [toTdx, toTdy]] =
|
||||
delta[backDir(v.toDir)];
|
||||
const fx = v.fx + fdx;
|
||||
const fy = v.fy + fdy;
|
||||
const tx = v.fx + tdx;
|
||||
const ty = v.fy + tdy;
|
||||
const index = fx + fy * width;
|
||||
const backIndex = tx + ty * width;
|
||||
if (index < 0 || backIndex < 0) return;
|
||||
let data = map.get(index);
|
||||
let backData = map.get(backIndex);
|
||||
if (!data) {
|
||||
data = {};
|
||||
map.set(index, data);
|
||||
}
|
||||
if (!backData) {
|
||||
backData = {};
|
||||
map.set(backIndex, backData);
|
||||
}
|
||||
|
||||
data[v.dir] = {
|
||||
x: v.tx + toFdx,
|
||||
y: v.ty + toFdy,
|
||||
dir: backDir(v.toDir)
|
||||
};
|
||||
backData[backDir(v.dir)] = {
|
||||
x: v.tx - toTdx,
|
||||
y: v.ty - toTdy,
|
||||
dir: v.toDir
|
||||
};
|
||||
});
|
||||
// 逆向映射
|
||||
p.forEach(v => {
|
||||
const [[fdx, fdy], [tdx, tdy]] = delta[backDir(v.toDir)];
|
||||
const [[toFdx, toFdy], [toTdx, toTdy]] = delta[v.dir];
|
||||
const fx = v.tx - fdx;
|
||||
const fy = v.ty - fdy;
|
||||
const tx = v.tx - tdx;
|
||||
const ty = v.ty - tdy;
|
||||
const index = fx + fy * width;
|
||||
const backIndex = tx + ty * width;
|
||||
|
||||
let data = map.get(index);
|
||||
let backData = map.get(backIndex);
|
||||
if (!data) {
|
||||
data = {};
|
||||
map.set(index, data);
|
||||
}
|
||||
if (!backData) {
|
||||
backData = {};
|
||||
map.set(backIndex, backData);
|
||||
}
|
||||
|
||||
data[v.toDir] = {
|
||||
x: v.fx + toFdx,
|
||||
y: v.fy + toFdy,
|
||||
dir: backDir(v.dir)
|
||||
};
|
||||
backData[backDir(v.toDir)] = {
|
||||
x: v.fx + toTdx,
|
||||
y: v.fy + toTdy,
|
||||
dir: v.dir
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initPortals() {
|
||||
generatePortalMap();
|
||||
}
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { HeroSkill, NightSpecial } from '../mechanism/misc';
|
||||
|
||||
/**
|
||||
* 获取勇士在某一点的属性
|
||||
* @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-ignore
|
||||
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 {
|
||||
const { getSkillLevel } = Mota.Plugin.require('skillTree_g');
|
||||
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;
|
||||
|
||||
// 永夜、极昼
|
||||
if (name === 'atk' || name === 'def') {
|
||||
s += NightSpecial.getNight(floorId);
|
||||
}
|
||||
|
||||
const enabled = HeroSkill.getEnabled();
|
||||
// 技能
|
||||
if (enabled === HeroSkill.Blade) {
|
||||
const level = getSkillLevel(2);
|
||||
if (name === 'atk') {
|
||||
s *= 1 + 0.1 * level;
|
||||
} else if (name === 'def') {
|
||||
s *= 1 - 0.1 * level;
|
||||
}
|
||||
} else if (enabled === HeroSkill.Shield) {
|
||||
const level = getSkillLevel(10);
|
||||
if (name === 'atk') {
|
||||
s *= 1 - 0.1 * level;
|
||||
} else if (name === 'def') {
|
||||
s *= 1 + 0.1 * level;
|
||||
}
|
||||
}
|
||||
|
||||
// buff
|
||||
s *= core.status.hero.buff[name] ?? 1;
|
||||
s = Math.floor(s);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
export interface IHeroStatusDefault {
|
||||
atk: number;
|
||||
def: number;
|
||||
hp: number;
|
||||
}
|
||||
|
||||
interface HeroStateEvent {
|
||||
set: [key: string | number | symbol, value: any];
|
||||
}
|
||||
|
||||
type HeroStatusCalculate = (
|
||||
hero: HeroState<any>,
|
||||
key: string | number | symbol,
|
||||
value: any
|
||||
) => any;
|
||||
|
||||
export class HeroState<
|
||||
T extends object = IHeroStatusDefault
|
||||
> extends EventEmitter<HeroStateEvent> {
|
||||
readonly status: T;
|
||||
readonly computedStatus: T;
|
||||
|
||||
readonly buffable: Set<keyof T> = new Set();
|
||||
readonly buffMap: Map<keyof T, number> = new Map();
|
||||
|
||||
private static cal: HeroStatusCalculate = (_0, _1, value) => value;
|
||||
|
||||
constructor(init: T) {
|
||||
super();
|
||||
this.status = init;
|
||||
this.computedStatus = cloneDeep(init);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个属性的值
|
||||
* @param key 要设置的属性
|
||||
* @param value 属性值
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
setStatus<K extends keyof T>(key: K, value: T[K]): boolean {
|
||||
this.status[key] = value;
|
||||
this.emit('set', key, value);
|
||||
return this.refreshStatus(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加或减少一个属性的值,只对数字型的属性有效
|
||||
* @param key 要修改的属性
|
||||
* @param value 属性的增量
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
addStatus<K extends SelectKey<T, number>>(key: K, value: number): boolean {
|
||||
if (typeof this.status[key] !== 'number') {
|
||||
logger.warn(14, String(key));
|
||||
return false;
|
||||
}
|
||||
return this.setStatus<K>(key, (this.status[key] + value) as T[K]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个属性的原始值
|
||||
* @param key 要获取的属性
|
||||
* @returns 属性的值
|
||||
*/
|
||||
getStatus<K extends keyof T>(key: K): T[K] {
|
||||
return this.status[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个属性计算后的值,也就是2.x所说的勇士真实属性
|
||||
* @param key 要获取的属性值
|
||||
*/
|
||||
getComputedStatus<K extends keyof T>(key: K): T[K] {
|
||||
return this.computedStatus[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记某个属性为可以被buff加成
|
||||
*/
|
||||
markBuffable(key: SelectKey<T, number>): void {
|
||||
if (typeof this.status[key] !== 'number') {
|
||||
logger.warn(12, String(key));
|
||||
return;
|
||||
}
|
||||
this.buffable.add(key);
|
||||
this.buffMap.set(key, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个属性的buff值
|
||||
* @param key 要设置buff的属性
|
||||
* @param value buff值
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
setBuff(key: SelectKey<T, number>, value: number): boolean {
|
||||
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
|
||||
logger.warn(13, String(key));
|
||||
return false;
|
||||
}
|
||||
this.buffMap.set(key, value);
|
||||
return this.refreshStatus(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加或减少一个buff值
|
||||
* @param key 要修改的buff属性
|
||||
* @param value buff增量
|
||||
* @returns 是否修改成功
|
||||
*/
|
||||
addBuff(key: SelectKey<T, number>, value: number): boolean {
|
||||
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
|
||||
logger.warn(13, String(key));
|
||||
return false;
|
||||
}
|
||||
return this.setBuff(key, this.buffMap.get(key)! + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新某个或所有属性,重新进行计算
|
||||
* @param key 要刷新的属性名,不填表示刷新所有属性
|
||||
* @returns 是否计算成功
|
||||
*/
|
||||
refreshStatus(key?: keyof T): boolean {
|
||||
if (key === void 0) {
|
||||
for (const [key, value] of Object.entries(this.status)) {
|
||||
// @ts-ignore
|
||||
this.computedStatus[key] = HeroState.cal(this, key, value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
this.computedStatus[key] = HeroState.cal(this, key, this.status[key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算所有可以buff加成的属性
|
||||
* @returns 是否计算成功
|
||||
*/
|
||||
refreshBuffable(): boolean {
|
||||
for (const key of this.buffable) {
|
||||
this.computedStatus[key] = HeroState.cal(
|
||||
this,
|
||||
key,
|
||||
this.status[key]
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复写属性计算函数,默认函数不进行计算,直接将原属性返回
|
||||
* @param fn 计算函数,传入两个参数,key表示属性名,value表示属性值,返回值表示计算结果
|
||||
*/
|
||||
static overrideCalculate(fn: HeroStatusCalculate) {
|
||||
this.cal = fn;
|
||||
}
|
||||
}
|
||||
|
||||
interface IHeroItem {
|
||||
items: Map<AllIdsOf<'items'>, number>;
|
||||
|
||||
/**
|
||||
* 设置勇士拥有的物品数量
|
||||
* @param item 物品id
|
||||
* @param value 物品数量
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
setItem(item: AllIdsOf<'items'>, value: number): boolean;
|
||||
|
||||
/**
|
||||
* 增加或减少勇士拥有的物品数量
|
||||
* @param item 物品id
|
||||
* @param value 物品数量增量
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
addItem(item: AllIdsOf<'items'>, value: number): boolean;
|
||||
|
||||
/**
|
||||
* 使用一个物品
|
||||
* @param item 物品id
|
||||
* @returns 是否使用成功
|
||||
*/
|
||||
useItem(item: AllIdsOf<'items'>, x?: number, y?: number): boolean;
|
||||
|
||||
/**
|
||||
* 获得一个物品
|
||||
* @param item 物品id
|
||||
* @param num 获得的数量
|
||||
*/
|
||||
getItem(item: AllIdsOf<'items'>, num: number): void;
|
||||
/**
|
||||
* 获得一个物品
|
||||
* @param item 物品id
|
||||
* @param x 物品所在x坐标
|
||||
* @param y 物品所在y坐标
|
||||
* @param floorId 物品所在楼层
|
||||
* @param num 获得的数量
|
||||
*/
|
||||
getItem(
|
||||
item: AllIdsOf<'items'>,
|
||||
x: number,
|
||||
y: number,
|
||||
floorId?: FloorIds,
|
||||
num?: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 获取某个物品的数量
|
||||
* @param item 物品id
|
||||
*/
|
||||
itemCount(item: AllIdsOf<'items'>): number;
|
||||
|
||||
/**
|
||||
* 判断勇士是否拥有这个物品
|
||||
* @param item 物品id
|
||||
*/
|
||||
hasItem(item: AllIdsOf<'items'>): boolean;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface IStateDamageable {
|
||||
/** 生命值 */
|
||||
hp: number;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { loading } from '../game';
|
||||
|
||||
type EffectFn = () => void;
|
||||
type CanUseEffectFn = () => boolean;
|
||||
|
||||
interface ItemStateEvent {
|
||||
use: [];
|
||||
}
|
||||
|
||||
export class ItemState<
|
||||
I extends AllIdsOf<'items'> = AllIdsOf<'items'>
|
||||
> extends EventEmitter<ItemStateEvent> {
|
||||
static items: Map<AllIdsOf<'items'>, ItemState> = new Map();
|
||||
|
||||
id: I;
|
||||
cls: ItemClsOf<I>;
|
||||
name: string;
|
||||
text?: string;
|
||||
hideInToolBox: boolean;
|
||||
hideInReplay: boolean;
|
||||
|
||||
/** 即捡即用效果 */
|
||||
itemEffect?: string;
|
||||
/** 即捡即用道具捡过之后的提示 */
|
||||
itemEffectTip?: string;
|
||||
/** 使用道具时执行的事件 */
|
||||
useItemEvent?: MotaEvent;
|
||||
/** 使用道具时执行的代码 */
|
||||
useItemEffect?: string;
|
||||
/** 能否使用道具 */
|
||||
canUseItemEffect?: string | boolean;
|
||||
|
||||
private noRoute: boolean = false;
|
||||
|
||||
itemEffectFn?: EffectFn;
|
||||
useItemEffectFn?: EffectFn;
|
||||
canUseItemEffectFn?: CanUseEffectFn;
|
||||
|
||||
constructor(id: I) {
|
||||
super();
|
||||
const items = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
|
||||
this.id = id;
|
||||
const item = items[id];
|
||||
this.cls = item.cls;
|
||||
this.name = item.name;
|
||||
this.text = item.text;
|
||||
this.hideInToolBox = item.hideInToolBox;
|
||||
this.hideInReplay = item.hideInReplay;
|
||||
this.itemEffect = item.itemEffect;
|
||||
this.itemEffectTip = item.itemEffectTip;
|
||||
this.useItemEvent = item.useItemEvent;
|
||||
this.useItemEffect = item.useItemEffect;
|
||||
this.canUseItemEffect = item.canUseItemEffect;
|
||||
|
||||
this.compileFunction();
|
||||
}
|
||||
|
||||
private compileFunction() {
|
||||
if (this.itemEffect) {
|
||||
this.itemEffectFn = new Function(
|
||||
`state`,
|
||||
this.itemEffect
|
||||
) as EffectFn;
|
||||
}
|
||||
if (this.useItemEffect) {
|
||||
this.useItemEffectFn = new Function(
|
||||
`state`,
|
||||
this.useItemEffect
|
||||
) as EffectFn;
|
||||
}
|
||||
if (this.canUseItemEffect) {
|
||||
if (typeof this.canUseItemEffect === 'boolean') {
|
||||
this.canUseItemEffectFn = () =>
|
||||
this.canUseItemEffect as boolean;
|
||||
} else {
|
||||
this.useItemEffectFn = new Function(
|
||||
`state`,
|
||||
this.canUseItemEffect
|
||||
) as CanUseEffectFn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个道具的信息
|
||||
* @param id 要获取的道具id
|
||||
*/
|
||||
static item<I extends AllIdsOf<'items'>>(id: I): ItemState<I> | undefined {
|
||||
return this.items.get(id) as ItemState<I>;
|
||||
}
|
||||
}
|
||||
|
||||
loading.once('coreInit', () => {
|
||||
for (const key of Object.keys(items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a)) {
|
||||
ItemState.items.set(
|
||||
key as AllIdsOf<'items'>,
|
||||
new ItemState(key as AllIdsOf<'items'>)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,959 +0,0 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { backDir, checkCanMoveExtended, toDir } from './utils';
|
||||
import { loading } from '../game';
|
||||
import type {
|
||||
RenderAdapter,
|
||||
HeroRenderer,
|
||||
FloorViewport,
|
||||
FloorLayer,
|
||||
Layer,
|
||||
LayerGroup,
|
||||
LayerMovingRenderable,
|
||||
LayerFloorBinder
|
||||
} from '@motajs/render';
|
||||
import type { HeroKeyMover } from '@/module/action/move';
|
||||
import { BluePalace, MiscData } from '../mechanism/misc';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const enum BlockMoveCode {
|
||||
Step
|
||||
}
|
||||
|
||||
export class BlockMover extends ObjectMoverBase {
|
||||
/** 楼层渲染适配器,用于显示动画 */
|
||||
static adapter?: RenderAdapter<Layer>;
|
||||
|
||||
x: number;
|
||||
y: number;
|
||||
floorId: FloorIds;
|
||||
layer: FloorLayer;
|
||||
|
||||
/** 本次移动中需要进行动画移动的楼层渲染组件 */
|
||||
private layerItems: Layer[] = [];
|
||||
/** 本次移动过程中的移动renderable实例 */
|
||||
private renderable?: LayerMovingRenderable;
|
||||
/** 本次移动的图块id */
|
||||
private blockNum: number = 0;
|
||||
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
layer: FloorLayer,
|
||||
dir: Dir = 'down'
|
||||
) {
|
||||
super();
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.moveDir = dir;
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定移动点
|
||||
* @param x 绑定点横坐标
|
||||
* @param y 绑定点纵坐标
|
||||
* @param floorId 绑定点楼层
|
||||
* @returns 是否绑定成功,例如如果当前绑定点正在移动,那么就会绑定失败
|
||||
*/
|
||||
bind(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
layer: FloorLayer,
|
||||
dir: Dir = 'down'
|
||||
) {
|
||||
if (this.moving) return false;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.moveDir = dir;
|
||||
this.layer = layer;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async onMoveStart(_controller: IMoveController): Promise<void> {
|
||||
const adapter = BlockMover.adapter;
|
||||
if (adapter) {
|
||||
const list = adapter.items;
|
||||
const items = [...list].filter(v => {
|
||||
if (v.layer !== this.layer) return false;
|
||||
const ex = v.getExtends('floor-binder') as LayerFloorBinder;
|
||||
if (!ex) return false;
|
||||
return ex.getFloor() === core.status.floorId;
|
||||
});
|
||||
this.layerItems = items;
|
||||
}
|
||||
|
||||
let blockNum: number = 0;
|
||||
if (this.layer === 'event') {
|
||||
blockNum = core.status.maps[this.floorId].map[this.y][this.x];
|
||||
} else {
|
||||
const array = core.maps._getBgFgMapArray(this.layer, this.floorId);
|
||||
blockNum = array[this.y][this.x];
|
||||
}
|
||||
this.blockNum = blockNum;
|
||||
|
||||
Mota.r(() => {
|
||||
const { Layer } = Mota.require('module', 'Render');
|
||||
const r = Layer.getMovingRenderable(blockNum, this.x, this.y);
|
||||
|
||||
if (r) {
|
||||
this.renderable = r;
|
||||
this.layerItems.forEach(v => {
|
||||
v.moving.add(r);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.layer === 'event') {
|
||||
core.removeBlock(this.x, this.y, this.floorId);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveEnd(_controller: IMoveController): Promise<void> {
|
||||
if (this.renderable) {
|
||||
this.layerItems.forEach(v => {
|
||||
v.moving.delete(this.renderable!);
|
||||
});
|
||||
}
|
||||
|
||||
this.layerItems = [];
|
||||
this.renderable = void 0;
|
||||
|
||||
if (this.layer === 'event') {
|
||||
core.setBlock(this.blockNum as AllNumbers, this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onStepStart(
|
||||
step: MoveStepDir,
|
||||
_controller: IMoveController
|
||||
): Promise<BlockMoveCode> {
|
||||
await this.moveAnimate(step);
|
||||
const { x: dx, y: dy } = core.utils.scan2[this.moveDir];
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
|
||||
return BlockMoveCode.Step;
|
||||
}
|
||||
|
||||
protected async onStepEnd(
|
||||
_step: MoveStepDir,
|
||||
_code: BlockMoveCode,
|
||||
_controller: IMoveController
|
||||
): Promise<void> {}
|
||||
|
||||
protected onSetMoveSpeed(
|
||||
_speed: number,
|
||||
_controller: IMoveController
|
||||
): void {}
|
||||
|
||||
private moveAnimate(_step: MoveStepDir) {
|
||||
const layer = this.layerItems[0];
|
||||
if (!layer) return;
|
||||
if (!this.renderable) return;
|
||||
const data = this.renderable;
|
||||
const fx = this.x;
|
||||
const fy = this.y;
|
||||
const { x: dx, y: dy } = core.utils.scan2[this.moveDir];
|
||||
const start = Date.now();
|
||||
const replay = core.status.replay.speed ?? 1;
|
||||
const time = replay === 24 ? 1 : this.moveSpeed / replay;
|
||||
|
||||
return new Promise<void>(res => {
|
||||
layer.delegateTicker(
|
||||
() => {
|
||||
const now = Date.now() - start;
|
||||
const progress = now / time;
|
||||
data.x = fx + dx * progress;
|
||||
data.y = fy + dy * progress;
|
||||
this.layerItems.forEach(v => {
|
||||
v.update(v);
|
||||
});
|
||||
},
|
||||
this.moveSpeed,
|
||||
() => {
|
||||
data.x = fx + dx;
|
||||
data.y = fy + dy;
|
||||
data.zIndex = fy + dy;
|
||||
res();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface CanMoveStatus {
|
||||
/** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */
|
||||
canMove: boolean;
|
||||
/** 由图块的noPass计算出的信息,不可移动时会触发触发器 */
|
||||
noPass: boolean;
|
||||
}
|
||||
|
||||
interface PortalStatus {
|
||||
/** 下一步是否会步入传送门 */
|
||||
portal: boolean;
|
||||
/** 传送门会传到哪 */
|
||||
data?: BluePalace.PortalTo;
|
||||
}
|
||||
|
||||
const enum HeroMoveCode {
|
||||
Step,
|
||||
Stop,
|
||||
/** 不能移动,并撞击前面一格的图块,触发其触发器 */
|
||||
Hit,
|
||||
/** 不能移动,同时当前格有CannotOut,或目标格有CannotIn,不会触发前面一格的触发器 */
|
||||
CannotMove,
|
||||
/** 进入传送门 */
|
||||
Portal,
|
||||
/** 循环式地图 */
|
||||
Loop,
|
||||
/** 循环式地图撞击 */
|
||||
LoopHit
|
||||
}
|
||||
|
||||
export class HeroMover extends ObjectMoverBase {
|
||||
/** 勇士渲染适配器,用于等待动画等操作 */
|
||||
static adapter?: RenderAdapter<HeroRenderer>;
|
||||
/** 视角适配器 */
|
||||
static viewport?: RenderAdapter<FloorViewport>;
|
||||
|
||||
/** 当前移动是否忽略地形 */
|
||||
private ignoreTerrain: boolean = false;
|
||||
/** 当前移动是否不计入录像 */
|
||||
private noRoute: boolean = false;
|
||||
/** 当前移动是否是在lockControl条件下开始的 */
|
||||
private inLockControl: boolean = false;
|
||||
/** 是否会在特殊时刻进行自动存档 */
|
||||
private autoSave: boolean = false;
|
||||
|
||||
/** 本次移动开始时的移动速度 */
|
||||
private beforeMoveSpeed: number = 100;
|
||||
|
||||
/** 这一步的传送门信息 */
|
||||
private portalData?: BluePalace.PortalTo;
|
||||
|
||||
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 (nextDam?.mockery || (!dam?.hunt && nextDam?.hunt)) {
|
||||
core.autosave();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveStart(controller: IMoveController): Promise<void> {
|
||||
this.beforeMoveSpeed = this.moveSpeed;
|
||||
const adapter = HeroMover.adapter;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!adapter || !viewport) return;
|
||||
if (!core.isReplaying() || core.status.replay.speed <= 3) {
|
||||
adapter.sync('startAnimate');
|
||||
await adapter.all('readyMove');
|
||||
}
|
||||
// 这里要检查前面那一格能不能走,不能走则不触发平滑视角,以避免撞墙上视角卡住
|
||||
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);
|
||||
const adapter = HeroMover.adapter;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!adapter || !viewport) return;
|
||||
adapter.sync('endAnimate');
|
||||
await adapter.all('endMove');
|
||||
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;
|
||||
}
|
||||
|
||||
// 检查传送门
|
||||
if (!this.ignoreTerrain) {
|
||||
const { portal, data } = this.checkPortal(x, y, dir4Move);
|
||||
if (portal && data) {
|
||||
this.portalData = data;
|
||||
await this.renderHeroSwap(data);
|
||||
return HeroMoveCode.Portal;
|
||||
}
|
||||
}
|
||||
|
||||
const dir = this.moveDir;
|
||||
if (!this.ignoreTerrain) {
|
||||
const { noPass, canMove } = this.checkCanMove(x, y, dir4Move);
|
||||
|
||||
if (!canMove) {
|
||||
return HeroMoveCode.CannotMove;
|
||||
}
|
||||
// 循环式地图
|
||||
const floorId = core.status.floorId;
|
||||
if (MiscData.loopMaps.has(core.status.floorId)) {
|
||||
const floor = core.status.maps[floorId];
|
||||
const width = floor.width;
|
||||
if (
|
||||
(x === 0 && dir === 'left') ||
|
||||
(x === width - 1 && dir === 'right')
|
||||
) {
|
||||
if (noPass) {
|
||||
return HeroMoveCode.LoopHit;
|
||||
}
|
||||
await Promise.all([
|
||||
this.renderHeroLoop(),
|
||||
this.moveAnimate(nx, ny, showDir, dir)
|
||||
]);
|
||||
return HeroMoveCode.Loop;
|
||||
}
|
||||
}
|
||||
// 不能移动
|
||||
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 ||
|
||||
code === HeroMoveCode.LoopHit
|
||||
) {
|
||||
controller.stop();
|
||||
this.onCannotMove(showDir);
|
||||
if (code === HeroMoveCode.Hit) {
|
||||
core.trigger(nx, ny);
|
||||
} else if (code === HeroMoveCode.LoopHit) {
|
||||
const floor = core.status.thisMap;
|
||||
if (x === 0) core.trigger(floor.width - 1, y);
|
||||
else core.trigger(0, y);
|
||||
}
|
||||
core.checkRouteFolding();
|
||||
return;
|
||||
}
|
||||
|
||||
// 本次移动停止
|
||||
if (code === HeroMoveCode.Stop) {
|
||||
core.clearContinueAutomaticRoute();
|
||||
core.stopAutomaticRoute();
|
||||
controller.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// 本次移动正常完成
|
||||
if (
|
||||
code === HeroMoveCode.Step ||
|
||||
code === HeroMoveCode.Portal ||
|
||||
code === HeroMoveCode.Loop
|
||||
) {
|
||||
if (code === HeroMoveCode.Portal) {
|
||||
const data = this.portalData;
|
||||
if (!data) return;
|
||||
core.setHeroLoc('x', data.x);
|
||||
core.setHeroLoc('y', data.y);
|
||||
core.setHeroLoc('direction', data.dir);
|
||||
} else if (code === HeroMoveCode.Loop) {
|
||||
const map = core.status.thisMap;
|
||||
if (x === 0) core.setHeroLoc('x', map.width - 1);
|
||||
else core.setHeroLoc('x', 0);
|
||||
} else {
|
||||
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);
|
||||
|
||||
core.moveOneStep();
|
||||
core.checkRouteFolding();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected onSetMoveSpeed(
|
||||
speed: number,
|
||||
_controller: IMoveController
|
||||
): void {
|
||||
const adapter = HeroMover.adapter;
|
||||
if (!adapter) return;
|
||||
adapter.sync('setMoveSpeed', speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动动画
|
||||
* @param x 目标横坐标
|
||||
* @param y 目标纵坐标
|
||||
* @param showDir 显示方向
|
||||
* @param moveDir 移动方向
|
||||
*/
|
||||
private async moveAnimate(
|
||||
x: number,
|
||||
y: number,
|
||||
showDir: Dir,
|
||||
moveDir: Dir2
|
||||
) {
|
||||
const adapter = HeroMover.adapter;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!adapter || !viewport) return;
|
||||
const replay = core.status.replay.speed;
|
||||
const speed = replay === 24 ? 1 : this.moveSpeed / replay;
|
||||
viewport.all('moveTo', x, y, speed * 1.6);
|
||||
adapter.sync('setAnimateDir', showDir);
|
||||
if (core.isReplaying() && core.status.replay.speed > 3) {
|
||||
adapter.sync('endAnimate');
|
||||
await sleep(speed);
|
||||
await adapter.all('setHeroLoc', x, y);
|
||||
} else {
|
||||
if (core.isReplaying()) {
|
||||
adapter.sync('startAnimate');
|
||||
}
|
||||
await adapter.all('move', moveDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前方一格不能走时,执行的函数
|
||||
* @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 floorId = core.status.floorId;
|
||||
if (MiscData.loopMaps.has(floorId)) {
|
||||
const floor = core.status.maps[floorId];
|
||||
const width = floor.width;
|
||||
if (x === 0 && dir === 'left') {
|
||||
const noPass = core.noPass(width - 1, y);
|
||||
const move = checkCanMoveExtended(0, y, width - 1, y, 'left');
|
||||
return { noPass, canMove: move };
|
||||
} else if (x === width - 1 && dir === 'right') {
|
||||
const noPass = core.noPass(0, y);
|
||||
const move = checkCanMoveExtended(width - 1, y, 0, y, 'right');
|
||||
return { noPass, canMove: move };
|
||||
}
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查前方一格是否会步入传送门
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
* @param dir 移动方向
|
||||
*/
|
||||
private checkPortal(x: number, y: number, dir: Dir): PortalStatus {
|
||||
const map = BluePalace.portalMap.get(core.status.floorId);
|
||||
if (!map) {
|
||||
return { portal: false };
|
||||
}
|
||||
const width = core.status.thisMap.width;
|
||||
const index = x + y * width;
|
||||
const data = map?.get(index);
|
||||
if (!data) {
|
||||
return { portal: false };
|
||||
}
|
||||
const to = data[dir];
|
||||
if (to) {
|
||||
return { portal: true, data: to };
|
||||
}
|
||||
return { portal: false };
|
||||
}
|
||||
|
||||
private renderHeroSwap(data: BluePalace.PortalTo) {
|
||||
const adapter = HeroMover.adapter;
|
||||
if (!adapter) return;
|
||||
const list = adapter.items;
|
||||
const { x: tx, y: ty, dir: toDir } = data;
|
||||
const { x, y, direction } = core.status.hero.loc;
|
||||
const { x: dx } = core.utils.scan[direction];
|
||||
const { x: tdx } = core.utils.scan[toDir];
|
||||
|
||||
const promises = [...list].map(v => {
|
||||
if (!v.renderable) return;
|
||||
const renderable = { ...v.renderable };
|
||||
renderable.render = v.getRenderFromDir(toDir);
|
||||
renderable.zIndex = ty;
|
||||
const heroDir = v.moveDir;
|
||||
|
||||
const width = v.renderable.render[0][2];
|
||||
const height = v.renderable.render[0][3];
|
||||
const cell = v.layer.cellSize;
|
||||
const restHeight = height - cell;
|
||||
if (!width || !height) return;
|
||||
|
||||
const originFrom = structuredClone(v.renderable.render);
|
||||
const originTo = structuredClone(renderable.render);
|
||||
v.layer.moving.add(renderable);
|
||||
v.layer.requestUpdateMoving();
|
||||
|
||||
const start = Date.now();
|
||||
return new Promise<void>(res => {
|
||||
const tick = () => {
|
||||
const now = Date.now();
|
||||
const progress = (now - start) / this.moveSpeed;
|
||||
const clipWidth = cell * progress;
|
||||
const clipHeight = cell * progress;
|
||||
const beforeWidth = width - clipWidth;
|
||||
const beforeHeight = height - clipHeight;
|
||||
|
||||
v.renderable!.x = x;
|
||||
v.renderable!.y = y;
|
||||
if (heroDir === 'left' || heroDir === 'right') {
|
||||
v.renderable!.x = x + (clipWidth / 2 / cell) * dx;
|
||||
v.renderable!.render.forEach((v, i) => {
|
||||
v[2] = beforeWidth;
|
||||
if (heroDir === 'left') {
|
||||
v[0] = originFrom[i][0] + clipWidth;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
v.renderable!.render.forEach((v, i) => {
|
||||
v[3] = beforeHeight;
|
||||
if (heroDir === 'up') {
|
||||
v[1] =
|
||||
originFrom[i][1] + clipHeight + restHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderable.x = tx;
|
||||
renderable.y = ty;
|
||||
if (toDir === 'left' || toDir === 'right') {
|
||||
renderable.x = tx + (clipWidth / 2 / cell - 0.5) * tdx;
|
||||
renderable.render.forEach((v, i) => {
|
||||
v[2] = clipWidth;
|
||||
if (toDir === 'right') {
|
||||
v[0] = originTo[i][0] + beforeWidth;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (toDir === 'down') renderable.y = ty - 1 + progress;
|
||||
renderable.render.forEach((v, i) => {
|
||||
v[3] = clipHeight + restHeight;
|
||||
if (toDir === 'down') {
|
||||
v[1] = originTo[i][1] + clipHeight + restHeight;
|
||||
v[3] = clipHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
v.layer.delegateTicker(tick, this.moveSpeed, () => {
|
||||
v.renderable!.render = originFrom;
|
||||
v.setAnimateDir(data.dir);
|
||||
v.layer.moving.delete(renderable);
|
||||
v.layer.requestUpdateMoving();
|
||||
res();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
private renderHeroLoop() {
|
||||
const adapter = HeroMover.adapter;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!adapter || !viewport) return;
|
||||
const MotaRenderer = Mota.require('module', 'Render').MotaRenderer;
|
||||
const render = MotaRenderer.get('render-main');
|
||||
const group = render?.getElementById('layer-loop') as LayerGroup;
|
||||
const layer = group?.getLayer('event');
|
||||
const mainGroup = render?.getElementById('layer-main') as LayerGroup;
|
||||
const mainLayer = mainGroup?.getLayer('event');
|
||||
const hero = mainLayer?.getExtends('floor-hero') as HeroRenderer;
|
||||
const renderable = hero?.renderable;
|
||||
if (!layer || !hero || !renderable) return;
|
||||
const { x, y } = core.status.hero.loc;
|
||||
const width = core.status.thisMap.width;
|
||||
const loopHero = { ...renderable };
|
||||
layer.moving.add(loopHero);
|
||||
|
||||
let target: number;
|
||||
let from: number;
|
||||
if (x === 0) {
|
||||
from = width;
|
||||
target = width - 1;
|
||||
} else {
|
||||
from = -1;
|
||||
target = 0;
|
||||
}
|
||||
const delta = target - from;
|
||||
loopHero.x = from;
|
||||
|
||||
layer.requestUpdateMoving();
|
||||
|
||||
const startTime = Date.now();
|
||||
return new Promise<void>(res => {
|
||||
layer.delegateTicker(
|
||||
() => {
|
||||
const progress = (Date.now() - startTime) / this.moveSpeed;
|
||||
const dx = delta * progress;
|
||||
loopHero.x = dx + from;
|
||||
layer.update(layer);
|
||||
},
|
||||
this.moveSpeed,
|
||||
() => {
|
||||
layer.moving.delete(loopHero);
|
||||
layer.requestUpdateMoving();
|
||||
viewport.all('setPosition', x === 0 ? width - 1 : 0, y);
|
||||
res();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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('module', 'Action');
|
||||
const gameKey = Mota.require('var', 'gameKey');
|
||||
const keyMover = new HeroKeyMover(gameKey, heroMover);
|
||||
heroMoveCollection.keyMover = keyMover;
|
||||
});
|
||||
});
|
||||
|
||||
// Adapter初始化
|
||||
loading.once('coreInit', () => {
|
||||
if (main.replayChecking || main.mode === 'editor') return;
|
||||
const Adapter = Mota.require('module', 'Render').RenderAdapter;
|
||||
const adapter = Adapter.get<HeroRenderer>('hero-adapter');
|
||||
const viewport = Adapter.get<FloorViewport>('viewport');
|
||||
const layerAdapter = Adapter.get<Layer>('layer');
|
||||
HeroMover.adapter = adapter;
|
||||
HeroMover.viewport = viewport;
|
||||
BlockMover.adapter = layerAdapter;
|
||||
});
|
||||
@@ -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]
|
||||
];
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import type * as Client from '@motajs/client';
|
||||
import type * as ClientBase from '@motajs/client-base';
|
||||
import type * as Common from '@motajs/common';
|
||||
import type * as LegacyClient from '@motajs/legacy-client';
|
||||
import type * as LegacyCommon from '@motajs/legacy-common';
|
||||
import type * as LegacySystem from '@motajs/legacy-system';
|
||||
import type * as LegacyUI from '@motajs/legacy-ui';
|
||||
import type * as Render from '@motajs/render';
|
||||
import type * as RenderCore from '@motajs/render-core';
|
||||
import type * as RenderElements from '@motajs/render-elements';
|
||||
import type * as RenderStyle from '@motajs/render-style';
|
||||
import type * as RenderVue from '@motajs/render-vue';
|
||||
import type * as System from '@motajs/system';
|
||||
import type * as SystemAction from '@motajs/system-action';
|
||||
import type * as SystemUI from '@motajs/system-ui';
|
||||
|
||||
interface ModuleInterface {
|
||||
'@motajs/client': typeof Client;
|
||||
'@motajs/client-base': typeof ClientBase;
|
||||
'@motajs/common': typeof Common;
|
||||
'@motajs/legacy-client': typeof LegacyClient;
|
||||
'@motajs/legacy-common': typeof LegacyCommon;
|
||||
'@motajs/legacy-system': typeof LegacySystem;
|
||||
'@motajs/legacy-ui': typeof LegacyUI;
|
||||
'@motajs/render': typeof Render;
|
||||
'@motajs/render-core': typeof RenderCore;
|
||||
'@motajs/render-elements': typeof RenderElements;
|
||||
'@motajs/render-style': typeof RenderStyle;
|
||||
'@motajs/render-vue': typeof RenderVue;
|
||||
'@motajs/system': typeof System;
|
||||
'@motajs/system-action': typeof SystemAction;
|
||||
'@motajs/system-ui': typeof SystemUI;
|
||||
}
|
||||
|
||||
export interface IMota {
|
||||
r: typeof r;
|
||||
rf: typeof rf;
|
||||
|
||||
/**
|
||||
* 获取一个样板接口
|
||||
* @param key 接口名称
|
||||
*/
|
||||
require<K extends keyof ModuleInterface>(key: K): ModuleInterface[K];
|
||||
/**
|
||||
* 获取一个样板接口
|
||||
* @param key 接口名称
|
||||
*/
|
||||
require<T = unknown>(key: string): T;
|
||||
|
||||
/**
|
||||
* 注册一个样板接口
|
||||
* @param key 接口名称
|
||||
* @param data 接口内容
|
||||
*/
|
||||
register<K extends keyof ModuleInterface>(
|
||||
key: K,
|
||||
data: ModuleInterface[K]
|
||||
): void;
|
||||
/**
|
||||
* 注册一个样板接口
|
||||
* @param key 接口名称
|
||||
* @param data 接口内容
|
||||
*/
|
||||
register(key: string, data: unknown): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 样板接口系统,通过 Mota 获取到样板的核心功能
|
||||
*/
|
||||
class Mota implements IMota {
|
||||
private modules: Record<string, any> = {};
|
||||
|
||||
r = r;
|
||||
rf = rf;
|
||||
|
||||
constructor() {
|
||||
throw new Error(`System interface class cannot be constructed.`);
|
||||
}
|
||||
|
||||
require(key: string): any {
|
||||
const data = this.modules[key];
|
||||
if (data) return data;
|
||||
else {
|
||||
throw new Error(`Cannot resolve module '${key}'`);
|
||||
}
|
||||
}
|
||||
|
||||
register(key: string, data: any) {
|
||||
if (key in this.modules) {
|
||||
console.warn(`模块注册重复: '${key}',已将其覆盖`);
|
||||
}
|
||||
this.modules[key] = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在渲染进程包裹下执行一段代码,该段代码不会在录像验证中执行,因此里面的内容一定不会引起录像报错
|
||||
* 一般特效,或者是ui显示、内容显示、交互监听等内容应当在渲染进程包裹下执行。
|
||||
* 无法获取到执行内容的返回值,因为渲染进程中的值不应当直接出现在游戏进程中,否则很可能导致录像出错,
|
||||
* 如果需要其返回值,应当直接在函数后面新增内容,而不是在游戏进程中使用
|
||||
* @param fn 要执行的函数,传入一个参数,表示所有的第三方库,也就是`Mota.Package.requireAll()`的内容
|
||||
* @param thisArg 函数的执行上下文,即函数中`this`指向
|
||||
*/
|
||||
function r<T = undefined>(fn: (this: T) => void, thisArg?: T) {
|
||||
if (!main.replayChecking && main.mode === 'play') fn.call(thisArg as T);
|
||||
}
|
||||
|
||||
const empty = () => {};
|
||||
|
||||
/**
|
||||
* 将一个函数包裹成渲染进程函数,执行这个函数时将直接在渲染进程下执行。该函数与 {@link r} 函数的关系,
|
||||
* 与`call`和`bind`的关系类似。
|
||||
* ```ts
|
||||
* const fn = rf((x) => x * x);
|
||||
* console.log(fn(2)); // 在正常游玩中会输出 4,但是录像验证中会输出undefined,因为录像验证中不会执行渲染进程函数
|
||||
* ```
|
||||
* @param fn 要执行的函数
|
||||
* @param thisArg 函数执行时的上下文,即this指向
|
||||
* @returns 经过渲染进程包裹的函数,直接调用即是在渲染进程下执行的
|
||||
*/
|
||||
function rf<F extends (...params: any) => any, T>(
|
||||
fn: F,
|
||||
thisArg?: T
|
||||
): (this: T, ...params: Parameters<F>) => ReturnType<F> | undefined {
|
||||
// @ts-expect-error 录像验证的时候不能执行任何操作,因此返回空函数
|
||||
if (main.replayChecking || main.mode === 'editor') return empty;
|
||||
else {
|
||||
return (...params) => {
|
||||
return fn.call(thisArg, ...params);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Mota: IMota;
|
||||
}
|
||||
}
|
||||
|
||||
window.Mota = new Mota();
|
||||
@@ -1,81 +0,0 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
interface RangeTypeData {
|
||||
square: { x: number; y: number; d: number };
|
||||
rect: { x: number; y: number; w: number; h: number };
|
||||
}
|
||||
|
||||
type InRangeFn<E extends Partial<Loc>, T> = (item: E, data: T) => boolean;
|
||||
|
||||
export class Range {
|
||||
static rangeType: Record<string, RangeType> = {};
|
||||
|
||||
/**
|
||||
* 获取一个范围类型,并进行判断
|
||||
* @param type 范围类型
|
||||
*/
|
||||
type<T extends string>(
|
||||
type: T
|
||||
): T extends keyof RangeTypeData ? RangeType<RangeTypeData[T]> : RangeType {
|
||||
return Range.rangeType[type] as T extends keyof RangeTypeData
|
||||
? RangeType<RangeTypeData[T]>
|
||||
: RangeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个新的范围类型
|
||||
* @param type 范围类型
|
||||
* @param fn 判断是否在范围内的函数
|
||||
*/
|
||||
static register<K extends keyof RangeTypeData>(
|
||||
type: K,
|
||||
fn: InRangeFn<Partial<Loc>, RangeTypeData[K]>
|
||||
): void;
|
||||
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void;
|
||||
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void {
|
||||
const range = new RangeType(type, fn);
|
||||
this.rangeType[type] = range;
|
||||
}
|
||||
}
|
||||
|
||||
class RangeType<Type = any> {
|
||||
readonly type: string;
|
||||
/**
|
||||
* 判断一个元素是否在指定范围内
|
||||
* @param item 要判断的元素
|
||||
* @param data 范围数据
|
||||
*/
|
||||
readonly inRange: InRangeFn<Partial<Loc>, Type>;
|
||||
|
||||
constructor(type: string, fn: InRangeFn<Partial<Loc>, Type>) {
|
||||
this.type = type;
|
||||
this.inRange = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描一个列表中所有在范围内的元素
|
||||
* @param items 元素列表
|
||||
* @param data 范围数据
|
||||
*/
|
||||
scan<T extends Partial<Loc>>(items: Iterable<T>, data: Type): T[] {
|
||||
const res: T[] = [];
|
||||
for (const ele of items) {
|
||||
if (this.inRange(ele, data)) {
|
||||
res.push(ele);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
Range.register('square', (item, { x, y, d }) => {
|
||||
if (isNil(item.x) || isNil(item.y)) return false;
|
||||
const r = Math.floor(d / 2);
|
||||
return Math.abs(item.x - x) <= r && Math.abs(item.y - y) <= r;
|
||||
});
|
||||
Range.register('rect', (item, { x, y, w, h }) => {
|
||||
if (isNil(item.x) || isNil(item.y)) return false;
|
||||
const ex = x + w;
|
||||
const ey = y + h;
|
||||
return item.x >= x && item.y >= y && item.x < ex && item.y < ey;
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { Hotkey, HotkeyData } from '@motajs/system-action';
|
||||
import type { HeroMover, IMoveController } from '@/game/state/move';
|
||||
import type { HeroMover, IMoveController } from '@user/data-state';
|
||||
import { Ticker } from 'mutate-animate';
|
||||
import { mainScope } from '@motajs/legacy-ui';
|
||||
|
||||
|
||||
Reference in new issue
Block a user