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, 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): 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 }; }