import { _decorator, Collider2D, Component, Mat4, Node, Vec3, isValid } from 'cc'; import { tetriNode } from './tetriNode'; import { ROLE_HAND_LAYER } from './roleHand'; const { ccclass, property } = _decorator; /** * 战斗中方块落稳后:角色挂到 ui_roleLayer,在 lateUpdate 跟随方块根节点(followTarget)的世界位置与旋转 * (物理步之后采样,避免 update 落后一帧导致角色与方块脱节)。 * 角色为动态创建时请调用 {@link setFollowTarget};也可依赖 onLoad 时父链上的 tetriNode 自动推断。 * 方块 `destroy` 后 followTarget 会 `!isValid`,此时销毁本节点。 * * 渲染层:预制体若设为 battleScene 而父节点在 card(UI_2D)或 ui_roleLayer(UI_2D),与相机 visibility 不一致会偶现看不见; * 落稳前与本节点父级对齐 layer,挂到 ui_roleLayer 后与该层对齐(含子节点递归)。 * 分组:挂到 ui_roleLayer 时同步 Node.group 与 Collider2D.group(与参考层一致),避免渲染/物理分组不一致。 */ @ccclass('role') export class role extends Component { @property({ type: Node, tooltip: '可选:编辑器里指定;运行时优先用 setFollowTarget()', }) followTarget: Node | null = null; private _carrierTetri: tetriNode | null = null; private _movedToRoleLayer = false; /** 落稳挂层时,角色世界位置在 followTarget 空间下的局部点(用于每帧跟旋转) */ private readonly _offsetInTargetSpace = new Vec3(); private readonly _tmpWorldPos = new Vec3(); private readonly _matInv = new Mat4(); /** followTarget 世界坐标合理范围(玩法区约 ±2500,超出视为方块已滑出/掉落失控) */ private static readonly _MAX_FOLLOW_WORLD_X = 2500; private static readonly _MAX_FOLLOW_WORLD_Y = 50000; private _isFollowTargetWorldSane(target: Node): boolean { const wp = target.worldPosition; return ( Math.abs(wp.x) <= role._MAX_FOLLOW_WORLD_X && Math.abs(wp.y) <= role._MAX_FOLLOW_WORLD_Y && wp.y >= -1200 && Number.isFinite(wp.x) && Number.isFinite(wp.y) ); } /** * 动态设置跟随目标(一般为带刚体的方块根节点 / tetriNode 所在节点)。 * 动态创建角色后务必调用一次;会刷新内部 tetriNode 引用。 */ public setFollowTarget(node: Node | null): void { this.followTarget = node?.isValid ? node : null; this._refreshCarrierTetri(); } private _refreshCarrierTetri(): void { const t = this.followTarget; this._carrierTetri = t?.getComponent(tetriNode) ?? t?.getComponentInChildren(tetriNode) ?? null; } /** 与子节点统一 layer,避免 UI 相机不画 battleScene 等;hand 为 Sprite 保持 UI_2D */ private _applyLayerRecursive(n: Node, layer: number): void { if (n.name === 'hand') { n.layer = ROLE_HAND_LAYER; return; } n.layer = layer; const ch = n.children; for (let i = 0; i < ch.length; i++) { const c = ch[i]; if (c?.isValid) this._applyLayerRecursive(c, layer); } } /** 从参考节点取 group(编辑器分组 / 部分版本 Node 上仍有 group 字段) */ private _getRefNodeGroup(ref: Node): unknown { return (ref as any).group; } /** 参考层上用于碰撞的 group(取 ref 子树里第一个 Collider2D;空层则无) */ private _getRefColliderGroup(ref: Node): number | undefined { const cols = ref.getComponentsInChildren(Collider2D) ?? []; for (let i = 0; i < cols.length; i++) { const col = cols[i]; if (!col?.isValid) continue; const g = (col as any).group; if (typeof g === 'number') return g; } return undefined; } /** 角色子树与 ui_roleLayer 对齐 Node.group + Collider2D.group */ private _applyGroupRecursiveFromRef(root: Node, ref: Node): void { const ng = this._getRefNodeGroup(ref); const cg = this._getRefColliderGroup(ref); const hasNg = ng !== undefined && ng !== null && (typeof ng === 'number' || typeof ng === 'string'); const walk = (n: Node): void => { if (!n?.isValid) return; if (hasNg) (n as any).group = ng; if (cg !== undefined) { const cols = n.getComponents(Collider2D) ?? []; for (let i = 0; i < cols.length; i++) { const c = cols[i]; if (c?.isValid) (c as any).group = cg; } } const ch = n.children; for (let i = 0; i < ch.length; i++) walk(ch[i]); }; walk(root); } /** 仍在方块子树时:与直接父节点 layer 一致(card→战场 reparent 后也会自动跟上) */ private _syncLayerToParent(): void { if (this._movedToRoleLayer) return; const p = this.node.parent; if (!p?.isValid) return; if (this.node.layer === p.layer) return; this._applyLayerRecursive(this.node, p.layer); } onLoad(): void { if (!this.followTarget?.isValid) { let cur: Node | null = this.node.parent; while (cur) { const tn = cur.getComponent(tetriNode); if (tn) { this.followTarget = cur; this._carrierTetri = tn; break; } cur = cur.parent; } } else { this._refreshCarrierTetri(); } } /** * 方块销毁时同步清理 ui_roleLayer 上仍跟随该方块(或其子树)的角色。 * 角色落稳后会 reparent 到 roleLayer,不再随方块节点级联 destroy。 */ public static destroyRoleLayerFollowersOf(blockRoot: Node | null): void { if (!blockRoot) return; const layer = gg?.game?.CurentBattle?.ui_roleLayer; if (!layer?.isValid) return; const list = layer.getComponentsInChildren(role); for (let i = 0; i < list.length; i++) { const comp = list[i]; if (!comp?.node?.isValid) continue; const ft = comp.followTarget; if (!ft) continue; if (!ft.isValid) { comp.node.destroy(); continue; } let cur: Node | null = ft; while (cur) { if (cur === blockRoot) { comp.node.destroy(); break; } cur = cur.parent; } } } /** * 扫描 ui_roleLayer:followTarget 已失效的孤儿角色(兜底,不依赖方块 onDestroy 时序)。 */ public static pruneOrphanRoleLayer(): void { const layer = gg?.game?.CurentBattle?.ui_roleLayer; if (!layer?.isValid) return; const list = layer.getComponentsInChildren(role); for (let i = 0; i < list.length; i++) { const comp = list[i]; if (!comp?.node?.isValid) continue; const ft = comp.followTarget; if (!ft || !ft.isValid) { comp.node.destroy(); } } } /** * 视口变化后重算 ui_roleLayer 上角色相对方块的跟随偏移(小窗 PiP 等)。 */ public static refreshFollowOffsetsAfterViewportChange(): void { const layer = gg?.game?.CurentBattle?.ui_roleLayer; if (!layer?.isValid) return; const list = layer.getComponentsInChildren(role); for (let i = 0; i < list.length; i++) { list[i].refreshFollowOffset(); } } /** 重算已挂到 roleLayer 的跟随偏移 */ public refreshFollowOffset(): void { const target = this.followTarget; if (!target?.isValid || !this._movedToRoleLayer) return; Mat4.invert(this._matInv, target.worldMatrix); Vec3.transformMat4(this._offsetInTargetSpace, this.node.worldPosition, this._matInv); } private _destroySelf(): void { if (isValid(this.node)) this.node.destroy(); } /** * 用 lateUpdate:方块带 RigidBody2D,世界坐标在物理步之后才最终确定; * 若在 update 里采样会落后一帧,表现为角色与方块脱节、拖影。 */ lateUpdate(): void { const target = this.followTarget; if (target != null && !target.isValid) { this._destroySelf(); return; } const battle = gg?.game?.CurentBattle; // 暂停时仍同步位置(预警/弹窗叠层下物理或外力仍可能挪块),避免角色留在旧坐标 if (battle && (battle.IsEdit || battle.IsGameOver)) { return; } if (!target?.isValid) { return; } if (!this._isFollowTargetWorldSane(target)) { console.warn( '[role] followTarget 世界坐标异常,销毁角色', target.name, target.worldPosition, 'role=', this.node.name, ); this._destroySelf(); return; } this._syncLayerToParent(); const tn = this._carrierTetri ?? target.getComponent(tetriNode) ?? target.getComponentInChildren(tetriNode); if (tn?.isSettled && !this._movedToRoleLayer) { const layer = gg?.game?.CurentBattle?.ui_roleLayer; if (layer?.isValid) { Mat4.invert(this._matInv, target.worldMatrix); const wp = this.node.worldPosition; Vec3.transformMat4(this._offsetInTargetSpace, wp, this._matInv); this.node.setParent(layer, true); this._applyLayerRecursive(this.node, layer.layer); this._applyGroupRecursiveFromRef(this.node, layer); this._movedToRoleLayer = true; } } if (this._movedToRoleLayer && target.isValid) { Vec3.transformMat4(this._tmpWorldPos, this._offsetInTargetSpace, target.worldMatrix); this.node.setWorldPosition(this._tmpWorldPos); this.node.worldRotation = target.worldRotation; } } }