消消方块阵换皮表情
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.
 
 
 

268 lines
10 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();
}
}
}
/**
* 视口变化后重算 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;
}
}
}