import { Node } from 'cc'; import { DEV } from 'cc/env'; import { tetriFloorNode } from '../tetriCard/tetriFloorNode'; import { tetriNode } from '../tetriCard/tetriNode'; import { PfDiagnostics } from '../diagnostics/PfDiagnostics'; type BlockCounts = { weapon: number; floor: number; weaponHidden: number }; /** * 叠塔异常诊断(问题2/3): * - BLOCK_STACK:有 floor 无 weapon(幽灵架 / 方块不见了只剩无重力底) * - BLOCK_HIDDEN:武器块还在场景但不可见 * - PIN_COLLAPSE:钉 floor 后武器块大量消失(崩开) */ export class TetriStackDiagnostics { private static _lastPinMs = 0; private static _sessionBattle: object | null = null; private static _ghostShelfReported = false; private static _pinWeaponBaseline = 0; private static _pinFloorName = ''; private static readonly LOG_COOLDOWN_MS = 120000; private static readonly PIN_COLLAPSE_COOLDOWN_MS = 30000; private static readonly LAUNCH_ZERO_COOLDOWN_MS = 60000; private static readonly PIN_WINDOW_MS = 8000; /** 离开战斗 / 新局初始化时清空,避免上一局 pin/幽灵架状态污染 */ static resetBattle(): void { this._lastPinMs = 0; this._sessionBattle = null; this._ghostShelfReported = false; this._pinWeaponBaseline = 0; this._pinFloorName = ''; } static countBlocks(): BlockCounts { const map = gg.game?.CurentBattle?.TetraMap; if (!map?._tetrisTable?.isValid) return { weapon: 0, floor: 0, weaponHidden: 0 }; let weapon = 0; let floor = 0; let weaponHidden = 0; const walk = (n: Node) => { const children = n.children; for (let i = 0; i < children.length; i++) { const c = children[i]; if (c.getComponent(tetriNode) && !c.getComponent(tetriFloorNode)) { weapon++; if (c.isValid && !c.activeInHierarchy) weaponHidden++; } if (c.getComponent(tetriFloorNode)) floor++; walk(c); } }; walk(map._tetrisTable); return { weapon, floor, weaponHidden }; } private static _syncBattleSession(): void { const b = gg.game?.CurentBattle ?? null; if (b !== this._sessionBattle) { this._sessionBattle = b; this._ghostShelfReported = false; } } private static _log(tag: string, detail: string, cooldownMs = this.LOG_COOLDOWN_MS, level: 'error' | 'warn' | 'info' = 'warn'): void { PfDiagnostics.write('STACK', tag, detail, cooldownMs, level); } /** pinFloor 开始时记时间戳与武器基线,用于 pin 后塌塔判断 */ static onPinFloor(floorNodeName: string): void { this._lastPinMs = Date.now(); this._pinFloorName = floorNodeName ?? ''; this._pinWeaponBaseline = this.countBlocks().weapon; } /** * pin 序列跑完后:塌塔 / 隐藏块 / 幽灵架。 */ static afterPinFloorSequence(): void { const after = this.countBlocks(); const before = this._pinWeaponBaseline; if (after.weaponHidden >= 2) { if (DEV) { console.warn('[StackDiag] BLOCK_HIDDEN', `hid=${after.weaponHidden} w=${after.weapon} f=${after.floor}`); } } if (before >= 2) { const lost = before - after.weapon; if (lost >= 2) { this._log( 'PIN_COLLAPSE', `w${before}->${after.weapon} f=${after.floor} lost=${lost} floor=${this._pinFloorName}`, this.PIN_COLLAPSE_COOLDOWN_MS, 'error', ); } } if (after.floor >= 1 && after.weapon === 0 && before >= 2) { const afterPin = this._lastPinMs > 0 && Date.now() - this._lastPinMs < this.PIN_WINDOW_MS; this._log( 'BLOCK_STACK', `w${before}->0 f=${after.floor} afterPin=${afterPin ? 1 : 0} floor=${this._pinFloorName}`, this.LOG_COOLDOWN_MS, 'error', ); } } /** 发射点落到世界原点兜底(仅 DEV console,不上传) */ static onLaunchOriginFallback(weaponId: number, carrierCount: number): void { if (!DEV) return; const c = this.countBlocks(); console.warn('[StackDiag] LAUNCH_ZERO', `wid=${weaponId} carriers=${carrierCount} w=${c.weapon} f=${c.floor}`); } /** 武器块已从场景移除之后调用 */ static afterWeaponRemoved(source: string, weaponBefore: number, nodeName: string): void { if (source === 'hammer') return; this._syncBattleSession(); if (this._ghostShelfReported) return; const after = this.countBlocks(); if (after.floor < 1 || after.weapon > 0) return; if (weaponBefore < 2) return; this._ghostShelfReported = true; const afterPin = this._lastPinMs > 0 && Date.now() - this._lastPinMs < this.PIN_WINDOW_MS; this._log( 'BLOCK_STACK', `w${weaponBefore}->0 f=${after.floor} src=${source} node=${nodeName} afterPin=${afterPin ? 1 : 0}`, this.LOG_COOLDOWN_MS, 'error', ); } }