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.
316 lines
11 KiB
316 lines
11 KiB
|
3 weeks ago
|
import { Node, sp, UIOpacity } from 'cc';
|
||
|
|
import { TextGrid } from '../view/TextGrid';
|
||
|
|
|
||
|
|
const TAG = '[BagTextDrag]';
|
||
|
|
/** 池污染偷字专项:控制台过滤此标签即可复制整段证据 */
|
||
|
|
const STEAL = '[BagTextSteal]';
|
||
|
|
|
||
|
|
/** 默认关;控制台 `setBagTextDragLogVerbose(true)` 开启 */
|
||
|
|
let _verbose = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 开关 BagTextDrag / BagTextSteal 打印。
|
||
|
|
* @param enabled 是否开启
|
||
|
|
*/
|
||
|
|
export function setBagTextDragLogVerbose(enabled: boolean): void {
|
||
|
|
_verbose = !!enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 当前是否开启拖动诊断打印 */
|
||
|
|
export function isBagTextDragLogVerbose(): boolean {
|
||
|
|
return _verbose;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
(globalThis as { setBagTextDragLogVerbose?: typeof setBagTextDragLogVerbose }).setBagTextDragLogVerbose = setBagTextDragLogVerbose;
|
||
|
|
} catch {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Spine 组件上可读的缓存/皮肤字段(引擎 protected,仅诊断用) */
|
||
|
|
type SpineDebugFields = sp.Skeleton & {
|
||
|
|
_cacheMode?: number;
|
||
|
|
_skinName?: string;
|
||
|
|
_animationName?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 字块显示/皮肤快照(拖动诊断) */
|
||
|
|
export interface BagTextDragVisualSnap {
|
||
|
|
/** 节点是否有效 */
|
||
|
|
valid: boolean;
|
||
|
|
/** 世界坐标 */
|
||
|
|
world: string;
|
||
|
|
/** 本地坐标 */
|
||
|
|
local: string;
|
||
|
|
/** 根节点 active */
|
||
|
|
nodeActive: boolean;
|
||
|
|
/** 根节点缩放 */
|
||
|
|
scale: string;
|
||
|
|
/** UIOpacity(无组件为 null) */
|
||
|
|
opacity: number | null;
|
||
|
|
/** skin 节点 active(无节点为 null) */
|
||
|
|
skinActive: boolean | null;
|
||
|
|
/** skin 本地坐标 */
|
||
|
|
skinLocal: string | null;
|
||
|
|
/** Skeleton.enabled */
|
||
|
|
skEnabled: boolean | null;
|
||
|
|
/** Spine cacheMode 数值 */
|
||
|
|
cacheMode: number | null;
|
||
|
|
/** cacheMode 可读名 */
|
||
|
|
cacheModeName: string;
|
||
|
|
/** 是否动画缓存模式 */
|
||
|
|
animCached: boolean | null;
|
||
|
|
/** 当前皮肤名 */
|
||
|
|
spineSkin: string | null;
|
||
|
|
/** 当前动画名 */
|
||
|
|
anim: string | null;
|
||
|
|
/** 业务字 id */
|
||
|
|
weapon2Id: number;
|
||
|
|
/** 位置描述 */
|
||
|
|
location: string;
|
||
|
|
/** 是否疑似「可点不可见」 */
|
||
|
|
suspectHidden: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 背包/地图字块拖动显示诊断。
|
||
|
|
* 排查「拖动后文字偶现消失但仍可操作 / 槽下无 textGrid」。
|
||
|
|
*/
|
||
|
|
export class BagTextDragDebugLog {
|
||
|
|
private static _seq = 0;
|
||
|
|
/** 当前进行中的拖动序号(未开始为 0) */
|
||
|
|
private static _activeSeq = 0;
|
||
|
|
|
||
|
|
/** 新一次拖动开始,返回序号 */
|
||
|
|
static begin(grid: TextGrid | null, stage = 'start'): number {
|
||
|
|
this._seq += 1;
|
||
|
|
this._activeSeq = this._seq;
|
||
|
|
this.snap(this._seq, stage, grid);
|
||
|
|
this.steal('DRAG_BEGIN', `seq=#${this._seq} path=${this.parentPath(grid)} w2=${grid?.weapon2Id ?? 0}`);
|
||
|
|
return this._seq;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 当前拖动序号(无进行中返回 0) */
|
||
|
|
static activeSeq(): number {
|
||
|
|
return this._activeSeq;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 池污染/偷字专项日志(默认关;开启后便于整段复制)。
|
||
|
|
* @param event 事件码
|
||
|
|
* @param detail 详情
|
||
|
|
*/
|
||
|
|
static steal(event: string, detail: string): void {
|
||
|
|
if (!_verbose) return;
|
||
|
|
const seq = this._activeSeq > 0 ? this._activeSeq : this._seq;
|
||
|
|
console.log(`${STEAL} #${seq} ${event} | ${detail}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 打印槽位子节点名(佐证 textGrid 是否还在 uigrid 下)。
|
||
|
|
* @param slotIndex 槽位
|
||
|
|
* @param slotNode 槽节点
|
||
|
|
* @param stage 阶段
|
||
|
|
*/
|
||
|
|
static slotChildren(slotIndex: number, slotNode: Node | null | undefined, stage: string): void {
|
||
|
|
if (!slotNode?.isValid) {
|
||
|
|
this.steal('SLOT_CHILDREN', `${stage} slot=${slotIndex} slotNode=invalid`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const names: string[] = [];
|
||
|
|
let hasTextGrid = false;
|
||
|
|
for (let i = 0; i < slotNode.children.length; i++) {
|
||
|
|
const c = slotNode.children[i];
|
||
|
|
if (!c?.isValid) continue;
|
||
|
|
names.push(c.name);
|
||
|
|
if (c.name === 'textGrid' || !!c.getComponent(TextGrid)) hasTextGrid = true;
|
||
|
|
}
|
||
|
|
this.steal(
|
||
|
|
hasTextGrid ? 'SLOT_OK' : 'SLOT_MISSING_TEXTGRID',
|
||
|
|
`${stage} slot=${slotIndex} name=${slotNode.name} children=[${names.join(',')}] hasTextGrid=${hasTextGrid}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* getNode 取到仍在场景/索引中的在用节点。
|
||
|
|
* @param reason 原因
|
||
|
|
* @param detail 详情
|
||
|
|
*/
|
||
|
|
static poolHitLive(reason: string, detail: string): void {
|
||
|
|
this.steal(`POOL_HIT_LIVE:${reason}`, detail);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 源字已离槽。
|
||
|
|
* @param slotIndex 槽位
|
||
|
|
* @param grid 字块
|
||
|
|
* @param repaired 是否已挂回
|
||
|
|
*/
|
||
|
|
static sourceLeftSlot(slotIndex: number, grid: TextGrid | null | undefined, repaired: boolean): void {
|
||
|
|
this.steal(
|
||
|
|
repaired ? 'SOURCE_LEFT_SLOT_REPAIRED' : 'SOURCE_LEFT_SLOT',
|
||
|
|
`slot=${slotIndex} w2=${grid?.weapon2Id ?? 0} path=${this.parentPath(grid)} repaired=${repaired}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 补齐空槽。
|
||
|
|
* @param slotIndex 槽位
|
||
|
|
* @param weapon2Id 字 id
|
||
|
|
* @param ok 是否成功
|
||
|
|
*/
|
||
|
|
static fillMissing(slotIndex: number, weapon2Id: number, ok: boolean): void {
|
||
|
|
this.steal(
|
||
|
|
ok ? 'FILL_MISSING_OK' : 'FILL_MISSING_FAIL',
|
||
|
|
`slot=${slotIndex} w2=${weapon2Id}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 从池数组摘掉仍在场景的节点。
|
||
|
|
* @param removed 摘掉个数
|
||
|
|
* @param detail 详情
|
||
|
|
*/
|
||
|
|
static detachFromPool(removed: number, detail: string): void {
|
||
|
|
if (removed <= 0) return;
|
||
|
|
this.steal('DETACH_FROM_POOL', `removed=${removed} ${detail}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 打印字块显示快照。
|
||
|
|
* @param seq 拖动序号
|
||
|
|
* @param stage 阶段名
|
||
|
|
* @param grid 字块
|
||
|
|
* @param extra 附加说明
|
||
|
|
*/
|
||
|
|
static snap(seq: number, stage: string, grid: TextGrid | null | undefined, extra?: string): void {
|
||
|
|
if (!_verbose) return;
|
||
|
|
const s = this.capture(grid);
|
||
|
|
const flag = s.suspectHidden ? ' SUSPECT_HIDDEN' : '';
|
||
|
|
const more = extra ? ` ${extra}` : '';
|
||
|
|
console.log(
|
||
|
|
`${TAG} #${seq} ${stage}${flag}${more}`
|
||
|
|
+ ` w2=${s.weapon2Id} loc=${s.location}`
|
||
|
|
+ ` world=${s.world} local=${s.local}`
|
||
|
|
+ ` nodeActive=${s.nodeActive} scale=${s.scale} opacity=${s.opacity}`
|
||
|
|
+ ` skinActive=${s.skinActive} skinLocal=${s.skinLocal}`
|
||
|
|
+ ` skEnabled=${s.skEnabled} cache=${s.cacheModeName}(${s.cacheMode}) cached=${s.animCached}`
|
||
|
|
+ ` spineSkin=${s.spineSkin} anim=${s.anim}`
|
||
|
|
+ ` path=${this.parentPath(grid)}`,
|
||
|
|
);
|
||
|
|
if (s.suspectHidden) {
|
||
|
|
console.log(`${TAG} #${seq} ${stage} 疑似隐藏但仍可能可点`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 松手/取消结束 */
|
||
|
|
static end(seq: number, grid: TextGrid | null | undefined, outcome: string, extra?: string): void {
|
||
|
|
this.snap(seq, `end:${outcome}`, grid, extra);
|
||
|
|
this.steal('DRAG_END', `outcome=${outcome}${extra ? ` ${extra}` : ''} path=${this.parentPath(grid)}`);
|
||
|
|
if (seq === this._activeSeq) this._activeSeq = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 采集显示/皮肤状态 */
|
||
|
|
static capture(grid: TextGrid | null | undefined): BagTextDragVisualSnap {
|
||
|
|
if (!grid?.isValid || !grid.node?.isValid) {
|
||
|
|
return {
|
||
|
|
valid: false,
|
||
|
|
world: 'n/a',
|
||
|
|
local: 'n/a',
|
||
|
|
nodeActive: false,
|
||
|
|
scale: 'n/a',
|
||
|
|
opacity: null,
|
||
|
|
skinActive: null,
|
||
|
|
skinLocal: null,
|
||
|
|
skEnabled: null,
|
||
|
|
cacheMode: null,
|
||
|
|
cacheModeName: 'n/a',
|
||
|
|
animCached: null,
|
||
|
|
spineSkin: null,
|
||
|
|
anim: null,
|
||
|
|
weapon2Id: 0,
|
||
|
|
location: 'invalid',
|
||
|
|
suspectHidden: true,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const node = grid.node;
|
||
|
|
const wp = node.worldPosition;
|
||
|
|
const lp = node.position;
|
||
|
|
const uo = node.getComponent(UIOpacity);
|
||
|
|
const skin = grid.getSkinNode();
|
||
|
|
const sk = skin?.isValid ? skin.getComponent(sp.Skeleton) : null;
|
||
|
|
const skDbg = sk?.isValid ? (sk as SpineDebugFields) : null;
|
||
|
|
const cacheMode = skDbg != null ? Number(skDbg._cacheMode) : null;
|
||
|
|
const loc = grid.location;
|
||
|
|
const location = loc?.kind === 'bag'
|
||
|
|
? `bag[${loc.slotIndex}]`
|
||
|
|
: loc?.kind === 'map'
|
||
|
|
? `map[${loc.gridIndex}]`
|
||
|
|
: 'none';
|
||
|
|
|
||
|
|
const nodeActive = !!node.active;
|
||
|
|
const opacity = uo?.isValid ? uo.opacity : null;
|
||
|
|
const scaleNearZero = Math.abs(node.scale.x) < 0.05 || Math.abs(node.scale.y) < 0.05;
|
||
|
|
const skinActive = skin?.isValid ? skin.active : null;
|
||
|
|
const skEnabled = sk?.isValid ? sk.enabled : null;
|
||
|
|
const suspectHidden = nodeActive
|
||
|
|
&& (
|
||
|
|
(opacity != null && opacity < 16)
|
||
|
|
|| scaleNearZero
|
||
|
|
|| skinActive === false
|
||
|
|
|| skEnabled === false
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
valid: true,
|
||
|
|
world: this._fmtXY(wp.x, wp.y),
|
||
|
|
local: this._fmtXY(lp.x, lp.y),
|
||
|
|
nodeActive,
|
||
|
|
scale: `${node.scale.x.toFixed(2)},${node.scale.y.toFixed(2)}`,
|
||
|
|
opacity,
|
||
|
|
skinActive,
|
||
|
|
skinLocal: skin?.isValid ? this._fmtXY(skin.position.x, skin.position.y) : null,
|
||
|
|
skEnabled,
|
||
|
|
cacheMode,
|
||
|
|
cacheModeName: this._cacheModeName(cacheMode),
|
||
|
|
animCached: sk?.isValid ? sk.isAnimationCached() : null,
|
||
|
|
spineSkin: skDbg?._skinName ?? null,
|
||
|
|
anim: skDbg?._animationName ?? null,
|
||
|
|
weapon2Id: grid.weapon2Id,
|
||
|
|
location,
|
||
|
|
suspectHidden,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 父节点路径(诊断「跑哪去了」) */
|
||
|
|
static parentPath(grid: TextGrid | null | undefined): string {
|
||
|
|
if (!grid?.node?.isValid) return 'n/a';
|
||
|
|
return this.nodeParentPath(grid.node);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 任意节点父路径 */
|
||
|
|
static nodeParentPath(node: Node | null | undefined): string {
|
||
|
|
if (!node?.isValid) return 'n/a';
|
||
|
|
const parts: string[] = [];
|
||
|
|
let n: Node | null = node;
|
||
|
|
for (let i = 0; i < 8 && n?.isValid; i++) {
|
||
|
|
parts.push(n.name || '?');
|
||
|
|
n = n.parent;
|
||
|
|
}
|
||
|
|
return parts.join(' ← ');
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _fmtXY(x: number, y: number): string {
|
||
|
|
return `(${x.toFixed(1)},${y.toFixed(1)})`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static _cacheModeName(mode: number | null): string {
|
||
|
|
if (mode == null || Number.isNaN(mode)) return 'n/a';
|
||
|
|
if (mode === sp.SpineAnimationCacheMode.REALTIME) return 'REALTIME';
|
||
|
|
if (mode === sp.SpineAnimationCacheMode.SHARED_CACHE) return 'SHARED';
|
||
|
|
if (mode === sp.SpineAnimationCacheMode.PRIVATE_CACHE) return 'PRIVATE';
|
||
|
|
return `mode${mode}`;
|
||
|
|
}
|
||
|
|
}
|