You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/battle/bridge/CocosBattleMovementBridge.ts

367 lines
13 KiB

import { Vec3, v3 } from 'cc';
import type { IBattleBridge, BattleSession, EntityId } from 'db://assets/mx/module/battle';
import { isBajiaoBlowRetreating, MovementComp, TagComp, TransformComp } from 'db://assets/mx/module/battle';
import type { CocosBattleMonsterPresenter } from '../view/CocosBattleMonsterPresenter';
import { BATTLE_MONSTER_CCCLASS, type IBattleMonsterView } from '../view/IBattleMonsterView';
import { MonsterState } from '../../ui/BattleGame/MonsterState';
import type { MapManager } from '../map/legacy/MapManager';
import { applyMonsterFootOffset } from '../map/BattleMapSpec';
import { MECHANISM_UNIT_TAG } from '../mechanism/MechanismConstants';
export { applyMonsterFootOffset, MONSTER_VISUAL_OFFSET_Y_CELLS } from '../map/BattleMapSpec';
const _syncWorldPos = new Vec3();
const _syncFootPos = new Vec3();
/** 怪物腰点世界坐标(与 Monster.getPosByAtkPosIndex(2) 一致) */
export function getMonsterWaistWorldPos(node: import('cc').Node): Vec3 {
const comp = node.getComponent(BATTLE_MONSTER_CCCLASS) as {
getPosByAtkPosIndex?: (idx: number) => Vec3;
} | null;
if (comp?.getPosByAtkPosIndex) {
return comp.getPosByAtkPosIndex(2);
}
return node.worldPosition.clone();
}
/**
* 牢笼/爪子锚点世界坐标 → 怪物根节点 worldPosition。
* 地图上怪物节点在脚底偏移后落位;牢笼锚点应对齐腰点而非脚底。
*/
export function monsterNodePosForCageAnchor(
cageAnchorWorld: Readonly<Vec3>,
monsterNode: import('cc').Node,
): Vec3 {
const waist = getMonsterWaistWorldPos(monsterNode);
const p = monsterNode.worldPosition;
return new Vec3(
cageAnchorWorld.x - (waist.x - p.x),
cageAnchorWorld.y - (waist.y - p.y),
cageAnchorWorld.z - (waist.z - p.z),
);
}
/**
* 战斗表现桥接:MX 逻辑坐标 → Cocos 怪物节点
*/
export class CocosBattleMovementBridge implements IBattleBridge {
constructor(private readonly _presenter: CocosBattleMonsterPresenter) {}
onCombatTick(session: BattleSession, _dt: number): void {
const map = this._presenter.battle?.Map;
if (!map?.gridsInitialized) return;
for (const [entityId, node] of this._presenter.getEntityNodeMap()) {
if (!node?.isValid) continue;
// 占用表现已死:等 releaseVisual 回收,勿再同步/强行显形
if (this._presenter.isPendingRecycle(entityId)) continue;
// 出怪跳跃中由位移动画接管坐标,只同步战斗状态
if (this._presenter.isSpawning(entityId)) {
syncMonsterCombatState(session, entityId, node);
continue;
}
this._presenter.ensureMarchVisible(entityId, node);
syncMonsterNode(session, map, entityId, node);
}
this._presenter.tickMonsterYSort();
this._presenter.tickSpineLod();
}
}
/**
* 将 MX MovementComp 同步到 Cocos 节点(怪物、技能子弹等通用)
*/
export function syncMovementNode(
session: BattleSession,
map: MapManager,
entityId: EntityId,
node: import('cc').Node,
): void {
const move = session.world.getComponent(entityId, MovementComp);
const tf = session.world.getComponent(entityId, TransformComp);
if (!move || !tf) return;
if (!resolveEntityWorldPosInto(session, map, tf.row, tf.col, move, _syncWorldPos)) {
console.error('[CocosBattleMovementBridge] syncMovementNode 坐标解析失败,保持原位', entityId, tf.row, tf.col);
return;
}
node.setWorldPosition(_syncWorldPos);
}
/**
* 将 MX 实体移动状态同步到怪物节点(含攻击状态)
*/
export function syncMonsterNode(
session: BattleSession,
map: MapManager,
entityId: EntityId,
node: import('cc').Node,
): void {
const move = session.world.getComponent(entityId, MovementComp);
const tf = session.world.getComponent(entityId, TransformComp);
// 阎王拘魂:牢笼内直接落怪物节点锚点(已按腰点对齐牢笼),不做格心脚底偏移
if (move?.imprisoned && tf) {
node.setWorldPosition(move.imprisonCocosAnchorX, move.imprisonCocosAnchorY, 0);
syncMonsterCombatState(session, entityId, node);
return;
}
// 阎王拖入:表现层 tween 驱动坐标,逻辑层已 presentLocked
if (move?.imprisonBy != null && !move.imprisoned) {
syncMonsterCombatState(session, entityId, node);
return;
}
// 机制降落 / 出怪前:逻辑坐标已落格,表现由 tween 接管。
// 门神盾牌顶推:施法锁期间仍须跟逻辑坐标走。
const shieldPush = isBajiaoBlowRetreating(move) || !!move?.forcePresentSync;
if (move?.presentLocked && !shieldPush) {
syncMonsterCombatState(session, entityId, node);
return;
}
// 已停步开战:默认保持表现坐标原地攻击(避免与格间 lerp 不一致导致后退);
// 芭蕉扇吹退 / 击退等权威改写逻辑坐标时须同步表现。
const bajiaoRetreat = (move?.forcePathSpeed ?? 0) > 0 || move?.bajiaoBlowRetreating;
if (move?.reachedGoal && !move.forcePresentSync && !bajiaoRetreat) {
syncMonsterCombatState(session, entityId, node);
return;
}
if (move) move.forcePresentSync = false;
if (!move || !tf) {
syncMonsterCombatState(session, entityId, node);
return;
}
if (!resolveEntityWorldPosInto(session, map, tf.row, tf.col, move, _syncWorldPos)) {
console.error(
'[CocosBattleMovementBridge] syncMonsterNode 坐标解析失败,保持原位',
entityId,
tf.row,
tf.col,
move.worldX,
move.worldY,
);
syncMonsterCombatState(session, entityId, node);
return;
}
applyMonsterFootOffset(_syncWorldPos, _syncFootPos, map.mapGridSize);
if (!setMonsterNodeWorldPos(node, _syncFootPos)) {
syncMonsterCombatState(session, entityId, node);
return;
}
syncMonsterCombatState(session, entityId, node);
}
function setMonsterNodeWorldPos(node: import('cc').Node, pos: Readonly<Vec3>): boolean {
if (!node?.isValid) return false;
if (!Number.isFinite(pos.x) || !Number.isFinite(pos.y)) {
console.log('[CocosBattleMovementBridge] skip invalid monster world pos', pos.x, pos.y);
return false;
}
node.setWorldPosition(pos);
return true;
}
/** 白骨精等 mechanism_unit:不可攻唐僧;空投飞禽无此标签,可攻 */
function isWallAttackForbidden(
session: BattleSession,
entityId: EntityId,
comp: { mechanismImmobile?: boolean } | null,
): boolean {
if (comp?.mechanismImmobile) return true;
const tags = session.world.getComponent(entityId, TagComp);
return tags?.tags.includes(MECHANISM_UNIT_TAG) ?? false;
}
/** 将 MX 移动/攻击状态同步到怪物 Cocos 视图 */
export function syncMonsterCombatState(
session: BattleSession,
entityId: EntityId,
node: import('cc').Node,
): void {
const move = session.world.getComponent(entityId, MovementComp);
if (!move) return;
const comp = node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView & {
useMxMovement?: boolean;
state?: MonsterState;
resolveAttackTarget?: () => void;
mechanismImmobile?: boolean;
} | null;
if (!comp) return;
comp.useMxMovement = true;
if (move.presentLocked || move.spawnProtected) {
if (comp.state !== MonsterState.STOP) {
comp.state = MonsterState.STOP;
}
return;
}
// 策划:除空投飞禽外,机制怪不可进入攻墙态(白骨精 reachedGoal=true 易被误判开战)
if (isWallAttackForbidden(session, entityId, comp)) {
if (comp.state === MonsterState.ATTACKING) {
comp.state = MonsterState.STOP;
}
return;
}
if (move.reachedGoal) {
if (comp.state !== MonsterState.ATTACKING) {
comp.state = MonsterState.ATTACKING;
comp.resolveAttackTarget?.();
}
} else {
comp.state = MonsterState.MOVING_FORWARD;
}
}
/**
* 出怪跳出起/落点:沿出怪格→下一寻路格 Cocos 格心 lerp,再套脚底 Y 偏移(与 syncMonsterNode 一致)。
* @param t 0=出怪格,1=下一寻路格
*/
export function resolveSpawnJumpVisualPosInto(
map: MapManager,
spawnRow: number,
spawnCol: number,
nextRow: number,
nextCol: number,
t: number,
out: Vec3,
): boolean {
const g0 = map.getMapGrid(spawnRow, spawnCol);
const g1 = map.getMapGrid(nextRow, nextCol);
if (!g0?.node?.isValid || !g1?.node?.isValid) {
const fallback = g0?.node?.isValid ? g0.node.worldPosition : g1?.node?.worldPosition;
if (!fallback) return false;
applyMonsterFootOffset(fallback, out, map.mapGridSize);
return false;
}
Vec3.lerp(out, g0.node.worldPosition, g1.node.worldPosition, t);
applyMonsterFootOffset(out, out, map.mapGridSize);
return true;
}
/**
* 解析实体表现世界坐标。
* @returns 成功写入 out 时为 true;格节点缺失时为 false(不得写 0,0)
*/
function resolveEntityWorldPosInto(
session: BattleSession,
map: MapManager,
row: number,
col: number,
movement: MovementComp,
out: Vec3,
): boolean {
// 已停步开战(path 已清空)或走完路径:用逻辑 world 映射到 Cocos,禁止 (x,-y) 或强行吸回格心
if (movement.reachedGoal || movement.pathIndex >= movement.path.length) {
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const curGrid = map.getMapGrid(row, col);
if (!curGrid?.node?.isValid) {
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const curCenter = session.grid.cellToWorld({ row, col });
const next = movement.path[movement.pathIndex];
const nextGrid = map.getMapGrid(next.row, next.col);
if (!nextGrid?.node?.isValid) {
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const nextCenter = session.grid.cellToWorld(next);
const dx = nextCenter.x - curCenter.x;
const dy = nextCenter.y - curCenter.y;
const total = Math.sqrt(dx * dx + dy * dy);
const px = movement.worldX - curCenter.x;
const py = movement.worldY - curCenter.y;
const along = total > 1e-4 ? (px * dx + py * dy) / total : 0;
const perp = total > 1e-4
? Math.abs(px * dy - py * dx) / total
: Math.sqrt(px * px + py * py);
// 击退等会把逻辑坐标打出路径线段;再按路程投影会把后退/侧移折成前进
if (along < -1 || along > total + 1 || perp > 4) {
return logicalWorldToCocosInto(session, map, row, col, movement.worldX, movement.worldY, out);
}
const t = total > 1e-4 ? Math.min(1, Math.max(0, along / total)) : 1;
Vec3.lerp(out, curGrid.node.worldPosition, nextGrid.node.worldPosition, t);
return true;
}
/**
* 逻辑世界坐标 → Cocos 世界坐标(写入 out)。
* @returns 地图格可用时 true;缺失时 false 且不改 out(禁止写成屏幕原点)
*/
function logicalWorldToCocosInto(
session: BattleSession,
map: MapManager,
row: number,
col: number,
logicalX: number,
logicalY: number,
out: Vec3,
): boolean {
const logicalCenter = session.grid.cellToWorld({ row, col });
const ox = logicalX - logicalCenter.x;
const oy = logicalY - logicalCenter.y;
const curGrid = map.getMapGrid(row, col);
if (curGrid?.node?.isValid) {
const cw = curGrid.node.worldPosition;
out.set(cw.x + ox, cw.y - oy, 0);
return true;
}
console.error(
'[CocosBattleMovementBridge] map grid missing for visual sync',
row,
col,
'map=',
map.mapRows,
'x',
map.mapCols,
'logic=',
logicalX,
logicalY,
);
return false;
}
/**
* 逻辑世界坐标(GridMap,Y 向下)→ Cocos 世界坐标(Y 向上)。
* 以当前格心为锚做局部偏移;格节点缺失返回 null(禁止落到屏幕原点)。
*/
export function logicalWorldToCocos(
session: BattleSession,
map: MapManager,
row: number,
col: number,
logicalX: number,
logicalY: number,
): Vec3 | null {
const out = v3();
if (!logicalWorldToCocosInto(session, map, row, col, logicalX, logicalY, out)) {
return null;
}
return out;
}
/** Cocos 世界坐标 → 逻辑世界坐标(与 logicalWorldToCocos 互逆) */
export function cocosWorldToLogical(
session: BattleSession,
map: MapManager,
row: number,
col: number,
cocosX: number,
cocosY: number,
): { x: number; y: number } {
const logicalCenter = session.grid.cellToWorld({ row, col });
const curGrid = map.getMapGrid(row, col);
if (curGrid?.node?.isValid) {
const cw = curGrid.node.worldPosition;
return {
x: logicalCenter.x + (cocosX - cw.x),
y: logicalCenter.y + (cw.y - cocosY),
};
}
return { x: cocosX, y: -cocosY };
}