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.
248 lines
9.4 KiB
248 lines
9.4 KiB
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|