import MTools from '../../../mx/tools/MTools'; import { sys } from 'cc'; import { sdkConfig } from '../../sdk/sdkConfig'; import { MiniGamePlatform } from '../../sdk/platform/MiniGamePlatform'; import { SmartPreload } from '../../game/SmartPreload'; /** 堆叠高度分档:仅影响 VFX/SFX 表现,不改伤害/逻辑 */ enum StackPerfTier { Normal = 0, /** ≥30m */ Mid = 1, /** ≥60m */ High = 2, } /** * 堆叠越高,战斗特效/音效越精简,减轻同屏方块+特效压力。 * - 微信 iOS:物理冻结 + 全面降频 * - 全平台:落稳方块可冻 Static(撞击仍 unfreeze) * - 抖音小游戏:VFX/SFX/UI 轻量节流 * - 平台内存告警:升档节流,不主动 triggerGC */ export class BattlePerformance { /** 与左侧高度尺 30m 里程碑一致 */ static readonly HIGH_STACK_M = 30; /** 与左侧高度尺 60m 里程碑一致 */ static readonly ULTRA_HIGH_STACK_M = 60; /** 平台 onMemoryWarning 升档:0=无 1=轻 2=中 3=重 */ private static _memPressureLevel = 0; /** 微信小游戏 + iOS:需要重度性能节流 */ static isWeChatIos(): boolean { if (sys.isBrowser) { const p = sdkConfig.browserBattlePerfProfile; if (p === 'ios') return true; if (p === 'android') return false; return false; } return sys.platform === sys.Platform.WECHAT_GAME && sys.os === sys.OS.IOS; } /** 抖音小游戏:轻量战斗节流(主投放渠道) */ static isDouyinMiniGame(): boolean { if (sys.isBrowser) { return sdkConfig.browserBattlePerfProfile === 'douyin'; } return sys.platform === sys.Platform.BYTEDANCE_MINI_GAME; } /** 抖音鸿蒙(OpenHarmony):与抖音同档并略加强节流 */ static isDouyinHarmonyOS(): boolean { if (!this.isDouyinMiniGame()) return false; MiniGamePlatform.init(); return MiniGamePlatform.isHarmonyOS; } /** 抖音系(含鸿蒙)是否启用轻量节流 */ private static _isDouyinPerfHost(): boolean { return this.isDouyinMiniGame(); } /** 平台内存告警回调:只升档节流,不触发 GC */ static onMemoryPressure(warnLevel: number): void { if (warnLevel >= 15) { this._memPressureLevel = 3; } else if (warnLevel >= 10) { this._memPressureLevel = Math.max(this._memPressureLevel, 2); } else if (warnLevel >= 5) { this._memPressureLevel = Math.max(this._memPressureLevel, 1); } console.warn(`[BattlePerf] memory pressure → level=${this._memPressureLevel} (warn=${warnLevel})`); SmartPreload.onMemoryPressure(warnLevel); } static resetMemoryPressure(): void { this._memPressureLevel = 0; } static memoryPressureLevel(): number { return this._memPressureLevel; } /** 内存压力对特效保留率的乘数(越小越抽稀) */ private static _memPressureVfxMul(): number { switch (this._memPressureLevel) { case 3: return 0.45; case 2: return 0.62; case 1: return 0.78; default: return 1; } } /** 当前生效的性能档位(日志/调试) */ static activePerfProfile(): 'ios' | 'android' | 'douyin' | 'douyin-harmony' { if (this.isWeChatIos()) return 'ios'; if (this.isDouyinHarmonyOS()) return 'douyin-harmony'; if (this.isDouyinMiniGame()) return 'douyin'; return 'android'; } /** 浏览器切换性能档位;进战斗前切换更稳妥 */ static setBrowserPerfProfile(profile: 'auto' | 'ios' | 'android' | 'douyin'): void { if (!sys.isBrowser) { console.warn('[BattlePerf] setBrowserPerfProfile 仅浏览器预览有效'); return; } sdkConfig.browserBattlePerfProfile = profile; const map = gg.game?.CurentBattle?.TetraMap; if (map?.isValid) { map.recalcInterval = this.tetriRecalcIntervalSec(); } console.log(`[BattlePerf] browserBattlePerfProfile=${profile} → active=${this.activePerfProfile()}`); } /** 全平台:落稳后冻 Static,撞击时仍会 unfreeze(试塌塔/减算力) */ static shouldFreezeTetriPhysics(): boolean { return true; } /** 落稳后至少等待多久(ms)才允许冻结;buried=塔顶活跃带以下可更快冻 */ static tetriFreezeMinSettledMs(buried: boolean = false): number { if (buried) return this.isWeChatIos() ? 700 : 450; return this.isWeChatIos() ? 1600 : 1200; } /** 落稳后连续低速帧数才冻结(约 60fps) */ static tetriFreezeSleepStreak(buried: boolean = false): number { if (buried) return this.isWeChatIos() ? 50 : 36; return this.isWeChatIos() ? 120 : 90; } /** * 塔顶「活跃带」高度(世界 px):带内严条件慢冻,带下可快冻。 * 约 3 格,保证倾覆/新落块主要发生区保持 Dynamic。 */ static tetriFreezeActiveBandPx(): number { return 120; } /** 冻结前合成速度上限 */ static tetriFreezeMaxSpeed(): number { return 0.35; } /** 冻结前 |角速度| 上限 */ static tetriFreezeMaxAng(): number { return 0.2; } /** 冻结前 |vy| 上限 */ static tetriFreezeMaxVy(): number { return 0.35; } /** 倾斜超过此值(相对 90° 网格)禁止冻 Static,避免「翘着冻住」 */ static tetriFreezeMaxTiltFromGridDeg(): number { return 8; } /** 安静窗口内角度漂移超过此值则重新计时(慢倾覆) */ static tetriFreezeMaxQuietAngleDriftDeg(): number { return 2.5; } /** 白线全表重算间隔(秒) */ static tetriRecalcIntervalSec(): number { if (this.isWeChatIos()) return 0.3; if (this.isDouyinHarmonyOS()) return 0.22; if (this._isDouyinPerfHost()) return 0.18; if (this._memPressureLevel >= 2) return 0.2; return 0.1; } /** 武器 CD 倍率(>1 发射更慢)。仅极端机型启用,禁止把抖音常态当“优化”改战斗数值 */ static weaponCdMul(): number { if (this.isWeChatIos()) return 1.4; return 1; } /** 同武器两次发射最小间隔(ms);仅微信 iOS 启用 */ static weaponFireMinIntervalMs(): number { if (this.isWeChatIos()) return 150; return 0; } /** 秒伤 UI 刷新最小间隔(ms) */ static dpsUiMinIntervalMs(): number { if (this.isWeChatIos()) return 450; if (this.isDouyinHarmonyOS()) return 400; if (this._isDouyinPerfHost()) return 350; if (this._memPressureLevel >= 1) return 280; return 0; } /** 伤害飘字单帧上限 */ static hurtFloatMaxPerFrame(): number { let max = 15; if (this.isWeChatIos()) max = 8; else if (this.isDouyinHarmonyOS()) max = 6; else if (this._isDouyinPerfHost()) max = 8; if (this._memPressureLevel >= 3) max = Math.min(max, 5); else if (this._memPressureLevel >= 2) max = Math.min(max, 6); else if (this._memPressureLevel >= 1) max = Math.min(max, 7); return max; } static getTier(): StackPerfTier { const b = gg.game?.CurentBattle; if (!b || b.IsGameOver) return StackPerfTier.Normal; const h = b.TetriStackHeightMeters; if (h >= this.ULTRA_HIGH_STACK_M) return StackPerfTier.High; if (h >= this.HIGH_STACK_M) return StackPerfTier.Mid; return StackPerfTier.Normal; } static isHighStackMode(): boolean { return this.getTier() >= StackPerfTier.Mid; } static isUltraHighStackMode(): boolean { return this.getTier() >= StackPerfTier.High; } /** 一次性爆炸/受击特效:高堆叠时抽样显示 */ static rollShowCombatVfx(customRate?: number): boolean { const tier = this.getTier(); const memMul = this._memPressureVfxMul(); if (tier === StackPerfTier.Normal) { if (this.isWeChatIos() && Math.random() > 0.62 * memMul) return false; if (this.isDouyinHarmonyOS() && Math.random() > 0.68 * memMul) return false; if (this._isDouyinPerfHost() && Math.random() > 0.75 * memMul) return false; if (this._memPressureLevel >= 1 && Math.random() > memMul) return false; return true; } const tierRate = tier === StackPerfTier.High ? 0.28 : 0.5; const rate = (customRate != null ? Math.min(customRate, tierRate) : tierRate) * memMul; if (this.isWeChatIos()) { return Math.random() < rate * 0.75; } if (this.isDouyinHarmonyOS()) { return Math.random() < rate * 0.78; } if (this._isDouyinPerfHost()) { return Math.random() < rate * 0.85; } return Math.random() < rate; } static vfxScaleMul(): number { let mul: number; switch (this.getTier()) { case StackPerfTier.High: mul = 0.58; break; case StackPerfTier.Mid: mul = 0.72; break; default: mul = 1; break; } if (this._memPressureLevel >= 2) mul *= 0.88; return mul; } static adjustVfxScale(base: number): number { return base * this.vfxScaleMul(); } /** 怪物受击特效节流(ms) */ static hitVfxCooldownMs(): number { let ms: number; switch (this.getTier()) { case StackPerfTier.High: ms = 650; break; case StackPerfTier.Mid: ms = 420; break; default: ms = 200; break; } if (this.isWeChatIos()) ms = Math.floor(ms * 1.75); else if (this.isDouyinHarmonyOS()) ms = Math.floor(ms * 1.5); else if (this._isDouyinPerfHost()) ms = Math.floor(ms * 1.35); if (this._memPressureLevel >= 2) ms = Math.floor(ms * 1.25); else if (this._memPressureLevel >= 1) ms = Math.floor(ms * 1.12); return ms; } static isWeaponBattleSfx(path: string, name: string): boolean { if (path.includes('武器音效')) return true; return /命中|受击|子弹|飞弹|无人机攻击|溅水|冲击波/.test(name); } /** * 武器/子弹类音效是否应播放。 * 注意:MTools.beforeTimes 在冷却期内返回 true(应跳过),故这里必须取反。 * 旧逻辑直接 return beforeTimes,导致「第一次该播时被拦、射速低时几乎全无声」。 */ static shouldPlayWeaponSfx(path: string, name: string): boolean { if (!this.isWeaponBattleSfx(path, name)) return true; // 低堆叠且内存压力不高:不节流,保证前几波音效完整 const needThrottle = this.isHighStackMode() || this.isWeChatIos() || this.isDouyinHarmonyOS() || this._memPressureLevel >= 2; if (!needThrottle) return true; let minIntervalMs = 80; if (this.isHighStackMode()) { minIntervalMs = this.getTier() === StackPerfTier.High ? 140 : 90; } if (this.isWeChatIos()) { minIntervalMs = Math.max(minIntervalMs, 160); } else if (this.isDouyinHarmonyOS()) { minIntervalMs = Math.max(minIntervalMs, 150); } else if (this._isDouyinPerfHost()) { minIntervalMs = Math.max(minIntervalMs, 120); } if (this._memPressureLevel >= 2) { minIntervalMs = Math.max(minIntervalMs, 180); } // 按音效名分别节流,避免所有武器抢同一把锁 return !MTools.beforeTimes(minIntervalMs, `battle_sfx_${name || path}`); } }