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}`; } }