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.
319 lines
12 KiB
319 lines
12 KiB
|
1 week ago
|
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}`);
|
||
|
|
}
|
||
|
|
}
|