From 4087d189891c8c2008dc9a5a5f6c96e749448f41 Mon Sep 17 00:00:00 2001 From: humingxing <930621236@qq.com> Date: Thu, 15 Oct 2026 15:51:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0boss=E6=8A=80=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prefab/monster/Boss土.prefab | 4 +- .../prefab/monster/Boss土.prefab.meta | 2 +- .../prefab/monster/Boss木.prefab | 4 +- .../prefab/monster/Boss木.prefab.meta | 2 +- .../prefab/monster/Boss水.prefab | 4 +- .../prefab/monster/Boss水.prefab.meta | 2 +- .../prefab/monster/Boss火.prefab | 4 +- .../prefab/monster/Boss火.prefab.meta | 2 +- .../prefab/monster/Boss金.prefab | 4 +- .../prefab/monster/Boss金.prefab.meta | 2 +- assets/bundles/first/config/Skill.json | 10 +- .../mx/module/battle/combat/AbilitySystem.ts | 7 +- .../mx/module/battle/combat/TargetResolver.ts | 34 +- .../script/battle/combat/MonsterSkillHost.ts | 120 +++- .../battle/combat/PassiveSkillRuntime.ts | 93 ++- .../script/battle/config/BattleHeroTable.ts | 25 +- .../config/TableBattleConfigProvider.ts | 6 +- .../config/baseAtk/SkillAbilityBuilder.ts | 12 +- assets/script/battle/skill/SkillController.ts | 16 + .../battle/view/CocosBattleCombatPresenter.ts | 47 +- .../view/CocosBattleMonsterPresenter.ts | 63 +- assets/script/battle/view/TextGrid.ts | 63 ++ assets/script/manager/MonsterDataManager.ts | 52 +- assets/script/ui/BattleBoss/UIBattleBoss.ts | 9 +- .../ui/BattleGame/BattleGameExtraBossSkill.ts | 3 +- docs/方案-出怪逻辑实现.md | 10 +- docs/方案-怪物技能.md | 17 +- docs/方案-战斗技能逻辑层.md | 2 +- preview-template/index.ejs | 12 + preview-template/workbench/css/workbench.css | 482 ++++++++++++- preview-template/workbench/js/agent-assets.js | 646 +++++++++--------- preview-template/workbench/js/agent-core.js | 12 + .../workbench/js/agent-profiler.js | 227 ++++++ preview-template/workbench/js/device.js | 304 ++++++++- preview-template/workbench/js/host.js | 148 +++- .../workbench/js/panels/assets.js | 10 +- preview-template/workbench/js/panels/perf.js | 388 +++++++---- 配置表/excelList/技能表.xlsx | Bin 23981 -> 24289 bytes 38 files changed, 2124 insertions(+), 724 deletions(-) create mode 100644 preview-template/workbench/js/agent-profiler.js diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab b/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab index a2f579b..88d32ec 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Boss土", + "_name": "BOSS土", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,7 +13,7 @@ }, { "__type__": "cc.Node", - "_name": "Boss土", + "_name": "BOSS土", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab.meta b/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab.meta index c66e900..e7390b2 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab.meta +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss土.prefab.meta @@ -8,6 +8,6 @@ ], "subMetas": {}, "userData": { - "syncNodeName": "Boss土" + "syncNodeName": "BOSS土" } } diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab b/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab index 250beab..a5c810e 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Boss木", + "_name": "BOSS木", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,7 +13,7 @@ }, { "__type__": "cc.Node", - "_name": "Boss木", + "_name": "BOSS木", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab.meta b/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab.meta index 7cebeb9..745a297 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab.meta +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss木.prefab.meta @@ -8,6 +8,6 @@ ], "subMetas": {}, "userData": { - "syncNodeName": "Boss木" + "syncNodeName": "BOSS木" } } diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab b/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab index a9101c1..5b7c7ed 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Boss水", + "_name": "BOSS水", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,7 +13,7 @@ }, { "__type__": "cc.Node", - "_name": "Boss水", + "_name": "BOSS水", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab.meta b/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab.meta index 89cb549..f68ab9d 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab.meta +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss水.prefab.meta @@ -8,6 +8,6 @@ ], "subMetas": {}, "userData": { - "syncNodeName": "Boss水" + "syncNodeName": "BOSS水" } } diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab b/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab index 576ce5f..48f9134 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Boss火", + "_name": "BOSS火", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,7 +13,7 @@ }, { "__type__": "cc.Node", - "_name": "Boss火", + "_name": "BOSS火", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab.meta b/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab.meta index 1da9d8d..d090eb2 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab.meta +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss火.prefab.meta @@ -8,6 +8,6 @@ ], "subMetas": {}, "userData": { - "syncNodeName": "Boss火" + "syncNodeName": "BOSS火" } } diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab b/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab index 530373b..b9fd17c 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab @@ -1,7 +1,7 @@ [ { "__type__": "cc.Prefab", - "_name": "Boss金", + "_name": "BOSS金", "_objFlags": 0, "__editorExtras__": {}, "_native": "", @@ -13,7 +13,7 @@ }, { "__type__": "cc.Node", - "_name": "Boss金", + "_name": "BOSS金", "_objFlags": 0, "__editorExtras__": {}, "_parent": null, diff --git a/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab.meta b/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab.meta index f06837a..813d656 100644 --- a/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab.meta +++ b/assets/bundles/battleGameMonster/prefab/monster/Boss金.prefab.meta @@ -8,6 +8,6 @@ ], "subMetas": {}, "userData": { - "syncNodeName": "Boss金" + "syncNodeName": "BOSS金" } } diff --git a/assets/bundles/first/config/Skill.json b/assets/bundles/first/config/Skill.json index 2b6eba7..e30f1ae 100644 --- a/assets/bundles/first/config/Skill.json +++ b/assets/bundles/first/config/Skill.json @@ -558,7 +558,7 @@ "level": 0, "cd": 16, "priority": 0, - "animation": "攻击1", + "animation": "攻击", "showeffect": "", "target_stage": 1, "range": 0, @@ -580,7 +580,7 @@ "level": 0, "cd": 16, "priority": 0, - "animation": "攻击1", + "animation": "攻击", "showeffect": "", "target_stage": 3, "range": 0, @@ -602,7 +602,7 @@ "level": 0, "cd": 16, "priority": 0, - "animation": "攻击1", + "animation": "攻击", "showeffect": "", "target_stage": 2, "range": 0, @@ -624,7 +624,7 @@ "level": 0, "cd": 16, "priority": 0, - "animation": "攻击1", + "animation": "攻击", "showeffect": "", "target_stage": 1, "range": 0, @@ -646,7 +646,7 @@ "level": 0, "cd": 16, "priority": 0, - "animation": "攻击1", + "animation": "攻击", "showeffect": "", "target_stage": 1, "range": 0, diff --git a/assets/mx/module/battle/combat/AbilitySystem.ts b/assets/mx/module/battle/combat/AbilitySystem.ts index 1958e51..ff490b0 100644 --- a/assets/mx/module/battle/combat/AbilitySystem.ts +++ b/assets/mx/module/battle/combat/AbilitySystem.ts @@ -223,7 +223,12 @@ export class AbilitySystem implements ISystem { if (this._services.castBlockPredicate?.(entityId)) { return false; } - if (this._services.buffs.hasTag(entityId, 'debuff_8') || this._services.buffs.hasTag(entityId, 'debuff_13')) { + // 眩晕 / 混乱 / 冰冻:塔与怪均禁攻 + if ( + this._services.buffs.hasTag(entityId, 'debuff_4') + || this._services.buffs.hasTag(entityId, 'debuff_8') + || this._services.buffs.hasTag(entityId, 'debuff_13') + ) { return false; } const abilities = this._world.getComponent(entityId, AbilitySetComp); diff --git a/assets/mx/module/battle/combat/TargetResolver.ts b/assets/mx/module/battle/combat/TargetResolver.ts index f6d12c8..9fd7e93 100644 --- a/assets/mx/module/battle/combat/TargetResolver.ts +++ b/assets/mx/module/battle/combat/TargetResolver.ts @@ -15,7 +15,8 @@ import type { Cell } from '../battlefield/Cell'; import { applyTargetStageStrategy, type TargetStageStrategyOptions } from './TargetStageStrategy'; import { BattleMapSpec } from 'db://assets/script/battle/map/BattleMapSpec'; import { parseLegacyMonsterId, parseMonsterUnitDefId } from 'db://assets/script/battle/config/MonsterHpCalculator'; -import { isHeroWeaponTarget, isMonsterBasicHeroTarget, isMonsterDefaultEnemyTarget, isPeachTreeTower } from 'db://assets/script/battle/config/BattleHeroTable'; +import { isHeroWeaponTarget, isMonsterBasicHeroTarget, isMonsterDefaultEnemyTarget, isPeachTreeTower, resolveTowerComposeLevel } from 'db://assets/script/battle/config/BattleHeroTable'; +import { getTowerRuinHost } from 'db://assets/script/battle/combat/TowerRuinHost'; import { MonsterDataManager } from 'db://assets/script/manager/MonsterDataManager'; /** @@ -131,6 +132,7 @@ export class TargetResolver { filtered.push(id); } this._addSameSideCaster(ctx, spec, filtered); + this._excludeBoardMaxLevel(filtered, spec); if (filtered.length === 0) return []; @@ -236,6 +238,7 @@ export class TargetResolver { filtered.push(id); } this._addSameSideCaster(ctx, spec, filtered); + this._excludeBoardMaxLevel(filtered, spec); if (filtered.length === 0) return []; @@ -256,6 +259,30 @@ export class TargetResolver { ); } + /** + * params.monsterSkipMaxLevel:从候选中剔除当前等级最高的数字(火 Boss 41101)。 + * 候选全为同级最高时清空,本次不施法。 + * @param filtered 已筛列表(原地改) + * @param spec 索敌 + */ + private _excludeBoardMaxLevel(filtered: EntityId[], spec: TargetSpec): void { + if (!spec.params?.monsterSkipMaxLevel || filtered.length <= 0) return; + let maxLv = 0; + for (let i = 0; i < filtered.length; i++) { + const lv = resolveTowerComposeLevel(this._world, filtered[i]); + if (lv > maxLv) maxLv = lv; + } + if (!(maxLv > 0)) return; + let w = 0; + for (let i = 0; i < filtered.length; i++) { + const id = filtered[i]; + if (resolveTowerComposeLevel(this._world, id) >= maxLv) continue; + filtered[w] = id; + w += 1; + } + filtered.length = w; + } + private _stagePickOptions(ctx: AbilityContext, spec: TargetSpec): TargetStageStrategyOptions { const stageRangePx = Math.max(0, (spec.params?.stageRangePx as number) ?? 0); const posScratch = { x: 0, y: 0 }; @@ -602,7 +629,10 @@ export class TargetResolver { : BattleTeamId.isFriendly(casterTeam, team); if (!match) return false; if (spec.params?.monsterEnemyHero && !isMonsterDefaultEnemyTarget(world, id)) return false; - if (spec.params?.monsterBasicHero && !isMonsterBasicHeroTarget(world, id)) return false; + if (spec.params?.monsterBasicHero) { + if (!isMonsterBasicHeroTarget(world, id)) return false; + if (getTowerRuinHost().isEntityRuined(id)) return false; + } const weaponType = Math.floor(Number(spec.params?.weaponType) || 0); if (weaponType > 0 && !isHeroWeaponTarget(world, id, weaponType)) return false; return true; diff --git a/assets/script/battle/combat/MonsterSkillHost.ts b/assets/script/battle/combat/MonsterSkillHost.ts index 96fee58..1300ee5 100644 --- a/assets/script/battle/combat/MonsterSkillHost.ts +++ b/assets/script/battle/combat/MonsterSkillHost.ts @@ -29,6 +29,7 @@ import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; import { BATTLE_MONSTER_CCCLASS, type IBattleMonsterView } from '../view/IBattleMonsterView'; import { getMonsterStatusText } from '../view/MonsterStatusText'; import { pickSealablePutTiles, sealPlaceableTiles } from './SkillAtkBuffBoardBridge'; +import { getPassiveSkillHost, PassiveResultKind } from './PassiveSkillRuntime'; import { getTowerRuinHost } from './TowerRuinHost'; import { SkillAtkBuffExecutor } from './SkillAtkBuffExecutor'; @@ -82,6 +83,8 @@ const GOURD_TURN_SEC = 0.12; const RAIN_ABOVE_PX = BattleMapSpec.CELL_SIZE * 0.5; /** Skill.skill 类型 5:吞噬本方小兵 */ const EXTRA_DEVOUR = 5; +/** Skill.atk_buff1 类型 1:眩晕 */ +const BUFF_STUN = 1; /** Skill.atk_buff1 类型 3:移速 */ const BUFF_SPEED = 3; @@ -204,11 +207,13 @@ export function getMonsterSkillHost(): MonsterSkillHost { /** * 出怪绑定 Monster.skill → Ability,承接 monsterSkill cue 播释放动画。 * extra 9:起播时挂干扰迷雾并渐显(无 atk_type=半屏;atk_type=15=主目标格);同时降攻速。 - * atk_buff1 类型 12:技能动画结束后在锁定基础字格播损毁文字并使其失效。 + * atk_buff1 类型 12:技能动画结束后在锁定基础字格播损毁文字并使其失效(火 Boss 41101 索敌跳过场上最高等级)。 + * atk_buff1 类型 1 + atk_type=3:动画结束按**当前位置**重扫圈内数字并眩晕(土 Boss 51101;禁攻+格上怪_晕眩)。 + * result=11 且 atk_buff1=12:动画结束后放剑气,命中路径上基础字击倒(金 Boss 11101)。 * atk_buff1 类型 6:技能动画结束后降攻速;无 skill 类型 9 时再挂降雨(敖广)。 * 奔波儿灞:播「技能」同时出池 `葫芦` 挂怪物(预制体默认偏移),播「技能葫芦」并朝向主目标格;同时渐显干扰迷雾,播完回 45° 后回池。 * extra 5:释放同时吸入本方周围小兵,动画结束吞噬并加血上限。 - * target_stage=2:技能动画结束后给射程内同侧友军怪上 atk_buff(太白移速+回血,含自身)。 + * target_stage=2:技能动画结束后给射程内同侧友军怪上 atk_buff(太白移速+回血、水 Boss 31101 移速,含自身;依赖 Skill.range>0)。 * atk_buff1 类型 9:施法起播时出千里眼技能瞄向目标格并封为可开挖。 */ export class MonsterSkillHost { @@ -425,12 +430,6 @@ export class MonsterSkillHost { } const animName = String(event.params?.animation ?? '').trim(); - if (!animName) { - console.log('[MonsterSkillHost] Skill.animation 为空', event.abilityId); - ctx.finish(); - return; - } - const node = SkillPresentationEnv.getMonsterNode(event.caster); const view = node?.isValid ? node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null @@ -438,31 +437,46 @@ export class MonsterSkillHost { const sk = view?.anim; if (!node?.isValid || !view || !sk?.isValid) { console.log('[MonsterSkillHost] 怪物节点未就绪', event.caster, event.abilityId); - ctx.finish(); + this._finishPresent({ entity: event.caster, ctx, wait: 0, moveLocked: false }); return; } - const deathAnim = MonsterDataManager.resolveDeathAnim(view.Data ?? null); + // 无 Data 时不查死亡轨(避免 resolveDeathAnim 无配表刷屏);仅已绑定配表时拦截死亡态 + const deathAnim = view.Data ? MonsterDataManager.resolveDeathAnim(view.Data) : ''; if (sk.animation === '死亡' || (deathAnim && sk.animation === deathAnim)) { ctx.finish(); return; } - if (BattleSpinePlayback.getAnimDuration(sk, animName) == null) { - console.log('[MonsterSkillHost] 骨骼无技能动画', animName, node.name, event.abilityId); - ctx.finish(); - return; + + let playAnim = animName; + if (!playAnim) { + console.log('[MonsterSkillHost] Skill.animation 为空', event.abilityId); + } else if (BattleSpinePlayback.getAnimDuration(sk, playAnim) == null) { + // 五行 Boss 资源轨多为「攻击」,表若写「攻击1」则告警后改播「攻击」 + if (playAnim === '攻击1' && BattleSpinePlayback.getAnimDuration(sk, '攻击') != null) { + console.log('[MonsterSkillHost] 骨骼无', playAnim, '改播攻击', node.name, event.abilityId); + playAnim = '攻击'; + } else { + console.log('[MonsterSkillHost] 骨骼无技能动画', playAnim, node.name, event.abilityId); + playAnim = ''; + } } - const delay = BattleSpinePlayback.resolveLogicalCompleteDelay(sk, animName, 1); - // Boss/怪物主动技能:动画倍速固定 1(不跟攻速) - view.playSpineAnim?.(animName, false); + let wait = 0; + if (playAnim) { + const delay = BattleSpinePlayback.resolveLogicalCompleteDelay(sk, playAnim, 1); + // Boss/怪物主动技能:动画倍速固定 1(不跟攻速) + view.playSpineAnim?.(playAnim, false); + wait = delay != null && delay > 0 ? delay : 0; + } this._playGourdIfNeeded(event.caster, ctx); - const wait = delay != null && delay > 0 ? delay : 0; this._spawnFogIfNeeded(ctx, wait > 0 ? wait : FOG_FADE_SEC); // 迷雾起播即降攻速,避免动画结束前点面板看不到 if (Math.floor(Number(ctx.params.skillType ?? 0) || 0) === EXTRA_FOG) { this._spawnRainIfNeeded(ctx); } getMonsterStatusText().play(event.caster, '释放技能'); + // result 11 剑气与攻击同帧起飞(勿等动画结束,否则观感像没放) + this._fireSkillResultIfNeeded(ctx); this._beginDevour(ctx, wait); this._beginEyeIfNeeded(ctx, wait); if (!(wait > 0)) { @@ -490,22 +504,88 @@ export class MonsterSkillHost { ? node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null : null; if (view?.playWalk && view.anim?.isValid) { - const deathAnim = MonsterDataManager.resolveDeathAnim(view.Data ?? null); + const deathAnim = view.Data ? MonsterDataManager.resolveDeathAnim(view.Data) : ''; const cur = String(view.anim.animation ?? ''); if (cur !== '死亡' && !(deathAnim && cur === deathAnim)) { view.playWalk(); } } this._consumeDevour(item.entity); - this._spawnDestroyIfNeeded(ctx); + if (!this._hasSwordWaveRuin(ctx)) { + this._spawnDestroyIfNeeded(ctx); + } if (Math.floor(Number(ctx.params.skillType ?? 0) || 0) !== EXTRA_FOG) { this._spawnRainIfNeeded(ctx); } this._buffAlliesIfNeeded(ctx); + this._stunAroundIfNeeded(ctx); while (ctx.isAlive && ctx.hit()) { /* 召唤 / atk_buff */ } ctx.finish(); } + /** + * atk_type=3 + atk_buff1 类型1:按施法者**当前**位置为圆心重扫范围内敌方数字并眩晕。 + * @param ctx 本次施法 + */ + private _stunAroundIfNeeded(ctx: SkillPresentationContext): void { + const buff1 = String(ctx.params.buff1 ?? '').trim(); + const buff2 = String(ctx.params.buff2 ?? '').trim(); + const head = Math.floor(Number(buff1.split(',')[0]) || 0); + if (head !== BUFF_STUN) return; + const atkHead = Math.floor(Number(ctx.params.atkTypeHead ?? 0) || 0); + if (atkHead !== 3) return; + const battle = this._battle; + if (!battle) { + console.log('[MonsterSkillHost] 未绑定,无法圈晕', ctx.abilityId); + return; + } + const def = battle.session.config.getAbility(ctx.abilityId); + if (!def?.targeting) { + console.log('[MonsterSkillHost] 圈晕缺少 targeting', ctx.abilityId); + return; + } + const abilityCtx = createAbilityContext(battle.session, ctx.caster, ctx.abilityId); + abilityCtx.level = ctx.level; + const ids = battle.session.targets.resolve(abilityCtx, def.targeting); + if (ids.length <= 0) { + console.log('[MonsterSkillHost] 当前位置圈内无可晕数字', ctx.abilityId); + return; + } + for (let i = 0; i < ids.length; i++) { + SkillAtkBuffExecutor.applyPair(battle.session, abilityCtx, ids[i], buff1, buff2); + } + } + + /** + * 施法起播按 Skill.result 结算(金 Boss 等:result 11 剑气)。 + * @param ctx 本次施法 + */ + private _fireSkillResultIfNeeded(ctx: SkillPresentationContext): void { + const skillId = Math.floor(Number(ctx.params.skillId) || 0); + const session = this._battle?.session; + if (!session || !(skillId > 0)) return; + const row = gg.data.table.getTableData(TableNames.Skill, skillId); + if (!String(row?.result ?? '').trim()) return; + const target = ctx.targets[0] ?? 0; + getPassiveSkillHost().applySkillResult(session, ctx.caster, target, skillId); + } + + /** + * result=11 且 atk_buff1=12:损毁改由剑气命中路径结算。 + * @param ctx 本次施法 + */ + private _hasSwordWaveRuin(ctx: SkillPresentationContext): boolean { + const skillId = Math.floor(Number(ctx.params.skillId) || 0); + if (!(skillId > 0)) return false; + const row = gg.data.table.getTableData(TableNames.Skill, skillId); + const result = String(row?.result ?? '').trim(); + if (!result) return false; + const kind = Math.floor(Number(result.split(',')[0]) || 0); + if (kind !== PassiveResultKind.SwordWave) return false; + const buffHead = Math.floor(Number(String(ctx.params.buff1 ?? '').split(',')[0]) || 0); + return buffHead === BUFF_DESTROY; + } + /** * 取消指定实体未结束的表现。 * @param entityId 施法者 diff --git a/assets/script/battle/combat/PassiveSkillRuntime.ts b/assets/script/battle/combat/PassiveSkillRuntime.ts index e44ee10..926f620 100644 --- a/assets/script/battle/combat/PassiveSkillRuntime.ts +++ b/assets/script/battle/combat/PassiveSkillRuntime.ts @@ -16,10 +16,12 @@ import { import type { EntityId } from 'db://assets/mx/module/ecs'; import { TableNames } from '../../game/ConfigTableData'; import { SkillAbilityBuilder } from '../config/baseAtk/SkillAbilityBuilder'; +import { isMonsterBasicHeroTarget } from '../config/BattleHeroTable'; import { applySkillTableKnockback } from './AtkHookEffects'; import { tryExecuteByThresholdSession, tryInstantKillSession } from './ExecuteRules'; import { applySkillAtkBuffBoardEffect } from './SkillAtkBuffBoardBridge'; import { SkillAtkBuffExecutor } from './SkillAtkBuffExecutor'; +import { getTowerRuinHost } from './TowerRuinHost'; /** 被动 result 10 覆盖普攻(由装载服务实现,避免本文件再引用 Loadout) */ export interface IPassiveAtkOverrideHost { @@ -452,6 +454,8 @@ interface SwordWavePending { uy: number; /** 伤害倍率 */ scale: number; + /** atk_buff1=12 时命中基础字损毁概率万分比;<0 不损毁 */ + ruinWan: number; /** 距放出(秒) */ wait: number; } @@ -474,6 +478,8 @@ interface SwordWaveFlight { speed: number; /** 伤害倍率 */ scale: number; + /** atk_buff1=12 时命中基础字损毁概率万分比;<0 不损毁 */ + ruinWan: number; /** 已命中实体(每目标一次) */ hitIds: EntityId[]; } @@ -716,6 +722,34 @@ export class PassiveSkillHost { return idx >= 0 ? this._binds[idx].list : []; } + /** + * 按 Skill.result 执行(怪物主动技等外部触发)。当前支持 result 11 剑气。 + * @param session 战斗会话 + * @param caster 施法者 + * @param target 主目标(朝向优先) + * @param skillId Skill.id + */ + applySkillResult(session: BattleSession, caster: EntityId, target: EntityId, skillId: number): void { + this._session = session; + if (!(skillId > 0) || !session.world.isAlive(caster)) return; + const row = gg.data.table.getTableData(TableNames.Skill, skillId); + if (!row) { + console.log(`[PassiveSkill] applySkillResult 缺表 skill=${skillId}`); + return; + } + const result = String(row.result ?? '').trim(); + if (!result) return; + const parts = result.split(','); + const kind = Math.floor(Number(parts[0]) || 0); + const p: number[] = []; + for (let i = 1; i < parts.length; i++) p.push(Number(parts[i])); + if (kind === PassiveResultKind.SwordWave) { + this._applySwordWave(session, caster, target, skillId, p); + return; + } + console.log(`[PassiveSkill] applySkillResult 未支持 kind=${kind} skill=${skillId}`); + } + /** * 推进冷却、攻速层数、毒跳、剑阵与火球。 * @param dt 逻辑秒 @@ -1409,20 +1443,38 @@ export class PassiveSkillHost { } /** - * 玉帝刀光动画:轩辕剑蓝、吴钩双剑红。 + * 玉帝刀光动画:轩辕剑蓝、吴钩双剑红;无名默认金。 * @param skillId 被动 Skill.id */ private _swordWaveAnim(skillId: number): string { const row = gg.data.table.getTableData(TableNames.Skill, skillId); const name = String(row?.name ?? '').trim(); + if (!name) return SWORD_WAVE_ANIM_BLUE; if (name === '吴钩双剑') return SWORD_WAVE_ANIM_RED; if (name === '轩辕剑') return SWORD_WAVE_ANIM_BLUE; console.log(`[PassiveSkill] result 11 未配置剑气色 skill=${skillId} name=${name}`); return SWORD_WAVE_ANIM_BLUE; } + /** + * Skill.atk_buff1 类型 12 的损毁概率万分比;非 12 返回 -1。 + * @param skillId Skill.id + */ + private _swordWaveRuinWan(skillId: number): number { + const row = gg.data.table.getTableData(TableNames.Skill, skillId); + const parts = String(row?.atk_buff1 ?? '').split(','); + if (Math.floor(Number(parts[0]) || 0) !== 12) return -1; + const wan = Number(parts[1]); + if (!Number.isFinite(wan) || wan < 0) { + console.log(`[PassiveSkill] result 11 损毁概率无效 skill=${skillId}`, row?.atk_buff1); + return -1; + } + return wan; + } + /** * 直线剑气:按间隔连发 count 道,优先各朝不同敌人;敌数不足则循环复用。 + * 系数可为 0(仅表现/损毁);atk_buff1=12 时命中基础字击倒。 * @param session 战斗会话 * @param caster 施法者 * @param target 触发主目标(优先作为第一道朝向) @@ -1433,10 +1485,11 @@ export class PassiveSkillHost { const count = Math.floor(p[0]); const interval = p[1]; const coeffWan = p[2]; - if (!(count > 0) || !(interval >= 0) || !(coeffWan > 0)) { + if (!(count > 0) || !(interval >= 0) || !Number.isFinite(coeffWan) || coeffWan < 0) { console.log(`[PassiveSkill] result 11 参数无效 skill=${skillId}`, p.join(',')); return; } + // 系数 0:仅表现/航迹击倒(如金 Boss 11101),合法,不刷日志 const origin = this._logicPos(session, caster); if (!origin) { console.log(`[PassiveSkill] result 11 无起点 skill=${skillId}`); @@ -1444,25 +1497,33 @@ export class PassiveSkillHost { } const picks: EntityId[] = []; this._collectHostileIds(session, caster, picks); - if (picks.length === 0) { + if (picks.length === 0 && !(target > 0)) { console.log(`[PassiveSkill] result 11 无敌可朝向 skill=${skillId}`); return; } // 触发主目标优先,其余打乱;敌数不足时按序循环复用 const ordered: EntityId[] = []; if (target > 0) { + let inPicks = false; for (let i = 0; i < picks.length; i++) { if (picks[i] === target) { ordered.push(target); picks[i] = picks[picks.length - 1]; picks.length -= 1; + inPicks = true; break; } } + if (!inPicks && session.world.isAlive(target)) ordered.push(target); } this._shuffleIds(session, picks); for (let i = 0; i < picks.length; i++) ordered.push(picks[i]); + if (ordered.length === 0) { + console.log(`[PassiveSkill] result 11 无敌可朝向 skill=${skillId}`); + return; + } const scale = coeffWan / 10000; + const ruinWan = this._swordWaveRuinWan(skillId); let spawned = 0; for (let i = 0; i < count; i++) { let ux = 0; @@ -1492,6 +1553,7 @@ export class PassiveSkillHost { ux, uy, scale, + ruinWan, wait: spawned * interval, }); spawned += 1; @@ -1580,6 +1642,7 @@ export class PassiveSkillHost { uy: p.uy, speed: SWORD_WAVE_SPEED, scale: p.scale, + ruinWan: p.ruinWan, hitIds: [], }; this._waves.push(flight); @@ -1657,7 +1720,10 @@ export class PassiveSkillHost { const ly = rx * w.uy - ry * w.ux; if (Math.abs(lx) > SWORD_WAVE_HALF_LEN || Math.abs(ly) > SWORD_WAVE_HALF_WID) continue; w.hitIds.push(id); - this._dealResultDamage(session, w.caster, id, w.skillId, w.scale, SWORD_WAVE_DMG); + if (w.scale > 0) { + this._dealResultDamage(session, w.caster, id, w.skillId, w.scale, SWORD_WAVE_DMG); + } + this._ruinSwordWaveHit(session, w, id); } session.events.flush(); } finally { @@ -1665,6 +1731,25 @@ export class PassiveSkillHost { } } + /** + * 剑气命中:atk_buff1=12 时击倒基础字。 + * @param session 战斗会话 + * @param w 在飞剑气 + * @param id 命中实体 + */ + private _ruinSwordWaveHit(session: BattleSession, w: SwordWaveFlight, id: EntityId): void { + if (!(w.ruinWan >= 0)) return; + if (!isMonsterBasicHeroTarget(session.world, id)) return; + if (getTowerRuinHost().isEntityRuined(id)) return; + if (w.ruinWan < 10000 && session.rng.next() * 10000 >= w.ruinWan) return; + const team = session.world.getComponent(w.caster, TeamComp); + applySkillAtkBuffBoardEffect(session, { + kind: 12, + ownerPlayerId: team?.ownerId ?? team?.team ?? 0, + target: id, + }); + } + /** * 该剑气是否已命中过该实体。 * @param w 在飞剑气 diff --git a/assets/script/battle/config/BattleHeroTable.ts b/assets/script/battle/config/BattleHeroTable.ts index 881baf8..2cc67a2 100644 --- a/assets/script/battle/config/BattleHeroTable.ts +++ b/assets/script/battle/config/BattleHeroTable.ts @@ -129,13 +129,27 @@ function resolveTowerHero(world: World, entityId: EntityId): ITableHero | null { } /** - * 怪物技能 target_stage=1 的默认敌人:敌方阵营、非组合武将(herotype≠2)、非多格组合字。 + * 塔合成/显示等级(优先 UnitMetaComp.level,否则 Hero.level)。 + * @param world 战斗世界 + * @param entityId 实体 + */ +export function resolveTowerComposeLevel(world: World, entityId: EntityId): number { + const fromMeta = Math.floor(Number(world.getComponent(entityId, UnitMetaComp)?.level) || 0); + if (fromMeta > 0) return fromMeta; + const hero = resolveTowerHero(world, entityId); + return Math.max(0, Math.floor(Number(hero?.level) || 0)); +} + +/** + * 怪物技能 target_stage=1 的默认敌人:敌方阵营五行单字(herotype 1–5 金木水火土),排除多格组合。 + * 新表 herotype 表示五行,不再用旧口径「herotype≠2=非组合神将」。 * @param world 战斗世界 * @param entityId 候选目标 */ export function isMonsterDefaultEnemyTarget(world: World, entityId: EntityId): boolean { + if (isPeachTreeTower(world, entityId)) return false; const hero = resolveTowerHero(world, entityId); - if (hero == null || Number(hero.herotype) === 2) return false; + if (!isElementHero(hero)) return false; const cells = world.getComponent(entityId, GridOccupancyComp)?.cells; return !(cells && cells.length > 1); } @@ -150,15 +164,12 @@ export function isPeachTreeTower(world: World, entityId: EntityId): boolean { } /** - * atk_buff1 类型 12:只锁基础字(herotype=1),不含组合字。 - * 损毁态由调用方用 TowerRuinHost 判定,本表不反向依赖战斗宿主。 + * atk_buff1 类型 12:锁五行单字(非多格)。损毁态由调用方判定,本表不依赖 TowerRuinHost。 * @param world 战斗世界 * @param entityId 候选目标 */ export function isMonsterBasicHeroTarget(world: World, entityId: EntityId): boolean { - if (!isMonsterDefaultEnemyTarget(world, entityId)) return false; - const hero = resolveTowerHero(world, entityId); - return hero != null && isBasicHero(hero); + return isMonsterDefaultEnemyTarget(world, entityId); } /** diff --git a/assets/script/battle/config/TableBattleConfigProvider.ts b/assets/script/battle/config/TableBattleConfigProvider.ts index d6f7435..f3522d2 100644 --- a/assets/script/battle/config/TableBattleConfigProvider.ts +++ b/assets/script/battle/config/TableBattleConfigProvider.ts @@ -223,6 +223,10 @@ export class TableBattleConfigProvider implements IConfigProvider { else if (row.type === 3) tags.push(EntityTag.Elite); const prefabName = ChapterLevelBuilder.resolveMonsterPrefabFromRow(row); + const atkRange = Math.max(0, Math.floor(Number(row.atkdistance) || 0)); + if (!(atkRange > 0)) { + console.log('[TableBattleConfigProvider] Monster.atkdistance 无效', m, row.atkdistance); + } const unit: UnitDef = { id: unitDefId, name: prefabName ?? `monster_${m}`, @@ -232,7 +236,7 @@ export class TableBattleConfigProvider implements IConfigProvider { combat: { attack: (row as ITableMonster & { attack?: number }).attack ?? 1, attackSpeed: (row as ITableMonster & { attack_speed?: number }).attack_speed ?? 1, - range: 1, + range: atkRange, critRate: 0, critDamage: 1, }, diff --git a/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts b/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts index d82e86f..96b095a 100644 --- a/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts +++ b/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts @@ -139,6 +139,14 @@ export class SkillAbilityBuilder { targeting.params = { ...(targeting.params ?? {}), monsterEnemyHero: atkHead !== 9 }; if (buffHead === 12) targeting.params.monsterBasicHero = true; if (buffHead === 6) targeting.params.monsterRandomPick = true; + // 火 Boss 41101:写死不锁当前场上等级最高的数字 + if (row.id === 41101) targeting.params.monsterSkipMaxLevel = true; + // Skill.range<=0 的单体锁敌:全图索敌(金/火 Boss 等),避免仅靠 Monster.atkdistance 近战圈永远放不出 + if (this._rangePx(row) <= 0 && (atkHead === 0 || atkHead === 1)) { + targeting.selector = 'AllEnemies'; + delete targeting.rangeRef; + if (targeting.params) delete targeting.params.range; + } } if (kind === 'monster' && (row.target_stage || 1) === 2) { targeting.params = { ...(targeting.params ?? {}), monsterSameSide: true }; @@ -423,7 +431,7 @@ export class SkillAbilityBuilder { } /** - * 怪物 atk_buff1/2(类型 6/9/12、target_stage=2 由 Host 处理全部目标,不走此阶段)。 + * 怪物 atk_buff1/2(类型 1+atk_type3 圈晕、6/9/12、target_stage=2 由 Host 处理,不走此阶段)。 * @param row Skill 行 */ private static _monsterBuffStage(row: ITableSkill): EffectSpec[] | null { @@ -432,6 +440,8 @@ export class SkillAbilityBuilder { if (!buff1 && !buff2) return null; const head = this._splitNums(buff1)[0] ?? 0; if (head === 6 || head === 9 || head === 12) return null; + // 圈晕:动画结束按当前位置重扫,不锁开战目标 + if (head === 1 && this._atkHead(row) === 3) return null; if ((row.target_stage || 1) === 2) return null; return [{ type: 'ApplySkillAtkBuff', params: { buff1, buff2 } }]; } diff --git a/assets/script/battle/skill/SkillController.ts b/assets/script/battle/skill/SkillController.ts index 3834cb0..2c2daaf 100644 --- a/assets/script/battle/skill/SkillController.ts +++ b/assets/script/battle/skill/SkillController.ts @@ -34,6 +34,8 @@ const { ccclass } = _decorator; const PRESENTATION_SKILL_BIND_MAX_RETRIES = 60; const CONFUSION_BUFF_ID = 'debuff_8'; +/** 眩晕(土 Boss 51101 等) */ +const STUN_BUFF_ID = 'debuff_4'; /** 单个玩家的技能表现控制器(监听战斗事件 → 驱动 HeroActor Skill) */ @ccclass('SkillController') @@ -203,19 +205,25 @@ export class SkillController extends Component { case BattleEventType.BuffApplied: if (event.buffId === CONFUSION_BUFF_ID) { this._setTowerConfusionFx(event.target, true); + } else if (event.buffId === STUN_BUFF_ID) { + this._setTowerStunFx(event.target, true); } break; case BattleEventType.BuffRemoved: if (event.buffId === CONFUSION_BUFF_ID) { this._setTowerConfusionFx(event.target, false); + } else if (event.buffId === STUN_BUFF_ID) { + this._setTowerStunFx(event.target, false); } break; case BattleEventType.EntityKilled: case BattleEventType.UnitDespawned: if (event.type === BattleEventType.EntityKilled) { this._setTowerConfusionFx(event.victim, false); + this._setTowerStunFx(event.victim, false); } else { this._setTowerConfusionFx(event.entity, false); + this._setTowerStunFx(event.entity, false); } break; } @@ -229,6 +237,14 @@ export class SkillController extends Component { this._textGridAt(gridIndex)?.setConfusionFx(on); } + /** 眩晕:塔格挂「怪_晕眩」,禁攻由 AbilitySystem debuff_4 判定 */ + private _setTowerStunFx(entityId: EntityId, on: boolean): void { + if (!this._ownsEntity(entityId)) return; + const gridIndex = this._resolveGridIndex(entityId); + if (gridIndex == null) return; + this._textGridAt(gridIndex)?.setStunFx(on); + } + private _playPresentationCue(event: Extract): void { const session = this._match?.session; if (!session) return; diff --git a/assets/script/battle/view/CocosBattleCombatPresenter.ts b/assets/script/battle/view/CocosBattleCombatPresenter.ts index a1cd2f4..7d9b231 100644 --- a/assets/script/battle/view/CocosBattleCombatPresenter.ts +++ b/assets/script/battle/view/CocosBattleCombatPresenter.ts @@ -1,7 +1,7 @@ import { Node, sp, UITransform, Vec3, view } from 'cc'; import type { BattleEvent, EntityId } from 'db://assets/mx/module/battle'; import { BATTLE_EVENT_ALL, BattleEventType } from 'db://assets/mx/module/battle'; -import { AttributeComp, HealthComp } from 'db://assets/mx/module/battle/ecs/Components'; +import { AttributeComp, HealthComp, TransformComp } from 'db://assets/mx/module/battle/ecs/Components'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import type { ProjectBattleHandle } from '../runtime/BattleRuntime'; import type { CocosBattleMonsterPresenter } from './CocosBattleMonsterPresenter'; @@ -32,6 +32,7 @@ import { type SwordWaveCue, type LightningChainCue, } from '../combat/PassiveSkillRuntime'; +import { logicalWorldToCocos } from '../bridge/CocosBattleMovementBridge'; import { BattleMapSpec } from '../map/BattleMapSpec'; import { traceCombatDamage } from 'db://assets/mx/tools/CombatDamageTrace'; @@ -724,17 +725,11 @@ export class CocosBattleCombatPresenter { } /** - * 起点:优先塔格心,否则逻辑坐标。 + * 起点:实体世界坐标 → 塔格心 → 逻辑坐标换算。 * @param cue 逻辑层 cue */ private _lineDragonOrigin(cue: LineDragonCue): Vec3 | null { - const grid = SkillPresentationEnv.getCasterGridCenter(cue.caster); - if (grid) return grid; - if (Number.isFinite(cue.originX) && Number.isFinite(cue.originY)) { - this._dragonFrom.set(cue.originX, cue.originY, 0); - return this._dragonFrom; - } - return null; + return this._passiveProjectileOrigin(cue.caster, cue.originX, cue.originY); } /** @@ -863,17 +858,39 @@ export class CocosBattleCombatPresenter { } /** - * 剑气起点:优先塔格心,否则逻辑坐标。 + * 剑气起点:实体世界坐标 → 塔格心 → 逻辑坐标换算。 * @param cue 逻辑层 cue */ private _swordWaveOrigin(cue: SwordWaveCue): Vec3 | null { - const grid = SkillPresentationEnv.getCasterGridCenter(cue.caster); + return this._passiveProjectileOrigin(cue.caster, cue.originX, cue.originY); + } + + /** + * 被动弹道起点(怪/塔)。禁止把逻辑坐标当 Cocos 世界坐标用。 + * @param caster 施法者 + * @param logicX 逻辑 X + * @param logicY 逻辑 Y + */ + private _passiveProjectileOrigin(caster: EntityId, logicX: number, logicY: number): Vec3 | null { + if (this._entityWorld(caster, this._dragonFrom)) return this._dragonFrom; + const grid = SkillPresentationEnv.getCasterGridCenter(caster); if (grid) return grid; - if (Number.isFinite(cue.originX) && Number.isFinite(cue.originY)) { - this._dragonFrom.set(cue.originX, cue.originY, 0); - return this._dragonFrom; + if (!(Number.isFinite(logicX) && Number.isFinite(logicY))) return null; + const session = this._battle?.session; + const map = SkillPresentationEnv.getMap(); + if (!session || !map) { + console.log('[CombatPresenter] 被动弹道无法换算逻辑坐标', caster); + return null; } - return null; + const tf = session.world.getComponent(caster, TransformComp); + if (!tf) { + console.log('[CombatPresenter] 被动弹道施法者无 Transform', caster); + return null; + } + const cocos = logicalWorldToCocos(session, map, tf.row, tf.col, logicX, logicY); + if (!cocos) return null; + this._dragonFrom.set(cocos.x, cocos.y, cocos.z); + return this._dragonFrom; } /** diff --git a/assets/script/battle/view/CocosBattleMonsterPresenter.ts b/assets/script/battle/view/CocosBattleMonsterPresenter.ts index ec0a7af..f9bf4d9 100644 --- a/assets/script/battle/view/CocosBattleMonsterPresenter.ts +++ b/assets/script/battle/view/CocosBattleMonsterPresenter.ts @@ -116,6 +116,12 @@ const MONSTER_SHADOW_SCALE: { [prefabName: string]: number } = { Boss水: 1, Boss火: 1, Boss土: 1, + /** 表 icon 写 BOSS金 时与 Boss金 同缩放 */ + BOSS金: 1, + BOSS木: 1, + BOSS水: 1, + BOSS火: 1, + BOSS土: 1, 兵: 0.5, 将: 0.5, 帅: 0.55, @@ -1636,32 +1642,19 @@ export class CocosBattleMonsterPresenter { * @param entityId 实体 * @param spawnPort 出怪口 */ + /** + * 本项目怪物不设皮肤,仅挂行军 Spine 缓存。 + * @param monster 怪视图 + * @param _monsterData 配表(保留签名) + * @param _entityId 实体 + * @param _spawnPort 出怪口 + */ private _applyCampSkinToMonster( monster: IBattleMonsterView, - monsterData: ITableMonster, - entityId?: EntityId, - spawnPort?: number, + _monsterData: ITableMonster, + _entityId?: EntityId, + _spawnPort?: number, ): void { - // 五行新怪 / 新 Boss:仅 default 皮,不 setSkin、不区分阵营 - if (MonsterDataManager.skipsBattleSpineSkin(monsterData)) { - monster.applyMarchSpineCache?.(); - return; - } - const anim = monster.anim; - if (!anim?.isValid) return; - const override = this._resolveSpineSkinOverride(entityId); - if (override) { - this._setSkeletonCampSkin(anim, override, monsterData.id, spawnPort, this._isEnemyCampMonster(entityId, spawnPort)); - monster.applyMarchSpineCache?.(); - return; - } - const enemyCamp = this._isEnemyCampMonster(entityId, spawnPort); - const preferred = MonsterDataManager.resolveBattleSpineSkin(monsterData, enemyCamp); - if (!preferred) { - console.error('[CocosBattleMonsterPresenter] 阵营皮肤名为空', monsterData.id, 'port=', spawnPort); - return; - } - this._setSkeletonCampSkin(anim, preferred, monsterData.id, spawnPort, enemyCamp); monster.applyMarchSpineCache?.(); } @@ -1753,26 +1746,14 @@ export class CocosBattleMonsterPresenter { ); } - /** @deprecated 保留给挂皮流程;正式套皮见 _applyCampSkinToMonster */ + /** @deprecated 本项目怪物不设皮肤 */ private _applyMonsterSpineSkin( - anim: sp.Skeleton, - monsterData: ITableMonster, - entityId?: EntityId, - spawnPort?: number, + _anim: sp.Skeleton, + _monsterData: ITableMonster, + _entityId?: EntityId, + _spawnPort?: number, ): void { - if (MonsterDataManager.skipsBattleSpineSkin(monsterData)) return; - const override = this._resolveSpineSkinOverride(entityId); - if (override) { - this._setSkeletonCampSkin(anim, override, monsterData.id, spawnPort, this._isEnemyCampMonster(entityId, spawnPort)); - return; - } - const enemyCamp = this._isEnemyCampMonster(entityId, spawnPort); - const preferred = MonsterDataManager.resolveBattleSpineSkin(monsterData, enemyCamp); - if (!preferred) { - console.error('[CocosBattleMonsterPresenter] 阵营皮肤名为空', monsterData.id, 'port=', spawnPort); - return; - } - this._setSkeletonCampSkin(anim, preferred, monsterData.id, spawnPort, enemyCamp); + /* no-op */ } /** 配表 icon → 预制体文件名 */ diff --git a/assets/script/battle/view/TextGrid.ts b/assets/script/battle/view/TextGrid.ts index 587bc16..d43cce8 100644 --- a/assets/script/battle/view/TextGrid.ts +++ b/assets/script/battle/view/TextGrid.ts @@ -181,6 +181,8 @@ export class TextGrid extends Component { private _sleepFx: Node | null = null; /** 混乱(debuff_8)特效 */ private _confusionFx: Node | null = null; + /** 眩晕(debuff_4)特效 */ + private _stunFx: Node | null = null; /** attachBattleBuffFx 挂载的节点(施法幻魂镜等) */ private readonly _attachedBattleBuffFx: Node[] = []; /** @@ -454,6 +456,65 @@ export class TextGrid extends Component { gg.res.putNode(node); } + /** + * 土 Boss 等:格子上挂「怪_晕眩」。 + * @param visible 是否显示 + */ + setStunFx(visible: boolean): void { + if (!visible) { + this.clearStunFx(); + return; + } + if (!this.node?.isValid) return; + if (this._stunFx?.isValid) { + this._stunFx.active = true; + this._stunFx.setSiblingIndex(this.node.children.length - 1); + this._ensureDragSurface(); + return; + } + let node: Node | null = null; + try { + node = gg.res.getNode('prefab/', '怪_晕眩', GBundle.BattleGameMonster); + } catch (err) { + console.log('[TextGrid] 怪_晕眩 getNode 失败', String(err)); + return; + } + if (!node?.isValid) { + console.log('[TextGrid] 怪_晕眩预制体未加载'); + return; + } + node.parent = this.node; + node.setPosition(0, BattleMapSpec.CELL_SIZE * 0.25, 0); + node.active = true; + node.layer = this.node.layer; + node.setSiblingIndex(this.node.children.length - 1); + this._stunFx = node; + const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); + if (!sk?.isValid) { + console.log('[TextGrid] 怪_晕眩缺少 Skeleton'); + this.clearStunFx(); + return; + } + sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME); + if (!BattleSpinePlayback.play(sk, 'animation', { loop: true })) { + console.log('[TextGrid] 怪_晕眩无动画 animation'); + this.clearStunFx(); + return; + } + this._ensureDragSurface(); + } + + /** 回收眩晕特效 */ + clearStunFx(): void { + const node = this._stunFx; + this._stunFx = null; + if (!node?.isValid) return; + const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); + BattleSpinePlayback.clearPendingComplete(sk); + BattleSpinePlayback.resetTracks(sk); + gg.res.putNode(node); + } + /** 多格组合:特效锚点偏到占格几何中心(水平偏 X,竖直偏 Y) */ private _resolveComposeSpineOffset(): { x: number; y: number } { const composition = this._getComposition(); @@ -861,6 +922,7 @@ export class TextGrid extends Component { this.clearPositiveBuffFx(); this.clearSleepFx(); this.clearConfusionFx(); + this.clearStunFx(); this.clearAttachedBattleBuffFx(); this.resetNodeTransform(); this.resetSkinPresentation(); @@ -918,6 +980,7 @@ export class TextGrid extends Component { this.clearPositiveBuffFx(); this.clearSleepFx(); this.clearConfusionFx(); + this.clearStunFx(); this.clearAttachedBattleBuffFx(); this._releaseCanUpEff(); this._releaseTrails(); diff --git a/assets/script/manager/MonsterDataManager.ts b/assets/script/manager/MonsterDataManager.ts index e00a38b..5120bb0 100644 --- a/assets/script/manager/MonsterDataManager.ts +++ b/assets/script/manager/MonsterDataManager.ts @@ -35,13 +35,18 @@ const ELEMENT_MARCH_BY_ICON: Readonly> = { 小怪土: '土小怪', }; -/** 新 Boss 预制体 icon(仅 default 皮,行进=走路) */ +/** 新 Boss 预制体 icon(仅 default 皮,行进=走路;兼容表写 BOSS金 / 资源 Boss金) */ const NEW_BOSS_ICONS: Readonly> = { Boss金: true, Boss木: true, Boss水: true, Boss火: true, Boss土: true, + BOSS金: true, + BOSS木: true, + BOSS水: true, + BOSS火: true, + BOSS土: true, }; /** 怪物表数据管理 */ @@ -61,11 +66,14 @@ export class MonsterDataManager { } /** - * 是否五行新小怪(不设皮肤、不区分阵营;行进/死亡用系小怪轨)。 + * 是否五行新小怪/召唤(icon 为小怪金…土;不设皮肤;行进/死亡用系小怪轨)。 + * 含 type=3 召唤(如木 Boss 招出的 10004),勿按旧多皮走「走路/小怪木2」。 * @param row Monster 行 */ static isElementSmallMonster(row: ITableMonster | null | undefined): boolean { - if (!row || row.type !== MonsterType.SmallMonster) return false; + if (!row) return false; + const t = Math.floor(Number(row.type)); + if (t !== MonsterType.SmallMonster && t !== MonsterType.SummonMonster) return false; const icon = String(row.icon ?? '').trim(); return !!ELEMENT_MARCH_BY_ICON[icon]; } @@ -81,11 +89,11 @@ export class MonsterDataManager { } /** - * 新怪表现:不 setSkin。 - * @param row Monster 行 + * 本项目全部怪物仅 default 皮,永不 setSkin、不区分阵营。 + * @param _row Monster 行(保留签名供调用方) */ - static skipsBattleSpineSkin(row: ITableMonster | null | undefined): boolean { - return this.isElementSmallMonster(row) || this.isNewElementBoss(row); + static skipsBattleSpineSkin(_row: ITableMonster | null | undefined): boolean { + return true; } /** @@ -125,32 +133,12 @@ export class MonsterDataManager { } /** - * @deprecated 新五行怪不设皮肤;仅旧多皮资源(猴/哮天犬等)仍用。 - * - type1 小怪 / type3 召唤:己方口=`icon`,敌方口=`icon`+`2` - * - type2 Boss:己方口=`皮肤字1_1`,敌方口=`皮肤字2_1` - * @param row Monster 行 - * @param enemyCamp 是否敌方出怪口放出的怪 + * @deprecated 本项目怪物不设皮肤,恒返回空。 + * @param _row Monster 行 + * @param _enemyCamp 是否敌方出怪口 */ - public static resolveBattleSpineSkin(row: ITableMonster, enemyCamp: boolean): string { - if (this.skipsBattleSpineSkin(row)) { - return ''; - } - const icon = String(row.icon ?? '').trim(); - if (row.type === MonsterType.SmallMonster || row.type === MonsterType.SummonMonster) { - if (!icon) { - console.error('[MonsterDataManager] 怪物 icon 为空,无法定皮肤', row.id, row.type); - return ''; - } - return enemyCamp ? `${icon}2` : icon; - } - if (row.type === MonsterType.Boss) { - return enemyCamp ? '皮肤字2_1' : '皮肤字1_1'; - } - if (!icon) { - console.error('[MonsterDataManager] 怪物 icon 为空', row.id, row.type); - return ''; - } - return icon; + public static resolveBattleSpineSkin(_row: ITableMonster, _enemyCamp: boolean): string { + return ''; } /** diff --git a/assets/script/ui/BattleBoss/UIBattleBoss.ts b/assets/script/ui/BattleBoss/UIBattleBoss.ts index bd2d87d..f2a43e9 100644 --- a/assets/script/ui/BattleBoss/UIBattleBoss.ts +++ b/assets/script/ui/BattleBoss/UIBattleBoss.ts @@ -173,14 +173,7 @@ export class UIBattleBoss extends Auto_BattleBoss { if (!anim) { console.error('[UIBattleBoss] 怪物 Spine 缺失', prefabName, row.id); } else { - if (!MonsterDataManager.skipsBattleSpineSkin(row)) { - const skinName = MonsterDataManager.resolveBattleSpineSkin(row, true); - if (!skinName) { - console.error('[UIBattleBoss] Boss 皮肤名为空', row.id); - } else { - anim.setSkin(skinName); - } - } + // 本项目怪物仅 default 皮,不 setSkin const march = MonsterDataManager.resolveMarchAnim(row) || '走路'; if (!BattleSpinePlayback.play(anim, march, { loop: true })) { console.error('[UIBattleBoss] Boss 行进动画起播失败', row.id, march); diff --git a/assets/script/ui/BattleGame/BattleGameExtraBossSkill.ts b/assets/script/ui/BattleGame/BattleGameExtraBossSkill.ts index f3bb580..1b14955 100644 --- a/assets/script/ui/BattleGame/BattleGameExtraBossSkill.ts +++ b/assets/script/ui/BattleGame/BattleGameExtraBossSkill.ts @@ -55,12 +55,11 @@ export class BattleGameExtraBossSkill { const config = gg.battle.config as TableBattleConfigProvider; const unitDefId = config.ensureSpawnMonsterDef(monsterId, route.spawnPort, difficultyRow); - const skin = MonsterDataManager.resolveBattleSpineSkin(row, route.enemyCamp); + // 本项目怪物不设皮肤,不传 spineSkinOverride const entity = battle.session.spawner.spawnExtraEnemy( unitDefId, route.spawnPort, route.targetPlayerId, - skin || undefined, ); if (entity == null) return false; diff --git a/docs/方案-出怪逻辑实现.md b/docs/方案-出怪逻辑实现.md index 0955ddc..2a7e986 100644 --- a/docs/方案-出怪逻辑实现.md +++ b/docs/方案-出怪逻辑实现.md @@ -49,18 +49,18 @@ waveIndex|monsterId,... - **Monster1**:行 = `waveIndex`;列 = `Rank.diffcult` / Level 难度组。 - **血量计算时机**:**出怪当下**套公式 `floor(Monster.hp × hp_multiple × port_hp × Monster1[波次] × 战力修正 × weapon_para)`,不以开局缓存为准。波次怪、营召唤、`atk_type=8`(含哮天犬)、万神幡 Boss 同一套;Monster1 行取**当前波** `waveIndex`。 -#### 表现(五行新怪:不设皮肤、不区分阵营) +#### 表现(全部怪物:不设皮肤、不区分阵营) -Spine 资源仅 `default` 皮;双口同一种表现。 +Spine 资源仅 `default` 皮;双口同一种表现,**禁止 setSkin / 阵营皮后缀**。 | type | 预制体 | 皮肤 | 行进 | 死亡 | |------|--------|------|------|------| -| 1 五行小怪 | `prefab/monster/{icon}`(如 `小怪金`) | **不 setSkin** | `{系}小怪`(如 `金小怪`) | `{系}小怪2`(如 `金小怪2`) | -| 2 新 Boss | `prefab/monster/{icon}`(如 `Boss金`) | **不 setSkin** | `走路` | 按 Boss 资源轨(缺则 error) | +| 1 五行小怪 / 3 同 icon 召唤 | `prefab/monster/{icon}`(如 `小怪金`) | **不 setSkin** | `{系}小怪`(如 `金小怪`) | `{系}小怪2`(如 `金小怪2`) | +| 2 Boss | `prefab/monster/{icon}`(如 `BOSS金`) | **不 setSkin** | `走路`(或资源轨) | 按 Boss 资源轨(缺则 error) | `Monster.icon` → 行进 / 死亡:`小怪金`→`金小怪`/`金小怪2`,木/水/火/土同理。映射失败或动画不存在 → `console.error`,禁止静默兜底。 -实现:`MonsterDataManager.resolveElementMarchAnim` / `resolveElementDeathAnim` + `Monster.playWalk` / 死亡播控;Presenter 对新怪跳过换皮。 +实现:`MonsterDataManager.skipsBattleSpineSkin` 恒 true;`resolveMarchAnim` / `resolveDeathAnim` + `Monster.playWalk`;Presenter / Boss 预览 / 额外 Boss 召唤均不换皮。 #### 营字强化出怪(Hero.id=5) diff --git a/docs/方案-怪物技能.md b/docs/方案-怪物技能.md index 64bbd8b..3db2db7 100644 --- a/docs/方案-怪物技能.md +++ b/docs/方案-怪物技能.md @@ -1,13 +1,13 @@ # 方案:怪物技能 -> 状态:框架已落地;奔波儿灞 30105、牛魔王 30106、六耳 30107、蟹将 30205、龟丞相 30206、敖广 30207、黑无常 30305、白无常 30306、谛听 30307、千里眼 30405、顺风耳 30406、太白 30407 已落地 +> 状态:框架已落地;金 Boss 11101、水 Boss 31101、火 Boss 41101、土 Boss 51101、奔波儿灞 30105、牛魔王 30106、六耳 30107、蟹将 30205、龟丞相 30206、敖广 30207、黑无常 30305、白无常 30306、谛听 30307、千里眼 30405、顺风耳 30406、太白 30407 已落地 > 日期:2026-09-04 > 关联:[方案-战斗技能逻辑层.md](./方案-战斗技能逻辑层.md)、[方案-出怪逻辑实现.md](./方案-出怪逻辑实现.md) ## 1. 目标(本切片) 1. `Monster.skill` 关联 `Skill.id`,出怪后按表进入 OnTick 施法。 -2. 怪物技能 `target_stage=1`(敌人)默认只打 **敌方阵营、Hero.herotype ≠ 2**(非组合英雄 / Weapon2.type≠2 组合字)。无特殊需求时不选 type2。 +2. 怪物技能 `target_stage=1`(敌人)默认只打 **敌方阵营五行单字**(`Hero.herotype` 1–5 金木水火土,且非多格组合)。新表 `herotype` 表示五行,不再用旧「≠2=非组合神将」。 3. 做到播 `Skill.animation`(多为「技能」)并 `finish` 解锁;伤害 / 召唤 / debuff / 冲锋等 **后续逐个核对**。 ## 2. 管线 @@ -39,7 +39,7 @@ UnitSpawned(波次 / 营召唤 / atk_type=8 召唤) | `target_stage` | team | 本切片 | |---|---|---| -| 1 敌人 | enemy | `params.monsterEnemyHero`:只保留 herotype≠2 且非多格组合的塔(`atk_type=9` 除外,要打组合弓将);`atk_buff1` 类型 12 再加 `monsterBasicHero`(仅 herotype=1、**跳过已损毁**);类型 6 再加 `monsterRandomPick`(全屏洗牌后再取 `atk_para` 个数);`atk_type=9` 再加 `weaponType`=将 id(Hero.weapon,4=弓,含组合武将) | +| 1 敌人 | enemy | `params.monsterEnemyHero`:只保留五行单字且非多格(`atk_type=9` 除外);`atk_buff1` 类型 12 再加 `monsterBasicHero`(同口径、**跳过已损毁**);类型 6 再加 `monsterRandomPick`(全屏洗牌后再取 `atk_para` 个数);`atk_type=9` 再加 `weaponType`=将 id(Hero.weapon,4=弓,含组合武将) | | 2 友军 | ally | `params.monsterSameSide`:同一 `ownerId` 且同为敌/己的怪物;`skill` 类型 5 再加 `monsterDevourMinion`(仅小怪/召唤物),`stageStrategy=1` 去重 | | 3 自身 | self | 无 `HasTargetInRange`,到点即放 | @@ -49,6 +49,10 @@ UnitSpawned(波次 / 营召唤 / atk_type=8 召唤) | Skill.id | 怪 | 表现 | |---|---|---| +| 11101 | 金系 Boss | `result=11,1,0,0` + `atk_buff1=12,10000`:播「攻击」后放直线剑气(玉帝刀光),伤害系数 0 仅表现;剑气穿透路径上命中的基础字按类型 12 击倒(禁攻倒下);`Skill.range=0` 单体全图索敌;动画结束不再立刻损毁主目标 | +| 31101 | 水系 Boss | `target_stage=2` 射程 208 `atk_type=3` `atk_buff1=3,10000,3000,3`:播「攻击」后给射程内**同侧友军怪(含自身)**移速 ×1.3 持续 3 秒(身上 `怪_加速`);走太白同源 Host 友军 buff 通路,`Skill.range` 须 >0 | +| 41101 | 火系 Boss | `atk_buff1=12,10000`:播「攻击」后损毁锁定的五行单字(同牛魔王类型12);`Skill.range=0` 全图索敌;**写死不锁当前场上等级最高的数字**(`monsterSkipMaxLevel`;场上仅最高级时不施法) | +| 51101 | 土系 Boss | `atk_type=3` 圈径跟 `Monster.atkdistance`(表 `range=0`)`atk_buff1=1,10000,3`:播「攻击」后按**当时位置**为圆心重扫周围五行单字眩晕 3 秒(`debuff_4` 禁攻 + 格上 `怪_晕眩`;不锁开战目标) | | 30105 | 奔波儿灞 | `atk_type=15` `atk_para=半径,伤害`:射程内锁主目标(**跳过蟠桃树**;`atk_buff1` 类型6 时在非桃候选中随机),再对主目标圆心半径内敌方非组合字结算(半径按配表 83px/格 + 占地扫描);`skill=9,秒` 与葫芦同时在**主目标格**渐显 `干扰迷雾`(**隐藏全屏紫雾槽**,节点缩放 `0.2×0.4`;持续后渐隐);`atk_buff1=6,…` 迷雾起播即圆内降攻速(**不挂降雨**,降雨仅敖广);播「技能」同时出池 `葫芦` 挂怪物(预制体默认偏移),播 `技能葫芦` 并朝向同一目标格,播完回 45° 后回池 | | 30106 | 牛魔王 | `atk_buff1=12,概率`:播「技能」后在锁定基础字格播 `损毁文字`,该字**失效禁攻**(可拖可换位,不激活),字形倒下转 -90°;**不选已损毁、不选组合字**;营损毁后 Host 停出怪,合成升级解除后恢复;**损毁跟字块走**(进背包仍倒下,交换不传染别的字;倒下只拧本格 HeroActor/字形,合成升级一把不得扶正另一把);**己方与敌方字均倒下**;ebg/营底框保持世界角与世界坐标;**合成升级**后回正 | | 30107 | 六耳 | `atk_type=8` `atk_para=10002,4,0.5,2`:播「技能」后在**自身周围**每隔 0.5 秒招 1 只猴(共 4 只),阵营与六耳一致 | @@ -63,7 +67,7 @@ UnitSpawned(波次 / 营召唤 / atk_type=8 召唤) | 30407 | 太白 | `atk_type=3` 射程 291 `atk_buff1=3,10000,2000,3` `atk_buff2=10,3000`:播「技能」后给射程内**同侧友军怪(含自身)**移速 ×1.2 持续 3 秒(身上 `怪_加速`),并回复 30% 最大生命(`怪物加血`) | | 40001 | 哮天犬 | `type=0` 仍按主动绑定;播「技能」后给射程 208 内同侧友军移速 ×1.2 持续 3 秒(`怪_加速`);己方皮 `哮天犬`、敌方皮 `哮天犬2` | -类型 9(封格 atk_buff)不走 `HasTargetInRange`(封格不锁英雄)。`skill` 类型 9 且**无** `atk_type` 时仍半屏迷雾、不锁英雄;`atk_type=15` + `skill` 9 则锁主目标并扩圆。`atk_type=9` 指定将走 `HasTargetInRange` + `weaponType`。类型 12 只锁 `herotype=1` 基础字。类型 6 对每个锁定目标播降雨并加攻速(不走 `ApplySkillAtkBuff`);攻速写 `aspd_add` 万分比,负值降攻速。类型 5 范围内没有可吞小兵则不施法。`target_stage=2` 的 30407 对每个同侧友军上 buff(含自身)。`target_stage=3` 的 30107 / 30305 不锁敌。`ApplySkillAtkBuff` 对 `ctx.targets` 全部目标生效(龟丞相 / 白无常 / 顺风耳)。 +类型 9(封格 atk_buff)不走 `HasTargetInRange`(封格不锁英雄)。`skill` 类型 9 且**无** `atk_type` 时仍半屏迷雾、不锁英雄;`atk_type=15` + `skill` 9 则锁主目标并扩圆。`atk_type=9` 指定将走 `HasTargetInRange` + `weaponType`。类型 12 锁五行单字(非多格、跳过已损毁/蟠桃树);41101 再跳过当前场上最高等级。类型 6 对每个锁定目标播降雨并加攻速(不走 `ApplySkillAtkBuff`);攻速写 `aspd_add` 万分比,负值降攻速。类型 5 范围内没有可吞小兵则不施法。`target_stage=2` 的 30407 对每个同侧友军上 buff(含自身)。`target_stage=3` 的 30107 / 30305 不锁敌。`ApplySkillAtkBuff` 对 `ctx.targets` 全部目标生效(龟丞相 / 白无常 / 顺风耳)。 ## 6. 本切片不做 @@ -79,7 +83,7 @@ UnitSpawned(波次 / 营召唤 / atk_type=8 召唤) | Ability 构建 | `SkillAbilityBuilder.buildMonster` | | Ability id | `Weapon1BattleIds.entityMonsterAbilityId` | | 默认敌人过滤 | `BattleHeroTable.isMonsterDefaultEnemyTarget` → `TargetResolver` | -| 类型12基础字 | `isMonsterBasicHeroTarget` → `TargetResolver`;`TowerRuinHost` 失效至合成升级 | +| 类型12基础字 | `isMonsterBasicHeroTarget` → `TargetResolver`;`TowerRuinHost` 失效至合成升级;火 Boss 41101 另加 `monsterSkipMaxLevel` 剔除场上最高等级 | | atk_type=9 指定将 | `params.weaponType` → `isHeroWeaponTarget`;`monsterEnemyHero` 关闭 | | 类型6随机字 | `params.monsterRandomPick` → `TargetResolver` 洗牌;Host `applyPair` 降攻速;无 skill 类型9 时再播 `降雨`(敖广);奔波儿灞出池 `葫芦` 挂怪物(预制体默认偏移)、与迷雾同瞄主目标格 | | 类型5吞噬 | `params.monsterDevourMinion` → `TargetResolver`;Host 吸入后 `lifecycle.despawn` + `applyMaxHpCoeff` | @@ -87,4 +91,5 @@ UnitSpawned(波次 / 营召唤 / atk_type=8 召唤) | 类型7降级 | `TowerLevelDownHost`:钳制最低 1 级,持续秒内禁合成、可移动,到期恢复;等级字变色 | | 类型12损毁 | `TowerRuinHost`:禁攻、可移动;不选已损毁 / type2 组合字;损毁写在字块上(进背包仍倒下,交换不传染;倒下只拧本格字形,升级一把不得扶正另一把);**己方与敌方 AI 地图字均倒下**;ebg/营底框保持世界角与世界坐标;合成升级解除。营出怪走 `YingCampSummonHost`,须跟 `castBlockPredicate`(损毁/魅惑)停出怪,解除后恢复 | | 类型2友军 buff | `params.monsterSameSide` → `TargetResolver`;Host 扫描射程内同侧怪 `applyPair` + `怪_加速` | -| 表现分发 | `SkillController` 跳过;Host 听 `PresentationCue` | +| 类型1圈晕 | Host 动画结束按当前位置 `targets.resolve` → `applyPair` → `debuff_4`;`AbilitySystem` 禁攻;塔格 `SkillController` → `TextGrid.setStunFx`(`怪_晕眩`) | +| 表现分发 | `SkillController` 跳过怪物 cue;Host 听 `PresentationCue`;塔 debuff 特效仍由 SkillController 接 | diff --git a/docs/方案-战斗技能逻辑层.md b/docs/方案-战斗技能逻辑层.md index 8b676b2..5c16fae 100644 --- a/docs/方案-战斗技能逻辑层.md +++ b/docs/方案-战斗技能逻辑层.md @@ -60,7 +60,7 @@ Hero(合成等级 level) **result 10(改普攻)**:触发后覆盖**该宿主**普攻 Skill 的 `atk_type` / `atk_para`,按实体重建 `ability_norm_{id}_e{entity}`。不改共享配表,避免同技能其他塔串台。例:`10,12,5,12,10000` → `atk_type=12`,`atk_para=5,12,10000`。 -**result 11(直线剑气)**:参数 `数量,间隔秒,伤害系数万分比`。按间隔连发;优先各朝不同敌人(触发主目标优先,其余随机),敌数不足则循环复用同一批目标仍发满数量。穿透且每敌只伤一次;飞出地图边界结束。表现用 `battleHero/prefab/skill/玉帝刀光`,飞出可视区后回池。飞行速度代码常量(表无速度段)。动画:轩辕剑 `剑气蓝`,吴钩双剑 `剑气红`。例:轩辕剑 `11,3,0.3,15000`。 +**result 11(直线剑气)**:参数 `数量,间隔秒,伤害系数万分比`。按间隔连发;优先各朝不同敌人(触发主目标优先,其余随机),敌数不足则循环复用同一批目标仍发满数量。穿透且每敌只伤一次;飞出地图边界结束。伤害系数可为 `0`(仅表现/航迹效果,不结算 HP)。若同行 `atk_buff1` 头为 `12`,命中基础字时按该 buff 概率击倒(损毁)。怪物主动技也可在施法结束经 `PassiveSkillHost.applySkillResult` 触发(金 Boss `11101`)。表现用 `battleHero/prefab/skill/玉帝刀光`,飞出可视区后回池。飞行速度代码常量(表无速度段)。动画:轩辕剑 / 无名默认 `剑气金`,吴钩双剑同资源。例:轩辕剑 `11,3,0.3,15000`;金 Boss `11,1,0,0`。 **result 12(闪电链)**:参数 `蔓延范围像素,蔓延数量,伤害系数万分比`。从触发目标起跳,向范围内**未打过**的最近敌蔓延(触发目标算第 1 跳,不重复)。每跳先出电弧/受击,间隔后再结算该落点伤害。表现用老项目 `电母-大招`(`普通雷电`)与 `电母-大招受击`(`普通受击`),传导间隔对齐资源时长/2。例:三尖两面刀 `12,200,5,120000`。 diff --git a/preview-template/index.ejs b/preview-template/index.ejs index 4bf1b72..3386845 100644 --- a/preview-template/index.ejs +++ b/preview-template/index.ejs @@ -50,6 +50,17 @@ + + @@ -207,6 +218,7 @@ '/workbench/js/agent-pick.js', '/workbench/js/agent-console.js', '/workbench/js/agent-assets.js', + '/workbench/js/agent-profiler.js', '/workbench/js/agent.js', ]; var fi = 0; diff --git a/preview-template/workbench/css/workbench.css b/preview-template/workbench/css/workbench.css index b2abd85..abb63f8 100644 --- a/preview-template/workbench/css/workbench.css +++ b/preview-template/workbench/css/workbench.css @@ -1139,6 +1139,11 @@ body { vertical-align: middle; } +.pw-assets-tag-dep { + background: rgba(212, 162, 76, 0.16); + color: var(--pw-warn); +} + .pw-assets-side { width: 168px; flex-shrink: 0; @@ -1174,6 +1179,160 @@ body { overflow: auto; } +/* —— 性能面板 —— */ +.pw-perf-body { + flex: 1; + min-height: 0; + overflow: auto; + padding: var(--sp-2); + display: flex; + flex-direction: column; + gap: var(--sp-2); +} + +.pw-perf-charts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-2); +} + +.pw-perf-charts .pw-chart-card { + background: var(--pw-bg-1); + border: 1px solid var(--pw-border); + border-radius: 6px; + padding: var(--sp-1) var(--sp-2) var(--sp-2); + min-height: 120px; +} + +.pw-perf-charts h4 { + margin: 4px 0 6px; + font-size: var(--pw-fs-xs); + color: var(--pw-muted); + font-weight: 600; +} + +.pw-perf-charts canvas { + width: 100%; + height: 96px; + display: block; +} + +.pw-perf-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-2); +} + +.pw-perf-block { + background: var(--pw-bg-1); + border: 1px solid var(--pw-border); + border-radius: 6px; + min-height: 0; + display: flex; + flex-direction: column; +} + +.pw-perf-block h4 { + margin: 0; + padding: 8px 10px 4px; + font-size: var(--pw-fs-xs); + color: var(--pw-text-2); + font-weight: 600; +} + +.pw-perf-list { + overflow: auto; + max-height: 220px; + font-size: 11px; + padding: 0 6px 6px; +} + +.pw-perf-rowh { + display: grid; + grid-template-columns: 1fr 56px 56px; + gap: 6px; + padding: 4px 6px; + color: var(--pw-muted); + border-bottom: 1px solid var(--pw-border); + position: sticky; + top: 0; + background: var(--pw-bg-1); +} + +.pw-perf-item { + display: grid; + grid-template-columns: 1fr 56px 56px; + gap: 6px; + padding: 3px 6px; + border-bottom: 1px solid transparent; + align-items: start; +} + +.pw-perf-item:hover { background: rgba(255, 255, 255, 0.03); } + +.pw-perf-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--pw-font-mono); +} + +.pw-perf-name i { + font-style: normal; + font-size: 10px; +} + +.pw-perf-num { + font-family: var(--pw-font-mono); + font-variant-numeric: tabular-nums; + text-align: right; + color: var(--pw-text-2); +} + +.pw-perf-stack { + grid-column: 2 / -1; + color: var(--pw-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: var(--pw-font-mono); + font-size: 10px; +} + +.pw-slow-item { + grid-template-columns: 56px 1fr; +} + +.pw-bad { color: var(--pw-danger); } + +.pw-res-list .pw-perf-item { + grid-template-columns: 1fr 72px 64px auto; + align-items: center; +} + +.pw-res-list .pw-perf-rowh { + grid-template-columns: 1fr 72px 64px auto; +} + +.pw-res-ops { + display: flex; + gap: 2px; + flex-shrink: 0; +} + +.pw-btn-mini { + padding: 1px 6px; + font-size: 10px; + border-radius: 3px; +} + +.pw-perf-type { + color: var(--pw-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .pw-pick-overlay { position: fixed; pointer-events: none; @@ -1215,21 +1374,23 @@ body { .pw-gizmo-axis { position: absolute; pointer-events: auto; + z-index: 2; } +/* 轴原点 = 节点中心 */ .pw-gizmo-x { - right: -4px; + left: 50%; top: 50%; - width: 36px; - height: 18px; - margin-top: -9px; + width: 42px; + height: 16px; + margin-top: -8px; cursor: ew-resize; } .pw-gizmo-x i { position: absolute; left: 0; - right: 12px; + right: 10px; top: 50%; height: 3px; margin-top: -1.5px; @@ -1250,17 +1411,18 @@ body { .pw-gizmo-y { left: 50%; - top: -36px; - width: 18px; - height: 36px; - margin-left: -9px; + top: 50%; + width: 16px; + height: 42px; + margin-left: -8px; + margin-top: -42px; cursor: ns-resize; } .pw-gizmo-y i { position: absolute; top: 0; - bottom: 12px; + bottom: 10px; left: 50%; width: 3px; margin-left: -1.5px; @@ -2360,24 +2522,26 @@ html.pw-inspect #GameCanvas { .pw-shell-classic .pw-device-ear { display: block; } /* 刘海 / 灵动岛 / 打孔 */ +/* 挖孔 / 刘海 / 胶囊:相对「屏幕」而非中框 */ .pw-device-cutout { display: none; position: absolute; z-index: 6; pointer-events: none; background: #050505; - box-shadow: inset 0 0 0 1px #1a1a1a; } .pw-device-bezel.has-cutout .pw-device-cutout { display: block; } +/* 刘海:贴屏幕上沿居中 */ .pw-device-bezel.has-notch .pw-device-cutout { - top: 10px; + top: 0; left: 50%; transform: translateX(-50%); width: 36%; height: 22px; border-radius: 0 0 14px 14px; + background: #050505; } .pw-device-bezel.has-notch .pw-device-cutout::after { @@ -2390,17 +2554,18 @@ html.pw-inspect #GameCanvas { margin-top: -4px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #1e3a5f, #0a1520 70%); - box-shadow: 0 0 0 1px #222; } +/* 灵动岛 */ .pw-device-bezel.has-island .pw-device-cutout { - top: 12px; + top: 10px; left: 50%; transform: translateX(-50%); - width: 28%; - max-width: 120px; + width: 26%; + max-width: 118px; height: 24px; border-radius: 999px; + background: #050505; } .pw-device-bezel.has-island .pw-device-cutout::before { @@ -2415,14 +2580,293 @@ html.pw-inspect #GameCanvas { background: radial-gradient(circle at 30% 30%, #243b55, #0a1018 70%); } +/* 挖孔:贴屏幕上沿 */ .pw-device-bezel.has-punch .pw-device-cutout { - top: 10px; + top: 8px; left: 50%; transform: translateX(-50%); - width: 10px; - height: 10px; + width: 11px; + height: 11px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #1a3350, #05080c 75%); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06); +} + +.pw-device-bezel.has-punch-l .pw-device-cutout { + left: 10%; + transform: none; +} + +.pw-device-bezel.has-teardrop .pw-device-cutout { + top: 0; + left: 50%; + transform: translateX(-50%); + width: 14px; + height: 18px; + border-radius: 0 0 10px 10px; + background: radial-gradient(circle at 50% 20%, #1a3350, #05080c 70%); +} + +.pw-device-bezel.has-tablet-cam .pw-device-cutout { + display: block; + top: 50%; + left: 6px; + transform: translateY(-50%); + width: 8px; + height: 8px; + border-radius: 50%; + background: radial-gradient(circle at 35% 35%, #243b55, #0a1018 70%); +} + +/* 微信小程序胶囊:屏幕内右上,由 JS 按安全区调 top */ +.pw-device-capsule { + position: absolute; + z-index: 20; + top: 8px; + right: 10px; + display: none; + align-items: center; + gap: 0; + height: 32px; + padding: 0 12px; + border-radius: 999px; + pointer-events: none; + box-sizing: border-box; +} + +.pw-device-capsule.show { display: inline-flex; } + +/* —— 微信官方小程序菜单(浅色,对齐设计指南「小程序菜单」) —— */ +.pw-device-capsule.cap-wechat { + background: rgba(255, 255, 255, 0.96); + border: 0.5px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); + height: 32px; + border-radius: 16px; + padding: 0 12px; +} + +.pw-device-capsule.cap-wechat .pw-capsule-home { + width: 16px; + height: 16px; + border-radius: 50%; + border: 1.6px solid #07c160; + background: + radial-gradient(circle at 50% 42%, #07c160 0 2.2px, transparent 2.4px), + radial-gradient(circle at 50% 50%, transparent 0 4px, #07c160 4.2px 5.8px, transparent 6px); +} + +.pw-device-capsule.cap-wechat .pw-capsule-line { + width: 1px; + height: 16px; + margin: 0 12px; + background: rgba(0, 0, 0, 0.1); +} + +.pw-device-capsule.cap-wechat .pw-capsule-menu { + width: 18px; + height: 5px; + background: + radial-gradient(circle at 8% 50%, #111 0 1.8px, transparent 2px), + radial-gradient(circle at 50% 50%, #111 0 1.8px, transparent 2px), + radial-gradient(circle at 92% 50%, #111 0 1.8px, transparent 2px); +} + +/* —— 微信官方菜单(深色) —— */ +.pw-device-capsule.cap-wechat-dark { + background: rgba(0, 0, 0, 0.75); + height: 32px; + border-radius: 16px; + padding: 0 12px; +} + +.pw-device-capsule.cap-wechat-dark .pw-capsule-home { + width: 16px; + height: 16px; + border-radius: 50%; + border: 1.6px solid #fff; + background: + radial-gradient(circle at 50% 42%, #fff 0 2.2px, transparent 2.4px), + radial-gradient(circle at 50% 50%, transparent 0 4px, #fff 4.2px 5.8px, transparent 6px); +} + +.pw-device-capsule.cap-wechat-dark .pw-capsule-line { + width: 1px; + height: 16px; + margin: 0 12px; + background: rgba(255, 255, 255, 0.2); +} + +.pw-device-capsule.cap-wechat-dark .pw-capsule-menu { + width: 18px; + height: 5px; + background: + radial-gradient(circle at 8% 50%, #fff 0 1.8px, transparent 2px), + radial-gradient(circle at 50% 50%, #fff 0 1.8px, transparent 2px), + radial-gradient(circle at 92% 50%, #fff 0 1.8px, transparent 2px); +} + +/* —— 抖音:深色毛玻璃(默认) —— */ +.pw-device-capsule.cap-douyin { + background: rgba(0, 0, 0, 0.58); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + height: 32px; + border-radius: 16px; + padding: 0 12px; +} + +.pw-device-capsule.cap-douyin .pw-capsule-home { + width: 12px; + height: 12px; + border-radius: 50%; + border: 1.5px solid #fff; + background: radial-gradient(circle at 50% 50%, #fff 0 22%, transparent 23%); +} + +.pw-device-capsule.cap-douyin .pw-capsule-line { + width: 1px; + height: 14px; + margin: 0 10px; + background: rgba(255, 255, 255, 0.28); +} + +.pw-device-capsule.cap-douyin .pw-capsule-menu { + width: 16px; + height: 4px; + background: + radial-gradient(circle at 10% 50%, #fff 0 1.5px, transparent 1.7px), + radial-gradient(circle at 50% 50%, #fff 0 1.5px, transparent 1.7px), + radial-gradient(circle at 90% 50%, #fff 0 1.5px, transparent 1.7px); +} + +/* —— 支付宝:浅灰胶囊 —— */ +.pw-device-capsule.cap-alipay { + background: rgba(245, 246, 248, 0.96); + height: 30px; +} + +.pw-device-capsule.cap-alipay .pw-capsule-home { + width: 13px; + height: 13px; + border-radius: 3px; + background: #1677ff; + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.25); +} + +.pw-device-capsule.cap-alipay .pw-capsule-line { + width: 1px; + height: 14px; + margin: 0 10px; + background: rgba(0, 0, 0, 0.1); +} + +.pw-device-capsule.cap-alipay .pw-capsule-menu { + width: 14px; + height: 14px; + border: 1.5px solid #333; + border-radius: 50%; + background: transparent; +} + +/* —— QQ:浅色 + 企鹅感圆点 —— */ +.pw-device-capsule.cap-qq { + background: rgba(255, 255, 255, 0.95); + height: 30px; +} + +.pw-device-capsule.cap-qq .pw-capsule-home { + width: 14px; + height: 14px; + border-radius: 50%; + background: radial-gradient(circle at 35% 35%, #5ec7f5, #12b7f5 70%); +} + +.pw-device-capsule.cap-qq .pw-capsule-line { + width: 1px; + height: 14px; + margin: 0 10px; + background: rgba(0, 0, 0, 0.1); +} + +.pw-device-capsule.cap-qq .pw-capsule-menu { + width: 16px; + height: 4px; + background: + radial-gradient(circle at 10% 50%, #222 0 1.5px, transparent 1.7px), + radial-gradient(circle at 50% 50%, #222 0 1.5px, transparent 1.7px), + radial-gradient(circle at 90% 50%, #222 0 1.5px, transparent 1.7px); +} + +/* —— 快手:橙色点缀 —— */ +.pw-device-capsule.cap-kuaishou { + background: rgba(255, 255, 255, 0.94); + height: 30px; +} + +.pw-device-capsule.cap-kuaishou .pw-capsule-home { + width: 13px; + height: 13px; + border-radius: 50%; + background: #ff5000; +} + +.pw-device-capsule.cap-kuaishou .pw-capsule-line { + width: 1px; + height: 14px; + margin: 0 10px; + background: rgba(0, 0, 0, 0.1); +} + +.pw-device-capsule.cap-kuaishou .pw-capsule-menu { + width: 16px; + height: 4px; + background: + radial-gradient(circle at 10% 50%, #222 0 1.5px, transparent 1.7px), + radial-gradient(circle at 50% 50%, #222 0 1.5px, transparent 1.7px), + radial-gradient(circle at 90% 50%, #222 0 1.5px, transparent 1.7px); +} + +/* —— B站:粉点缀 —— */ +.pw-device-capsule.cap-bilibili { + background: rgba(255, 255, 255, 0.95); + height: 30px; +} + +.pw-device-capsule.cap-bilibili .pw-capsule-home { + width: 14px; + height: 12px; + border-radius: 3px; + border: 1.6px solid #fb7299; + background: transparent; + position: relative; +} + +.pw-device-capsule.cap-bilibili .pw-capsule-home::after { + content: ''; + position: absolute; + left: -3px; + top: -3px; + width: 4px; + height: 4px; + border: 1.5px solid #fb7299; + border-radius: 1px; +} + +.pw-device-capsule.cap-bilibili .pw-capsule-line { + width: 1px; + height: 14px; + margin: 0 10px; + background: rgba(0, 0, 0, 0.1); +} + +.pw-device-capsule.cap-bilibili .pw-capsule-menu { + width: 16px; + height: 4px; + background: + radial-gradient(circle at 10% 50%, #222 0 1.5px, transparent 1.7px), + radial-gradient(circle at 50% 50%, #222 0 1.5px, transparent 1.7px), + radial-gradient(circle at 90% 50%, #222 0 1.5px, transparent 1.7px); } /* Home 指示条 / 实体 Home 键 */ diff --git a/preview-template/workbench/js/agent-assets.js b/preview-template/workbench/js/agent-assets.js index 8103db7..467a1eb 100644 --- a/preview-template/workbench/js/agent-assets.js +++ b/preview-template/workbench/js/agent-assets.js @@ -2,9 +2,11 @@ 'use strict'; /** - * 资源面板数据源(仅引擎接口): - * assetManager.assets — 全部已加载资源 - * assetManager.bundles — 已加载 Bundle(归属用 cache/_map 尽力匹配) + * 资源进包(对齐 Cocos 构建规则): + * 1) 在 Bundle 目录/config 内 → 该 Bundle + * 2) 不在任何 Bundle 内,被若干 Bundle 引用 → 优先级数字最大的 Bundle + * 3) 无人引用 → internal + * 优先级:config.priority,数字越大越高;无配置则 0。 */ function fmtType(asset) { @@ -16,7 +18,6 @@ return (bytes || 0) / (1024 * 1024); } - /** GPU 估算:纹理去重只计一次 */ function texMemMB(tex) { if (!tex) return 0; var w = tex.width || 0; @@ -39,7 +40,6 @@ if (key) seenTex[key] = 1; return texMemMB(asset); } - // SpriteFrame / Image 挂在纹理上,不进总量 if (type === 'SpriteFrame' || type === 'ImageAsset') return 0; if (type === 'AudioClip' || /Audio/.test(type)) { try { @@ -48,12 +48,8 @@ } catch (e) { return 0.2; } } try { - if (asset._nativeAsset && asset._nativeAsset.byteLength) { - return memMB(asset._nativeAsset.byteLength); - } - if (asset.data && asset.data.byteLength) { - return memMB(asset.data.byteLength); - } + if (asset._nativeAsset && asset._nativeAsset.byteLength) return memMB(asset._nativeAsset.byteLength); + if (asset.data && asset.data.byteLength) return memMB(asset.data.byteLength); } catch (e) { /* */ } return 0; } @@ -84,204 +80,117 @@ return { gfxTextureMB: tex, gfxBufferMB: buf, jsHeapMB: heap }; } - /** - * 按构建「依赖打入分包」扩展归属: - * 包内资源引用到的、且尚未被其它包显式声明的资源,打入同一包。 - * 使用 assetManager.dependUtil.getDependsRecursively。 - */ - function expandOwnershipByDeps(cc, bi) { - var depUtil = cc.assetManager && cc.assetManager.dependUtil; - if (!depUtil || typeof depUtil.getDependsRecursively !== 'function') return; - - var uuidMap = bi.uuidMap || {}; - var depPacked = bi.depPacked || (bi.depPacked = {}); - var roots = []; - for (var u in uuidMap) { - if (Object.prototype.hasOwnProperty.call(uuidMap, u)) roots.push(u); - } - - for (var i = 0; i < roots.length; i++) { - var root = roots[i]; - var bname = uuidMap[root]; - if (!bname) continue; - var deps = null; - try { - deps = depUtil.getDependsRecursively(root); - } catch (e) { - try { - deps = depUtil.getDeps(root); - } catch (e2) { - deps = null; - } - } - if (!deps || !deps.length) continue; - for (var j = 0; j < deps.length; j++) { - var d = String(deps[j]); - if (!d || d === root) continue; - // 已被其它包显式占有则不抢(跨包引用) - if (uuidMap[d] && uuidMap[d] !== bname) continue; - if (!uuidMap[d]) { - uuidMap[d] = bname; - depPacked[d] = true; - } - } - } - } - function resolveBundleName(uuid, asset, bi) { + /** 优先级:数字越大越高 */ + function readPriority(bundle, name) { try { - if (asset) { - if (asset.bundle && asset.bundle.name) return asset.bundle.name; - if (asset._bundle && asset._bundle.name) return asset._bundle.name; - if (asset.__bundleName) return asset.__bundleName; - } + if (bundle && bundle.priority != null && isFinite(bundle.priority)) return Number(bundle.priority); } catch (e) { /* */ } - if (bi && bi.uuidMap && uuid && bi.uuidMap[uuid]) return bi.uuidMap[uuid]; + try { + var cfg = bundle && bundle._config; + if (cfg && cfg.priority != null && isFinite(cfg.priority)) return Number(cfg.priority); + } catch (e2) { /* */ } + try { + if (bundle && bundle._priority != null && isFinite(bundle._priority)) return Number(bundle._priority); + } catch (e3) { /* */ } + return 0; + } - var url = (asset && (asset.nativeUrl || asset._nativeUrl)) || ''; + /** nativeUrl 里的包名:…/home/import/… → home */ + function bundleFromUrl(url) { + if (!url) return ''; var m = String(url).match(/\/([A-Za-z0-9_\-]+)\/(?:import|native|js|ohm|chunks)\//i); - if (m && m[1]) return m[1]; - - var objs = (bi && bi.bundleObjs) || []; - for (var i = 0; i < objs.length; i++) { - var b = objs[i]; - if (!b) continue; - var bname = b.__pwName || b.name || ''; - try { - if (typeof b.getAssetInfo === 'function' && uuid) { - var info = b.getAssetInfo(uuid); - if (info && (info.uuid || info.redirect || info.path != null || info.ext)) return bname; - } - } catch (e2) { /* */ } - try { - if (b.cache && b.cache.has && uuid && b.cache.has(uuid)) return bname; - } catch (e3) { /* */ } - } - return ''; + return m && m[1] ? m[1] : ''; } /** - * 构建索引:uuid→bundle、uuid→path、pack(自动图集)成员。 - * 数据来自 bundle._config(paths / packs)与 cache。 + * 建 uuid → bundle 显式索引(构建归属)。 + * 顺序:nativeUrl → getAssetInfo → cache → config.paths/packs */ - function indexBundles(cc) { - var list = []; + function buildIndex(cc) { var uuidMap = {}; var pathMap = {}; - var packOf = {}; // uuid -> packId - var packMembers = {}; // packId -> [uuid] + var packOf = {}; + var packMembers = {}; var bundleObjs = []; + var list = []; - var addU = function (u, bname) { - if (u != null && bname) uuidMap[String(u)] = bname; - }; - - try { - cc.assetManager.bundles.forEach(function (bundle, name) { - var n = name || (bundle && bundle.name) || ''; - if (!n || !bundle) return; - bundle.__pwName = n; - bundleObjs.push(bundle); - list.push({ - name: n, - assetCount: 0, - memMB: 0, - texMB: 0, - audioMB: 0, - otherMB: 0, - status: 'loaded', - packCount: 0, - }); + cc.assetManager.bundles.forEach(function (bundle, name) { + var n = name || (bundle && bundle.name) || ''; + if (!n || !bundle) return; + bundle.__pwName = n; + bundleObjs.push(bundle); + list.push({ + name: n, + assetCount: 0, + memMB: 0, + texMB: 0, + audioMB: 0, + otherMB: 0, + status: 'loaded', + priority: readPriority(bundle, n), + }); - var cfg = bundle._config; + var put = function (u) { + if (u != null && u !== '') uuidMap[String(u)] = n; + }; - // 1) paths: 路径 → [uuid, type, ...] 或 {uuid} - try { - var paths = cfg && (cfg.paths || cfg._paths); - if (paths && typeof paths === 'object') { - for (var p in paths) { - if (!Object.prototype.hasOwnProperty.call(paths, p)) continue; - var val = paths[p]; - if (Array.isArray(val)) { - for (var i = 0; i < val.length; i++) { - var u1 = val[i]; - if (typeof u1 === 'string' && /^[0-9a-fA-F-]{8,}/.test(u1)) { - addU(u1, n); - if (!pathMap[u1]) pathMap[u1] = p; - } else if (u1 && u1.uuid) { - addU(u1.uuid, n); - if (!pathMap[u1.uuid]) pathMap[u1.uuid] = p; - } - } - } else if (val && val.uuid) { - addU(val.uuid, n); - if (!pathMap[val.uuid]) pathMap[val.uuid] = p; - } - } + // cache + try { + var cache = bundle.cache || bundle._cache; + if (cache && cache._map) { + var keys = Object.keys(cache._map); + for (var i = 0; i < keys.length; i++) { + if (cache._map[keys[i]]) put(keys[i]); } - } catch (e) { /* */ } + } + if (cache && typeof cache.forEach === 'function') { + cache.forEach(function (asset, uuid) { + put(uuid); + if (asset) put(asset._uuid); + }); + } + } catch (e) { /* */ } - // 2) packs: 自动图集 / 合包 - try { - var packs = cfg && (cfg.packs || cfg._packs); - if (packs && typeof packs === 'object') { - var packCount = 0; - for (var packId in packs) { - if (!Object.prototype.hasOwnProperty.call(packs, packId)) continue; - var members = packs[packId]; - if (!members) continue; - packCount += 1; - packMembers[packId] = packMembers[packId] || []; - var arr = Array.isArray(members) ? members : [members]; - for (var j = 0; j < arr.length; j++) { - var um = arr[j]; - if (um && typeof um === 'object') um = um.uuid; - if (!um) continue; - addU(um, n); - packOf[String(um)] = packId; - packMembers[packId].push(String(um)); - if (!pathMap[String(um)]) pathMap[String(um)] = packId; + // config.paths + try { + var cfg = bundle._config; + var paths = cfg && cfg.paths; + if (paths) { + for (var p in paths) { + if (!Object.prototype.hasOwnProperty.call(paths, p)) continue; + var val = paths[p]; + var listU = Array.isArray(val) ? val : (val ? [val] : []); + for (var j = 0; j < listU.length; j++) { + var uu = listU[j]; + if (uu && typeof uu === 'object') uu = uu.uuid; + if (typeof uu === 'string' && uu) { + put(uu); + if (!pathMap[uu]) pathMap[uu] = p; } } - for (var q = 0; q < list.length; q++) { - if (list[q].name === n) list[q].packCount = packCount; - } } - } catch (e2) { /* */ } - - // 3) cache - try { - var cache = bundle.cache || bundle._cache; - if (cache && typeof cache.forEach === 'function') { - cache.forEach(function (asset, uuid) { - addU(uuid, n); - if (asset) addU(asset._uuid, n); - }); - } - if (cache && cache._map) { - for (var k in cache._map) { - if (Object.prototype.hasOwnProperty.call(cache._map, k)) addU(k, n); - } - } - } catch (e3) { /* */ } - - // 4) getAssetInfo 扫一遍 config 里的 uuid - try { - if (typeof bundle.getAssetInfo === 'function') { - var keys = Object.keys(uuidMap); - for (var s = 0; s < keys.length; s++) { - if (uuidMap[keys[s]] === n) continue; - try { - var inf = bundle.getAssetInfo(keys[s]); - if (inf && (inf.path != null || inf.redirect)) { - addU(keys[s], n); - if (!pathMap[keys[s]] && inf.path) pathMap[keys[s]] = inf.path; - } - } catch (e4) { /* */ } + } + // config.packs(自动图集) + var packs = cfg && cfg.packs; + if (packs) { + for (var packId in packs) { + if (!Object.prototype.hasOwnProperty.call(packs, packId)) continue; + var members = packs[packId]; + if (!members) continue; + packMembers[packId] = packMembers[packId] || []; + var arr = Array.isArray(members) ? members : [members]; + for (var m = 0; m < arr.length; m++) { + var um = arr[m]; + if (um && typeof um === 'object') um = um.uuid; + if (!um) continue; + put(um); + packOf[String(um)] = packId; + packMembers[packId].push(String(um)); } } - } catch (e5) { /* */ } - }); - } catch (e6) { /* */ } + } + } catch (e2) { /* */ } + }); return { list: list, @@ -293,136 +202,235 @@ }; } + /** 对每个资源:getAssetInfo / nativeUrl 再补一轮归属 */ + function refineOwnership(cc, bi, rows) { + var objs = bi.bundleObjs || []; + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + var id = row.uuid; + if (bi.uuidMap[id]) continue; + + // nativeUrl + var asset = null; + try { + asset = cc.assetManager.assets.get(id); + } catch (e) { asset = null; } + var fromUrl = bundleFromUrl(asset && (asset.nativeUrl || asset._nativeUrl)); + if (fromUrl) { + bi.uuidMap[id] = fromUrl; + row.bundle = fromUrl; + continue; + } + + // getAssetInfo + for (var b = 0; b < objs.length; b++) { + var bundle = objs[b]; + if (!bundle || typeof bundle.getAssetInfo !== 'function') continue; + try { + var info = bundle.getAssetInfo(id); + if (info && (info.path != null || info.uuid || info.redirect)) { + bi.uuidMap[id] = bundle.__pwName || bundle.name; + row.bundle = bi.uuidMap[id]; + if (info.path && !bi.pathMap[id]) bi.pathMap[id] = info.path; + break; + } + } catch (e2) { /* */ } + } + } + } + /** - * @returns {{bundles:Array, rows:Array, totals:Object}} + * 依赖进包:不在任何 Bundle 的资源 → 优先级数字最大的引用方。 */ + function applyRefPacking(cc, bi, rows) { + var depUtil = cc.assetManager && cc.assetManager.dependUtil; + var byUuid = {}; + for (var i = 0; i < rows.length; i++) byUuid[rows[i].uuid] = rows[i]; + + var explicit = {}; + for (var k in bi.uuidMap) { + if (Object.prototype.hasOwnProperty.call(bi.uuidMap, k)) explicit[k] = bi.uuidMap[k]; + } + + var prio = {}; + for (var p = 0; p < bi.list.length; p++) { + prio[bi.list[p].name] = bi.list[p].priority || 0; + } + + // 引用根:各包内显式归属的资源 + var rootsByBundle = {}; + for (var r = 0; r < rows.length; r++) { + var row = rows[r]; + var bb = explicit[row.uuid]; + if (!bb) continue; + if (!rootsByBundle[bb]) rootsByBundle[bb] = []; + rootsByBundle[bb].push(row.uuid); + } + + var getDeps = function (uuid) { + if (!depUtil) return []; + try { + if (depUtil.getDependsRecursively) return depUtil.getDependsRecursively(uuid) || []; + } catch (e) { /* */ } + try { + if (depUtil.getDeps) return depUtil.getDeps(uuid) || []; + } catch (e2) { /* */ } + return []; + }; + + var refBy = {}; + var addRef = function (uuid, bname) { + if (!uuid || !bname) return; + if (!refBy[uuid]) refBy[uuid] = []; + if (refBy[uuid].indexOf(bname) < 0) refBy[uuid].push(bname); + }; + + for (var bn in rootsByBundle) { + if (!Object.prototype.hasOwnProperty.call(rootsByBundle, bn)) continue; + var roots = rootsByBundle[bn]; + for (var i2 = 0; i2 < roots.length; i2++) { + var deps = getDeps(roots[i2]); + addRef(roots[i2], bn); + for (var d = 0; d < deps.length; d++) addRef(String(deps[d]), bn); + } + } + + for (var j = 0; j < rows.length; j++) { + var row2 = rows[j]; + var u2 = row2.uuid; + + // 已显式归属 + if (explicit[u2]) { + row2.bundle = explicit[u2]; + row2.via = 'direct'; + row2.usedBy = refBy[u2] ? refBy[u2].slice() : [explicit[u2]]; + continue; + } + + var refs = refBy[u2] || []; + if (!refs.length) { + row2.bundle = 'internal'; + row2.via = 'direct'; + row2.usedBy = ['internal']; + continue; + } + + // 优先级数字最大的引用方 + var best = refs[0]; + for (var n = 1; n < refs.length; n++) { + var prBest = prio[best] || 0; + var prCur = prio[refs[n]] || 0; + if (prCur > prBest) best = refs[n]; + } + row2.bundle = best; + row2.via = 'dep'; + row2.usedBy = refs.slice(); + } + } + function collectAssets() { var cc = global.cc; if (!cc || !cc.assetManager || !cc.assetManager.assets) { return { bundles: [], rows: [], totals: gfxTotals() }; } - var bi = indexBundles(cc); - var bundleByName = {}; - for (var i = 0; i < bi.list.length; i++) { - bundleByName[bi.list[i].name] = bi.list[i]; - } - - var seenTex = {}; + var bi = buildIndex(cc); var rows = []; + var seenTex = {}; - // —— 全部已加载资源:assetManager.assets —— - try { - cc.assetManager.assets.forEach(function (asset, uuid) { - if (!asset) return; - var id = String(uuid || asset._uuid || ''); - var type = fmtType(asset); - var mem = assetMem(asset, type, seenTex); - - var bname = resolveBundleName(id, asset, bi); - if (!bname) bname = 'internal'; - var agg = bundleByName[bname]; - if (!agg) { - agg = { - name: bname, - assetCount: 0, - memMB: 0, - texMB: 0, - audioMB: 0, - otherMB: 0, - status: 'loaded', - packCount: 0, - }; - bundleByName[bname] = agg; - bi.list.push(agg); - } + cc.assetManager.assets.forEach(function (asset, uuid) { + if (!asset) return; + var id = String(uuid || asset._uuid || ''); + var type = fmtType(asset); + var mem = assetMem(asset, type, seenTex); + var bname = bi.uuidMap[id] || bundleFromUrl(asset.nativeUrl || asset._nativeUrl) || ''; - var kind = 'other'; - if (type === 'Texture2D' || type === 'Texture') kind = 'tex'; - else if (type === 'AudioClip' || /Audio/.test(type)) kind = 'audio'; - - var w = 0; - var h = 0; - if (type === 'SpriteFrame') { - var sz = frameSize(asset); - w = sz.w; - h = sz.h; - } else { - w = asset.width | 0; - h = asset.height | 0; - } + var kind = 'other'; + if (type === 'Texture2D' || type === 'Texture') kind = 'tex'; + else if (type === 'AudioClip' || /Audio/.test(type)) kind = 'audio'; - // 自动图集 / 合包:packId;SpriteFrame 挂到纹理 uuid - var packId = bi.packOf[id] || ''; - var texUuid = ''; - if (type === 'SpriteFrame') { - var tex = asset.texture || (asset.getTexture && asset.getTexture()); - texUuid = String((tex && (tex._uuid || tex.uuid)) || ''); - } else if (type === 'Texture2D' || type === 'Texture') { - texUuid = id; - } + var w = 0; + var h = 0; + if (type === 'SpriteFrame') { + var sz = frameSize(asset); + w = sz.w; + h = sz.h; + } else { + w = asset.width | 0; + h = asset.height | 0; + } - var display = asset.name || ''; - var pathStr = bi.pathMap[id] || ''; - if (!display || /^[0-9a-f-]{8,}$/i.test(display)) { - if (pathStr) { - var segs = String(pathStr).split('/'); - display = segs[segs.length - 1] || pathStr; - } - if (!display) { - try { - for (var bi2 = 0; bi2 < bi.bundleObjs.length; bi2++) { - var bb = bi.bundleObjs[bi2]; - if (bb && typeof bb.getAssetInfo === 'function') { - var inf = bb.getAssetInfo(id); - if (inf && inf.path) { - var ps = String(inf.path).split('/'); - display = ps[ps.length - 1] || inf.path; - break; - } - } - } - } catch (eName) { /* */ } - } - if (!display) display = id.slice(0, 8) + '…'; + var display = asset.name || ''; + var pathStr = bi.pathMap[id] || ''; + if (!display || /^[0-9a-f-]{8,}$/i.test(display)) { + if (pathStr) { + var segs = String(pathStr).split('/'); + display = segs[segs.length - 1] || pathStr; } + if (!display) display = id.slice(0, 8) + '…'; + } - rows.push({ - uuid: id, - name: display, - type: type, - bundle: bname, - mem: mem, - ref: asset.refCount != null ? asset.refCount : 0, - w: w, - h: h, - kind: kind, - pack: packId, - texUuid: texUuid, - path: pathStr, - }); - - agg.assetCount += 1; - agg.memMB += mem; - if (kind === 'tex') agg.texMB += mem; - else if (kind === 'audio') agg.audioMB += mem; - else agg.otherMB += mem; + rows.push({ + uuid: id, + name: display, + type: type, + bundle: bname, + mem: mem, + ref: asset.refCount != null ? asset.refCount : 0, + w: w, + h: h, + kind: kind, + pack: bi.packOf[id] || '', + texUuid: type === 'SpriteFrame' ? '' : id, + path: pathStr, + via: bname ? 'direct' : 'direct', }); - } catch (e) { - console.warn('[Agent] assets.scan', e); - } + }); - var bundles = bi.list; - for (var j = 0; j < bundles.length; j++) { - if (bundles[j].assetCount === 0 && bundles[j].status === 'loaded') { - // cache 里没建到索引,但 assets 已扫到就保持 loaded;全 0 才标空 - if (bundles[j].memMB === 0 && bundles[j].assetCount === 0) { - bundles[j].status = 'loaded-empty'; - } + // 补归属 + 依赖进包 + refineOwnership(cc, bi, rows); + applyRefPacking(cc, bi, rows); + + // 汇总 bundle + var byName = {}; + for (var i = 0; i < bi.list.length; i++) { + byName[bi.list[i].name] = bi.list[i]; + } + for (var z = 0; z < bi.list.length; z++) { + bi.list[z].assetCount = 0; + bi.list[z].memMB = 0; + bi.list[z].texMB = 0; + bi.list[z].audioMB = 0; + bi.list[z].otherMB = 0; + } + for (var r = 0; r < rows.length; r++) { + var row = rows[r]; + var agg = byName[row.bundle]; + if (!agg) { + agg = { + name: row.bundle || 'internal', + assetCount: 0, + memMB: 0, + texMB: 0, + audioMB: 0, + otherMB: 0, + status: 'loaded', + priority: 0, + }; + byName[agg.name] = agg; + bi.list.push(agg); } + agg.assetCount += 1; + agg.memMB += row.mem; + if (row.kind === 'tex') agg.texMB += row.mem; + else if (row.kind === 'audio') agg.audioMB += row.mem; + else agg.otherMB += row.mem; + } + for (var y = 0; y < bi.list.length; y++) { + if (bi.list[y].assetCount === 0) bi.list[y].status = 'loaded-empty'; } - bundles.sort(function (a, b) { - return (b.memMB - a.memMB) || (b.assetCount - a.assetCount) || String(a.name).localeCompare(String(b.name)); + bi.list.sort(function (a, b) { + return (b.memMB - a.memMB) || (b.assetCount - a.assetCount); }); var totals = gfxTotals(); @@ -430,21 +438,20 @@ totals.texMB = 0; totals.audioMB = 0; totals.otherMB = 0; - for (var r = 0; r < rows.length; r++) { - totals.estimateMB += rows[r].mem; - if (rows[r].kind === 'tex') totals.texMB += rows[r].mem; - else if (rows[r].kind === 'audio') totals.audioMB += rows[r].mem; - else totals.otherMB += rows[r].mem; + for (var t = 0; t < rows.length; t++) { + totals.estimateMB += rows[t].mem; + if (rows[t].kind === 'tex') totals.texMB += rows[t].mem; + else if (rows[t].kind === 'audio') totals.audioMB += rows[t].mem; + else totals.otherMB += rows[t].mem; } totals.assetCount = rows.length; - totals.bundleCount = bundles.length; - totals.packCount = Object.keys(bi.packMembers || {}).length; + totals.bundleCount = bi.list.length; return { - bundles: bundles, + bundles: bi.list, rows: rows, totals: totals, - packs: bi.packMembers || {}, - packOf: bi.packOf || {}, + packs: bi.packMembers, + packOf: bi.packOf, }; } @@ -459,32 +466,19 @@ var ih = data.height || img.height || 0; var sw = Math.max(1, rect.width | 0); var sh = Math.max(1, rect.height | 0); - var sx = rect.x | 0; - var sy = Math.max(0, ih - (rect.y | 0) - sh) | 0; var cv = document.createElement('canvas'); cv.width = sw; cv.height = sh; - cv.getContext('2d').drawImage(data, sx, sy, sw, sh, 0, 0, sw, sh); + cv.getContext('2d').drawImage(data, rect.x | 0, Math.max(0, ih - (rect.y | 0) - sh) | 0, sw, sh, 0, 0, sw, sh); return { kind: 'image', w: sw, h: sh, url: cv.toDataURL('image/png') }; } - if (img && img.nativeUrl) { - return { kind: 'url', url: img.nativeUrl, w: (rect.width | 0), h: (rect.height | 0) }; - } return { kind: 'none' }; } - var im = type === 'ImageAsset' ? asset : (asset.image || asset._image); var d2 = im && (im._data || im.data); if (d2 && typeof HTMLImageElement !== 'undefined' && d2 instanceof HTMLImageElement) { return { kind: 'image', w: d2.width, h: d2.height, url: d2.src || '' }; } - if (d2 && d2.data && d2.width && typeof ImageData !== 'undefined') { - var c2 = document.createElement('canvas'); - c2.width = d2.width; - c2.height = d2.height; - c2.getContext('2d').putImageData(new ImageData(d2.data, d2.width, d2.height), 0, 0); - return { kind: 'image', w: d2.width, h: d2.height, url: c2.toDataURL('image/png') }; - } if (im && im.nativeUrl) return { kind: 'url', url: im.nativeUrl, w: im.width | 0, h: im.height | 0 }; if (asset.nativeUrl) return { kind: 'url', url: asset.nativeUrl, w: asset.width | 0, h: asset.height | 0 }; } catch (e) { /* */ } @@ -496,9 +490,7 @@ var asset = null; try { asset = cc.assetManager.assets.get(uuid); - } catch (e) { - asset = null; - } + } catch (e) { /* */ } if (!asset) return { kind: 'none' }; return buildPreview(asset, fmtType(asset)); } @@ -511,6 +503,7 @@ try { done(collectAssets()); } catch (e) { + console.warn('[assets]', e); done({ bundles: [], rows: [], totals: gfxTotals() }); } }; @@ -522,6 +515,5 @@ global.PWAssetsUtil = { collectAssets: collectAssets, previewAsset: previewAsset, - buildPreview: buildPreview, }; })(window); diff --git a/preview-template/workbench/js/agent-core.js b/preview-template/workbench/js/agent-core.js index 1fe9642..8180eec 100644 --- a/preview-template/workbench/js/agent-core.js +++ b/preview-template/workbench/js/agent-core.js @@ -142,6 +142,10 @@ case 'unsubscribe': return this.unsubscribe(params.channel); case 'perf.get': return this._perfPayload(); case 'stats.get': return this._statsPayload(); + case 'prof.start': return this._profStart ? this._profStart() : false; + case 'prof.stop': return this._profStop ? this._profStop() : false; + case 'prof.clear': return this._profClear ? this._profClear() : false; + case 'prof.report': return this._profReport ? this._profReport() : { functions: [], slowFrames: [], live: null }; case 'perf.clear': if (this._sampler) this._sampler.clear(); return true; case 'perf.mark': if (this._sampler) this._sampler.userMark(params.text || ''); return true; case 'perf.setInterval': if (this._sampler) this._sampler.setIntervalMs(params.ms || PERF_DEFAULT_MS); return true; @@ -168,7 +172,15 @@ case 'device.getState': return this._device ? { id: this._device.deviceId, rotated: this._device.rotated, + capsule: this._device.isCapsule ? this._device.isCapsule() : false, } : null; + case 'device.setCapsule': return this._device && this._device.setCapsule ? this._device.setCapsule(!!params.on) : false; + case 'device.setCapsuleStyle': return this._device && this._device.setCapsuleStyle + ? this._device.setCapsuleStyle(params.style) + : 'wechat'; + case 'device.getCapsuleStyle': return this._device && this._device.getCapsuleStyle + ? this._device.getCapsuleStyle() + : 'wechat'; case 'console.inspect': return this._inspectConsoleObj(params.oid, params.path || ''); default: throw new Error('unknown method: ' + method); diff --git a/preview-template/workbench/js/agent-profiler.js b/preview-template/workbench/js/agent-profiler.js new file mode 100644 index 0000000..418e2ca --- /dev/null +++ b/preview-template/workbench/js/agent-profiler.js @@ -0,0 +1,227 @@ +(function (global) { + 'use strict'; + + /** + * 游戏侧性能剖析(零侵入,按需采样): + * - 函数耗时 Top(Error.stack 采样聚合) + * - 慢帧堆栈 Top(帧耗时超阈值时抓栈) + * - 真实 FPS / 内存 + */ + function StackProfiler() { + this.enabled = false; + this.intervalMs = 8; + this._timer = 0; + this._fnStats = {}; // name -> {count, ms} + this._slowFrames = []; + this._maxSlow = 20; + this._lastTick = 0; + this._tickHooked = false; + this._origTick = null; + this._cc = null; + } + + StackProfiler.prototype.bindEngine = function (cc) { + this._cc = cc; + this._hookTick(); + }; + + StackProfiler.prototype._hookTick = function () { + var self = this; + var cc = this._cc; + if (!cc || !cc.director || this._tickHooked) return; + var dir = cc.director; + var proto = Object.getPrototypeOf(dir) || dir; + if (typeof proto.tick !== 'function') return; + this._origTick = proto.tick; + this._tickHooked = true; + proto.tick = function () { + if (!self.enabled) { + return self._origTick.apply(this, arguments); + } + var t0 = performance.now(); + var ret = self._origTick.apply(this, arguments); + var dt = performance.now() - t0; + self._noteFrame(dt); + return ret; + }; + }; + + StackProfiler.prototype._noteFrame = function (ms) { + if (ms < 20) return; + var stack = ''; + try { + stack = new Error('pf').stack || ''; + } catch (e) { /* */ } + this._slowFrames.push({ + t: Date.now(), + ms: Math.round(ms * 100) / 100, + stack: this._cleanStack(stack), + }); + if (this._slowFrames.length > this._maxSlow * 3) { + this._slowFrames.sort(function (a, b) { return b.ms - a.ms; }); + this._slowFrames.length = this._maxSlow; + } + }; + + StackProfiler.prototype._cleanStack = function (stack) { + if (!stack) return ''; + var lines = String(stack).split('\n'); + var out = []; + for (var i = 0; i < lines.length && out.length < 12; i++) { + var s = lines[i].trim(); + if (!s || /^error/i.test(s) || s.indexOf('Error') === 0 && s.indexOf('at ') < 0) continue; + if (/StackProfiler|_cleanStack|_noteFrame|agent-profiler|workbench/.test(s)) continue; + out.push(s); + } + return out.join('\n'); + }; + + StackProfiler.prototype.start = function () { + if (this.enabled) return; + this.enabled = true; + this._fnStats = {}; + this._slowFrames = []; + var self = this; + this._timer = setInterval(function () { + self._sampleStack(); + }, this.intervalMs); + }; + + StackProfiler.prototype.stop = function () { + this.enabled = false; + if (this._timer) { + clearInterval(this._timer); + this._timer = 0; + } + }; + + StackProfiler.prototype.clear = function () { + this._fnStats = {}; + this._slowFrames = []; + }; + + /** 调用栈采样:每 tick 记一帧到最热函数 */ + StackProfiler.prototype._sampleStack = function () { + if (!this.enabled) return; + var stack = ''; + try { + stack = new Error('s').stack || ''; + } catch (e) { + return; + } + var lines = String(stack).split('\n'); + var name = ''; + for (var i = 1; i < lines.length; i++) { + var s = lines[i].trim(); + if (!s) continue; + if (/StackProfiler|_sampleStack|agent-profiler|workbench|setInterval/.test(s)) continue; + // at fnName (url:line:col) 或 at url:line:col + var m = s.match(/at\s+([^\s(]+)/); + name = (m && m[1]) || s; + break; + } + if (!name) return; + if (!this._fnStats[name]) this._fnStats[name] = { count: 0, ms: 0 }; + this._fnStats[name].count += 1; + this._fnStats[name].ms += this.intervalMs; + }; + + StackProfiler.prototype.topFunctions = function (limit) { + var arr = []; + for (var k in this._fnStats) { + if (!Object.prototype.hasOwnProperty.call(this._fnStats, k)) continue; + arr.push({ + name: k, + count: this._fnStats[k].count, + ms: Math.round(this._fnStats[k].ms * 10) / 10, + }); + } + arr.sort(function (a, b) { return b.ms - a.ms; }); + return arr.slice(0, limit || 20); + }; + + StackProfiler.prototype.topSlowFrames = function (limit) { + var arr = this._slowFrames.slice(); + arr.sort(function (a, b) { return b.ms - a.ms; }); + return arr.slice(0, limit || 10); + }; + + /** 当前真实帧率 / 内存 */ + StackProfiler.prototype.liveStats = function () { + var cc = this._cc; + var fps = 0; + var tex = 0; + var buf = 0; + try { + var st = cc && cc.profiler && cc.profiler.stats; + if (st) { + if (st.fps && st.fps.counter) fps = st.fps.counter.value || 0; + if (st.textureMemory && st.textureMemory.counter) tex = st.textureMemory.counter.value || 0; + if (st.bufferMemory && st.bufferMemory.counter) buf = st.bufferMemory.counter.value || 0; + } + } catch (e) { /* */ } + if (!fps && cc && cc.director && typeof cc.director.getTotalFrames === 'function') { + var now = performance.now(); + var frames = cc.director.getTotalFrames(); + if (this._fpsLastFrames != null && this._fpsLastTime) { + var dt = now - this._fpsLastTime; + if (dt > 0) fps = ((frames - this._fpsLastFrames) * 1000) / dt; + } + this._fpsLastFrames = frames; + this._fpsLastTime = now; + } + var heap = 0; + try { + if (performance.memory && performance.memory.usedJSHeapSize) { + heap = performance.memory.usedJSHeapSize / (1024 * 1024); + } + } catch (e2) { /* */ } + return { + fps: fps, + textureMemory: tex, + bufferMemory: buf, + jsHeap: heap, + totalMemory: tex + buf + heap, + }; + }; + + global.PWStackProfiler = StackProfiler; + + // —— 挂到 Agent —— + if (global.PWAgent) { + var Agent = global.PWAgent; + if (!Agent.prototype._profiler) { + Agent.prototype._ensureProfiler = function () { + if (!this.__stackProfiler) { + this.__stackProfiler = new StackProfiler(); + if (this.cc) this.__stackProfiler.bindEngine(this.cc); + } else if (this.cc && !this.__stackProfiler._cc) { + this.__stackProfiler.bindEngine(this.cc); + } + return this.__stackProfiler; + }; + Agent.prototype._profStart = function () { + var p = this._ensureProfiler(); + p.start(); + return true; + }; + Agent.prototype._profStop = function () { + if (this.__stackProfiler) this.__stackProfiler.stop(); + return true; + }; + Agent.prototype._profClear = function () { + if (this.__stackProfiler) this.__stackProfiler.clear(); + return true; + }; + Agent.prototype._profReport = function () { + var p = this._ensureProfiler(); + return { + enabled: p.enabled, + functions: p.topFunctions(20), + slowFrames: p.topSlowFrames(10), + live: p.liveStats(), + }; + }; + } + } +})(window); diff --git a/preview-template/workbench/js/device.js b/preview-template/workbench/js/device.js index 858683b..e6482b0 100644 --- a/preview-template/workbench/js/device.js +++ b/preview-template/workbench/js/device.js @@ -2,37 +2,90 @@ 'use strict'; /** - * shell: - * - classic 圆角矩形 + Home 键(iPhone SE/8) - * - notch 刘海(iPhone X / 14) - * - island 灵动岛(iPhone 14 Pro+) - * - android 窄边框 + 打孔/小刘海感 - * - tablet 平板宽边框 + * shell(按真机形态做 CSS 边框): + * - classic Home 键(iPhone 8 / SE) + * - notch 刘海(iPhone X~14 标准) + * - island 灵动岛(iPhone 14 Pro 起) + * - punch 居中挖孔(多数安卓) + * - punchL 左上挖孔(部分三星/小米) + * - teardrop 水滴(中端安卓) + * - tablet 平板 + * - tabletCam 平板+前摄 + * - flat 无装饰框 */ var SHELL = { - classic: { padX: 0.048, padY: 0.11, minPadX: 12, minPadY: 28, radius: 0.12 }, - notch: { padX: 0.03, padY: 0.03, minPadX: 10, minPadY: 10, radius: 0.14 }, - island: { padX: 0.028, padY: 0.028, minPadX: 10, minPadY: 10, radius: 0.145 }, - android: { padX: 0.02, padY: 0.02, minPadX: 8, minPadY: 8, radius: 0.1 }, - tablet: { padX: 0.032, padY: 0.032, minPadX: 14, minPadY: 14, radius: 0.05 }, + classic: { padX: 0.055, padY: 0.12, minPadX: 14, minPadY: 32, radius: 0.1 }, + notch: { padX: 0.032, padY: 0.032, minPadX: 10, minPadY: 10, radius: 0.13 }, + island: { padX: 0.028, padY: 0.028, minPadX: 10, minPadY: 10, radius: 0.14 }, + punch: { padX: 0.022, padY: 0.022, minPadX: 8, minPadY: 8, radius: 0.11 }, + punchL: { padX: 0.022, padY: 0.022, minPadX: 8, minPadY: 8, radius: 0.11 }, + teardrop: { padX: 0.024, padY: 0.024, minPadX: 8, minPadY: 8, radius: 0.11 }, + android: { padX: 0.022, padY: 0.022, minPadX: 8, minPadY: 8, radius: 0.11 }, + tablet: { padX: 0.036, padY: 0.036, minPadX: 16, minPadY: 16, radius: 0.05 }, + tabletCam: { padX: 0.036, padY: 0.036, minPadX: 16, minPadY: 16, radius: 0.05 }, + flat: { padX: 0.018, padY: 0.018, minPadX: 6, minPadY: 6, radius: 0.08 }, }; - /** 常见预览设备 */ + /** 主流厂商代表性 / 较新机型(逻辑分辨率) */ var DEVICES = [ - { id: 'fit', name: '适应窗口', w: 0, h: 0, frame: false, shell: 'island' }, - { id: 'iphone-se', name: 'iPhone SE', w: 375, h: 667, frame: true, shell: 'classic' }, - { id: 'iphone-8', name: 'iPhone 8', w: 375, h: 667, frame: true, shell: 'classic' }, - { id: 'iphone-x', name: 'iPhone X / XS', w: 375, h: 812, frame: true, shell: 'notch' }, - { id: 'iphone-14', name: 'iPhone 14', w: 390, h: 844, frame: true, shell: 'notch' }, - { id: 'iphone-14-pro', name: 'iPhone 14 Pro', w: 393, h: 852, frame: true, shell: 'island' }, - { id: 'iphone-14-promax', name: 'iPhone 14 Pro Max', w: 430, h: 932, frame: true, shell: 'island' }, - { id: 'pixel-5', name: 'Pixel 5', w: 393, h: 851, frame: true, shell: 'android' }, - { id: 'galaxy-s20', name: 'Galaxy S20', w: 360, h: 800, frame: true, shell: 'android' }, - { id: 'ipad', name: 'iPad', w: 768, h: 1024, frame: true, shell: 'tablet' }, - { id: 'ipad-pro', name: 'iPad Pro 11', w: 834, h: 1194, frame: true, shell: 'tablet' }, - { id: 'custom-720', name: '720×1280', w: 720, h: 1280, frame: true, shell: 'android' }, - { id: 'custom-1080', name: '1080×1920', w: 1080, h: 1920, frame: true, shell: 'android' }, - { id: 'design', name: '设计分辨率', w: 750, h: 1600, frame: true, shell: 'island' }, + { id: 'fit', name: '适应窗口', w: 0, h: 0, frame: false, shell: 'flat', brand: '—' }, + // —— Apple iPhone —— + { id: 'iphone-se', name: 'iPhone SE 3', w: 375, h: 667, frame: true, shell: 'classic', brand: 'Apple' }, + { id: 'iphone-8', name: 'iPhone 8', w: 375, h: 667, frame: true, shell: 'classic', brand: 'Apple' }, + { id: 'iphone-x', name: 'iPhone X / XS', w: 375, h: 812, frame: true, shell: 'notch', brand: 'Apple' }, + { id: 'iphone-xr', name: 'iPhone XR / 11', w: 414, h: 896, frame: true, shell: 'notch', brand: 'Apple' }, + { id: 'iphone-12', name: 'iPhone 12 / 13', w: 390, h: 844, frame: true, shell: 'notch', brand: 'Apple' }, + { id: 'iphone-12p', name: 'iPhone 12 / 13 Pro', w: 390, h: 844, frame: true, shell: 'notch', brand: 'Apple' }, + { id: 'iphone-12pm', name: 'iPhone 12 / 13 Pro Max', w: 428, h: 926, frame: true, shell: 'notch', brand: 'Apple' }, + { id: 'iphone-14', name: 'iPhone 14', w: 390, h: 844, frame: true, shell: 'notch', brand: 'Apple' }, + { id: 'iphone-14-plus', name: 'iPhone 14 Plus', w: 428, h: 926, frame: true, shell: 'notch', brand: 'Apple' }, + { id: 'iphone-14-pro', name: 'iPhone 14 Pro', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' }, + { id: 'iphone-14-promax', name: 'iPhone 14 Pro Max', w: 430, h: 932, frame: true, shell: 'island', brand: 'Apple' }, + { id: 'iphone-15', name: 'iPhone 15 / 15 Plus', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' }, + { id: 'iphone-15-pro', name: 'iPhone 15 Pro', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' }, + { id: 'iphone-15-promax', name: 'iPhone 15 Pro Max', w: 430, h: 932, frame: true, shell: 'island', brand: 'Apple' }, + { id: 'iphone-16', name: 'iPhone 16', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' }, + { id: 'iphone-16-pro', name: 'iPhone 16 Pro', w: 402, y: 0, h: 874, frame: true, shell: 'island', brand: 'Apple' }, + { id: 'iphone-16-promax', name: 'iPhone 16 Pro Max', w: 440, h: 956, frame: true, shell: 'island', brand: 'Apple' }, + // —— Apple iPad —— + { id: 'ipad', name: 'iPad 10', w: 820, h: 1180, frame: true, shell: 'tabletCam', brand: 'Apple' }, + { id: 'ipad-9', name: 'iPad 9', w: 768, h: 1024, frame: true, shell: 'classic', brand: 'Apple' }, + { id: 'ipad-air', name: 'iPad Air', w: 820, h: 1180, frame: true, shell: 'tabletCam', brand: 'Apple' }, + { id: 'ipad-pro', name: 'iPad Pro 11', w: 834, h: 1194, frame: true, shell: 'tabletCam', brand: 'Apple' }, + { id: 'ipad-pro13', name: 'iPad Pro 12.9', w: 1024, h: 1366, frame: true, shell: 'tabletCam', brand: 'Apple' }, + // —— Samsung —— + { id: 'galaxy-s20', name: 'Galaxy S20', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' }, + { id: 'galaxy-s21', name: 'Galaxy S21 / S22', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' }, + { id: 'galaxy-s23', name: 'Galaxy S23 / S24', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' }, + { id: 'galaxy-s24u', name: 'Galaxy S24 Ultra', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Samsung' }, + { id: 'galaxy-note20', name: 'Galaxy Note 20', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Samsung' }, + { id: 'galaxy-a54', name: 'Galaxy A54', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' }, + // —— Google —— + { id: 'pixel-5', name: 'Pixel 5', w: 393, h: 851, frame: true, shell: 'punch', brand: 'Google' }, + { id: 'pixel-6', name: 'Pixel 6 / 7', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Google' }, + { id: 'pixel-8', name: 'Pixel 8 / 8 Pro', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Google' }, + // —— 小米 / Redmi —— + { id: 'mi-13', name: '小米 13 / 14', w: 393, h: 873, frame: true, shell: 'punch', brand: 'Xiaomi' }, + { id: 'mi-14-pro', name: '小米 14 Pro', w: 400, h: 882, frame: true, shell: 'punch', brand: 'Xiaomi' }, + { id: 'redmi-note12', name: 'Redmi Note 12 / 13', w: 360, h: 800, frame: true, shell: 'teardrop', brand: 'Xiaomi' }, + { id: 'redmi-k60', name: 'Redmi K60 / K70', w: 393, h: 873, frame: true, shell: 'punch', brand: 'Xiaomi' }, + // —— 华为 / 荣耀 —— + { id: 'huawei-mate60', name: 'Mate 60 Pro', w: 400, h: 880, frame: true, shell: 'punch', brand: 'Huawei' }, + { id: 'huawei-p60', name: 'P60 / nova', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Huawei' }, + { id: 'honor-magic6', name: '荣耀 Magic6', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Honor' }, + // —— OPPO / 一加 / realme —— + { id: 'oppo-find', name: 'Find X6 / X7', w: 360, h: 800, frame: true, shell: 'punch', brand: 'OPPO' }, + { id: 'oppo-reno', name: 'Reno 11', w: 360, h: 800, frame: true, shell: 'punch', brand: 'OPPO' }, + { id: 'oneplus-12', name: '一加 12', w: 450, h: 1000, frame: true, shell: 'punch', brand: 'OnePlus' }, + // —— vivo / iQOO —— + { id: 'vivo-x100', name: 'vivo X100', w: 412, h: 915, frame: true, shell: 'punch', brand: 'vivo' }, + { id: 'iqoo-12', name: 'iQOO 12', w: 393, h: 873, frame: true, shell: 'punch', brand: 'iQOO' }, + // —— 其它常见分辨率 —— + { id: 'android-360', name: 'Android 360×800', w: 360, h: 800, frame: true, shell: 'punch', brand: '通用' }, + { id: 'android-412', name: 'Android 412×915', w: 412, h: 915, frame: true, shell: 'punch', brand: '通用' }, + { id: 'android-720', name: '720×1280', w: 720, h: 1280, frame: true, shell: 'flat', brand: '通用' }, + { id: 'android-1080', name: '1080×1920', w: 1080, h: 1920, frame: true, shell: 'flat', brand: '通用' }, + { id: 'design', name: '设计分辨率', w: 750, h: 1600, frame: true, shell: 'island', brand: '通用' }, ]; /** @@ -42,6 +95,8 @@ function DevicePreview() { this.deviceId = 'iphone-x'; this.rotated = false; + this._capsuleOn = true; + this._capsuleStyle = 'douyin'; this._stage = null; this._bezel = null; this._screen = null; @@ -73,8 +128,13 @@ '' + '' + '' + + '
' + '' + - '
' + + '' + + '
' + '' + ''; @@ -86,8 +146,9 @@ stage.appendChild(scaleLabel); var screen = bezel.querySelector('#pw-device-screen'); + // 内容与挖孔/胶囊同在 screen 内,保证位置相对屏幕 if (contentEl.parentNode) contentEl.parentNode.insertBefore(stage, contentEl); - screen.appendChild(contentEl); + screen.insertBefore(contentEl, screen.firstChild); this._stage = stage; this._bezel = bezel; @@ -95,6 +156,7 @@ this._cutout = bezel.querySelector('#pw-device-cutout'); this._homeBar = bezel.querySelector('#pw-device-home-bar'); this._homeBtn = bezel.querySelector('#pw-device-home-btn'); + this._capsule = bezel.querySelector('#pw-device-capsule'); this._scaleLabel = scaleLabel; this._center = centerEl; @@ -125,6 +187,65 @@ try { localStorage.setItem('preview-workbench-device', this.deviceId); } catch (e) { /* */ } }; + /** 微信/抖音等小程序右上角胶囊按钮开关 */ + DevicePreview.prototype.setCapsule = function (on) { + this._capsuleOn = !!on; + if (this._capsule) { + this._capsule.hidden = !this._capsuleOn; + this._capsule.className = 'pw-device-capsule' + + (this._capsuleOn ? ' show' : '') + + ' cap-' + (this._capsuleStyle || 'wechat'); + this._capsule.setAttribute('data-style', this._capsuleStyle || 'wechat'); + } + try { localStorage.setItem('preview-workbench-capsule', this._capsuleOn ? '1' : '0'); } catch (e) { /* */ } + return this._capsuleOn; + }; + + /** 胶囊样式:none=不显示;douyin 默认。wechat / wechat-dark 对齐微信官方菜单 */ + DevicePreview.prototype.setCapsuleStyle = function (style) { + var allow = { + none: 1, douyin: 1, wechat: 1, 'wechat-dark': 1, + alipay: 1, qq: 1, kuaishou: 1, bilibili: 1, + }; + this._capsuleStyle = allow[style] ? style : 'douyin'; + // 「无」= 不显示;其它样式默认显示 + if (this._capsuleStyle === 'none') { + this._capsuleOn = false; + } else { + this._capsuleOn = true; + } + this.setCapsule(this._capsuleOn); + if (this._capsule) { + this._capsule.setAttribute('data-style', this._capsuleStyle); + this._capsule.className = 'pw-device-capsule' + + ((this._capsuleOn && this._capsuleStyle !== 'none') ? ' show' : '') + + ' cap-' + this._capsuleStyle; + } + try { localStorage.setItem('preview-workbench-capsule-style', this._capsuleStyle); } catch (e) { /* */ } + return this._capsuleStyle; + }; + + DevicePreview.prototype.getCapsuleStyle = function () { + return this._capsuleStyle || 'douyin'; + }; + + DevicePreview.prototype.isCapsule = function () { + return !!this._capsuleOn && this._capsuleStyle !== 'none'; + }; + + DevicePreview.capsuleStyles = function () { + return [ + { id: 'none', name: '无' }, + { id: 'douyin', name: '抖音' }, + { id: 'wechat', name: '微信·浅' }, + { id: 'wechat-dark', name: '微信·深' }, + { id: 'alipay', name: '支付宝' }, + { id: 'qq', name: 'QQ' }, + { id: 'kuaishou', name: '快手' }, + { id: 'bilibili', name: 'B站' }, + ]; + }; + DevicePreview.prototype.toggleRotate = function () { this.rotated = !this.rotated; this.apply(false); @@ -143,6 +264,108 @@ return SHELL[key] || SHELL.island; }; + /** + * 安全区(逻辑分辨率单位,与 w/h 同一坐标系)。 + * 供 Cocos SafeArea / getSafeAreaEdge 使用。 + */ + DevicePreview.prototype._safeOf = function (d) { + if (d && d.safe) { + return { + top: d.safe.top || 0, + bottom: d.safe.bottom || 0, + left: d.safe.left || 0, + right: d.safe.right || 0, + }; + } + var shell = (d && d.shell) || 'island'; + var h = (d && d.h) || 812; + var w = (d && d.w) || 375; + // 以 iPhone 点值为基准按高度比例换算 + var s = h / 852; + if (shell === 'island') { + return { top: Math.round(59 * s), bottom: Math.round(34 * s), left: 0, right: 0 }; + } + if (shell === 'notch') { + return { top: Math.round(47 * s), bottom: Math.round(34 * s), left: 0, right: 0 }; + } + if (shell === 'classic') { + return { top: 20, bottom: 0, left: 0, right: 0 }; + } + if (shell === 'punch' || shell === 'punchL' || shell === 'android') { + return { top: Math.round(28 * s), bottom: Math.round(16 * s), left: 0, right: 0 }; + } + if (shell === 'teardrop') { + return { top: Math.round(24 * s), bottom: Math.round(12 * s), left: 0, right: 0 }; + } + if (shell === 'tablet' || shell === 'tabletCam') { + return { top: 24, bottom: 20, left: 0, right: 0 }; + } + return { top: 0, bottom: 0, left: 0, right: 0 }; + }; + + /** 把安全区注入引擎,供 cc.SafeArea / view.getSafeAreaEdge 使用 */ + DevicePreview.prototype._applySafeArea = function () { + var cc = global.cc; + if (!cc) return; + var d = this._find(); + var safe = this._safeOf(d); + if (this.rotated) { + // 横屏:上下安全区对应侧边 + safe = { + top: 0, + bottom: 0, + left: this._safeOf(d).bottom, + right: 0, + }; + // 保持 notch/island 在横屏左侧时的上/左边距 + var base = this._safeOf(d); + safe = { top: 0, bottom: 0, left: base.bottom || 0, right: base.top || 0 }; + } + this._safeEdge = safe; + + var applyEdge = function () { + return { + top: safe.top, + bottom: safe.bottom, + left: safe.left, + right: safe.right, + }; + }; + + try { + if (cc.view) { + cc.view.getSafeAreaEdge = applyEdge; + if (typeof cc.view.getSafeAreaRect === 'function') { + var origRect = cc.view.getSafeAreaRect; + cc.view.getSafeAreaRect = function () { + var vs = cc.view.getVisibleSize ? cc.view.getVisibleSize() : { width: 0, height: 0 }; + var origin = cc.view.getVisibleOrigin ? cc.view.getVisibleOrigin() : { x: 0, y: 0 }; + var e = applyEdge(); + // 逻辑点:与设计分辨率同一尺度(view 一般用设计/可见单位) + return { + x: origin.x + e.left, + y: origin.y + e.bottom, + width: Math.max(1, vs.width - e.left - e.right), + height: Math.max(1, vs.height - e.top - e.bottom), + contains: origRect ? undefined : undefined, + }; + }; + } + } + // 部分机型走 screen.safeAreaEdge + if (global.screen && global.screen.safeAreaEdge !== undefined) { + global.screen.safeAreaEdge = applyEdge(); + } + } catch (e) { /* */ } + + // 胶囊:贴齐安全区右上 + if (this._capsule) { + var capTop = Math.max(6, safe.top + 6); + this._capsule.style.top = capTop + 'px'; + this._capsule.style.right = '10px'; + } + }; + /** * @param {string} shell * @param {boolean} useFrame @@ -159,10 +382,16 @@ if (landscape) bezel.classList.add('is-landscape'); if (shell === 'notch' && !landscape) bezel.classList.add('has-cutout', 'has-notch'); if (shell === 'island' && !landscape) bezel.classList.add('has-cutout', 'has-island'); - if (shell === 'android' && !landscape) bezel.classList.add('has-cutout', 'has-punch'); + if ((shell === 'punch' || shell === 'android') && !landscape) { + bezel.classList.add('has-cutout', 'has-punch'); + } + if (shell === 'punchL' && !landscape) bezel.classList.add('has-cutout', 'has-punch', 'has-punch-l'); + if (shell === 'teardrop' && !landscape) bezel.classList.add('has-cutout', 'has-teardrop'); if (shell === 'classic') bezel.classList.add('has-home-btn'); - if (shell === 'notch' || shell === 'island') bezel.classList.add('has-home-bar'); - if (shell === 'tablet') bezel.classList.add('has-home-bar'); + if (shell === 'notch' || shell === 'island' || shell === 'tablet' || shell === 'tabletCam') { + bezel.classList.add('has-home-bar'); + } + if (shell === 'tabletCam' && !landscape) bezel.classList.add('has-tablet-cam'); }; /** @@ -272,6 +501,7 @@ this._bezel.style.transform = 'none'; this._fillScreen(screenW, screenH); + this._applySafeArea(); var key = this.deviceId + '|' + (this.rotated ? 1 : 0) + '|' + screenW + 'x' + screenH + '|' + stageW + 'x' + stageH; var changed = key !== this._lastKey; @@ -302,6 +532,7 @@ if (!w || !h) return; this._frameW = w; this._frameH = h; + this._applySafeArea(); var cc = global.cc; try { @@ -343,6 +574,17 @@ try { var id = localStorage.getItem('preview-workbench-device'); if (id) this.deviceId = id; + var capStyle = localStorage.getItem('preview-workbench-capsule-style'); + this._capsuleStyle = capStyle || 'douyin'; + // 默认显示抖音;仅样式为 none 时隐藏 + this._capsuleOn = this._capsuleStyle !== 'none'; + if (this._capsule) { + this._capsule.hidden = !this._capsuleOn; + this._capsule.setAttribute('data-style', this._capsuleStyle); + this._capsule.className = 'pw-device-capsule' + + (this._capsuleOn ? ' show' : '') + + ' cap-' + this._capsuleStyle; + } } catch (e) { /* */ } }; diff --git a/preview-template/workbench/js/host.js b/preview-template/workbench/js/host.js index b54c28d..f4ccc52 100644 --- a/preview-template/workbench/js/host.js +++ b/preview-template/workbench/js/host.js @@ -254,10 +254,11 @@ self._setPickHighlight(rect); }); b.on('perf.sample', function (data) { - if (self.panels.perf) self.panels.perf.applySample(data); + if (self.panels.perf && self.panels.perf.applySample) self.panels.perf.applySample(data); }); b.on('assets.update', function (data) { if (self.panels.assets) self.panels.assets.applyData(data); + if (self.panels.perf && self.panels.perf.applyAssets) self.panels.perf.applyAssets(data); }); b.on('console.batch', function (data) { if (self.panels.console) self.panels.console.applyBatch(data && data.items); @@ -300,6 +301,12 @@ this.bridge.unsubscribe(ch); } } + // 性能面板可见时启动函数采样剖析 + if (map.perf && this.bridge) { + this.bridge.call('prof.start').catch(function () { /* */ }); + } else if (this.bridge) { + this.bridge.call('prof.stop').catch(function () { /* */ }); + } var self = this; if (map.hierarchy) { this.bridge.call('hier.getTree').then(function (tree) { @@ -373,6 +380,36 @@ }); }); } + var capBtn = document.getElementById('pw-device-capsule-btn'); + if (capBtn) { + var syncCap = function (on) { + capBtn.classList.toggle('active', !!on); + capBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); + }; + capBtn.addEventListener('click', function () { + if (!self.bridge) return; + self.bridge.call('device.getState').then(function (st) { + var next = !(st && st.capsule); + return self.bridge.call('device.setCapsule', { on: next }).then(function (on) { + syncCap(on); + }); + }).catch(function () { /* */ }); + }); + if (self.bridge) { + self.bridge.call('device.getState').then(function (st) { + syncCap(st && st.capsule); + }).catch(function () { /* */ }); + } + } + var capStyle = document.getElementById('pw-capsule-style'); + if (capStyle && self.bridge) { + capStyle.addEventListener('change', function () { + self.bridge.call('device.setCapsuleStyle', { style: capStyle.value }).catch(function () { /* */ }); + }); + self.bridge.call('device.getCapsuleStyle').then(function (s) { + if (s) capStyle.value = s; + }).catch(function () { /* */ }); + } var refreshBtn = document.getElementById('pw-game-refresh'); if (refreshBtn) { @@ -647,12 +684,20 @@ }; /** - * 选中 Gizmo:半透明框可拖;红/绿箭头沿 X/Y 轴拖。 - * 坐标来自 pick.getRect(含 pos / scale)。 + * 选中高亮: + * - 游戏模式:只保留游戏内绿框,不显示坐标轴、禁止拖动 + * - 检视模式:绿框 + 中心坐标轴;点击循环选中,拖动才移动 */ PreviewWorkbenchHost.prototype._setPickHighlight = function (rect) { var gizmo = document.getElementById('pw-pick-gizmo'); if (!gizmo) return; + + // 非检视:隐藏 Gizmo(绿框由游戏内 overlay 绘制) + if (!this._inspectOn) { + gizmo.hidden = true; + this._gizmoState = null; + return; + } if (!rect || !(rect.width > 0) || !(rect.height > 0)) { gizmo.hidden = true; this._gizmoState = null; @@ -681,19 +726,23 @@ }; }; - /** Gizmo 拖拽:xy / 仅 x / 仅 y */ + /** + * Gizmo:位移阈值内视为点击(循环选中),超过阈值才拖动改坐标。 + * 轴从节点中心画出。 + */ PreviewWorkbenchHost.prototype._bindGizmoDrag = function (gizmo) { var self = this; - var drag = null; + var pending = null; + var THRESHOLD = 4; var onDown = function (ev) { if (ev.button != null && ev.button !== 0) return; + if (!self._inspectOn) return; var axis = ev.currentTarget.getAttribute('data-drag') || 'xy'; var st = self._gizmoState; if (!st || !st.uuid) return; - ev.preventDefault(); - ev.stopPropagation(); - drag = { + // 先不拦截,留给点击;真拖动再 prevent + pending = { axis: axis, x0: ev.clientX, y0: ev.clientY, @@ -704,51 +753,90 @@ uuid: st.uuid, left0: parseFloat(gizmo.style.left) || 0, top0: parseFloat(gizmo.style.top) || 0, + dragging: false, }; document.addEventListener('pointermove', onMove); document.addEventListener('pointerup', onUp); - gizmo.classList.add('dragging'); }; var onMove = function (ev) { - if (!drag) return; + if (!pending) return; + var dx = ev.clientX - pending.x0; + var dy = ev.clientY - pending.y0; + if (!pending.dragging) { + if (Math.abs(dx) < THRESHOLD && Math.abs(dy) < THRESHOLD) return; + pending.dragging = true; + ev.preventDefault(); + ev.stopPropagation(); + gizmo.classList.add('dragging'); + } ev.preventDefault(); - var dx = ev.clientX - drag.x0; - var dy = ev.clientY - drag.y0; - var nx = drag.px; - var ny = drag.py; - if (drag.axis !== 'y') nx = drag.px + dx * drag.sx; - if (drag.axis !== 'x') ny = drag.py - dy * drag.sy; + var nx = pending.px; + var ny = pending.py; + if (pending.axis !== 'y') nx = pending.px + dx * pending.sx; + if (pending.axis !== 'x') ny = pending.py - dy * pending.sy; nx = Math.round(nx * 100) / 100; ny = Math.round(ny * 100) / 100; if (self.bridge) { - if (drag.axis !== 'y') { - self.bridge.call('node.set', { uuid: drag.uuid, fid: 'px', value: nx }); + if (pending.axis !== 'y') { + self.bridge.call('node.set', { uuid: pending.uuid, fid: 'px', value: nx }); } - if (drag.axis !== 'x') { - self.bridge.call('node.set', { uuid: drag.uuid, fid: 'py', value: ny }); + if (pending.axis !== 'x') { + self.bridge.call('node.set', { uuid: pending.uuid, fid: 'py', value: ny }); } } - // Gizmo 跟手 - if (drag.axis !== 'y') gizmo.style.left = (drag.left0 + dx) + 'px'; - if (drag.axis !== 'x') gizmo.style.top = (drag.top0 + dy) + 'px'; + if (pending.axis !== 'y') gizmo.style.left = (pending.left0 + dx) + 'px'; + if (pending.axis !== 'x') gizmo.style.top = (pending.top0 + dy) + 'px'; if (self._gizmoState) { - self._gizmoState.pos = { x: nx, y: ny, z: (self._gizmoState.pos && self._gizmoState.pos.z) || 0 }; + self._gizmoState.pos = { + x: nx, + y: ny, + z: (self._gizmoState.pos && self._gizmoState.pos.z) || 0, + }; } }; - var onUp = function () { - if (!drag) return; - var uuid = drag.uuid; - drag = null; - gizmo.classList.remove('dragging'); + var onUp = function (ev) { document.removeEventListener('pointermove', onMove); document.removeEventListener('pointerup', onUp); + if (!pending) return; + var st = pending; + pending = null; + gizmo.classList.remove('dragging'); + + if (!st.dragging) { + // 视为点击 → 循环选中重叠节点 + var iframe = self._iframe; + var gameX = ev.clientX; + var gameY = ev.clientY; + if (iframe) { + var r = iframe.getBoundingClientRect(); + gameX = ev.clientX - r.left; + gameY = ev.clientY - r.top; + } + if (self.bridge) { + self.bridge.call('pick.at', { clientX: gameX, clientY: gameY, space: 'viewport' }) + .then(function (res) { + if (res && res.uuid) { + if (self.panels.inspector) self.panels.inspector.loadUuid(res.uuid); + if (self.panels.hierarchy) self.panels.hierarchy.setSelectedUuid(res.uuid); + } + return self.bridge.call('pick.getRect'); + }) + .then(function (rect) { + self._setPickHighlight(rect); + }) + .catch(function () { /* */ }); + } + return; + } + + // 真正拖动结束 → 刷新框与属性 if (self.bridge) { self.bridge.call('pick.getRect').then(function (r) { self._setPickHighlight(r); }).catch(function () { /* */ }); - if (self.panels.inspector) self.panels.inspector.loadUuid(uuid); + if (self.panels.inspector) self.panels.inspector.loadUuid(st.uuid); } }; diff --git a/preview-template/workbench/js/panels/assets.js b/preview-template/workbench/js/panels/assets.js index a1f304d..59ada5f 100644 --- a/preview-template/workbench/js/panels/assets.js +++ b/preview-template/workbench/js/panels/assets.js @@ -130,7 +130,8 @@ '' ); @@ -155,7 +156,11 @@ var bf = this._bundleFilter; var tf = this._typeFilter; rows = rows.filter(function (r) { - if (bf && r.bundle !== bf) return false; + if (bf) { + var inBundle = r.bundle === bf; + var used = r.usedBy && r.usedBy.indexOf(bf) >= 0; + if (!inBundle && !used) return false; + } if (tf && r.type !== tf && r.kind !== tf) return false; if (q && String(r.name).toLowerCase().indexOf(q) < 0 && String(r.type).toLowerCase().indexOf(q) < 0) return false; return true; @@ -203,6 +208,7 @@ var tag = ''; if (r.pack) tag += ' 图集'; if (r.type === 'Texture2D' && r.pack) tag += ' 合包'; + if (r.via === 'dep') tag += ' 依赖'; out.push( '
' + '' + escapeHtml(r.name) + tag + '' + diff --git a/preview-template/workbench/js/panels/perf.js b/preview-template/workbench/js/panels/perf.js index 19b2905..444a89f 100644 --- a/preview-template/workbench/js/panels/perf.js +++ b/preview-template/workbench/js/panels/perf.js @@ -2,17 +2,23 @@ 'use strict'; /** - * 性能面板(壳侧):采样在游戏 Agent,曲线只在壳 document 绘制。 + * 性能面板(壳侧): + * 1) 帧率 / 内存曲线 + * 2) 函数耗时 Top20 + * 3) 慢帧堆栈 Top10 + * 4) 内存资源 Top20(可复制名称 / UUID,可预览) * @constructor */ function PerfPanel() { this.el = null; - this.charts = {}; this._bridge = null; + this._chartFps = null; + this._chartMem = null; this._data = null; - this._renderTimer = 0; - this._dirty = false; this._paused = false; + this._profiling = true; + this._pollTimer = 0; + this._assetRows = []; } PerfPanel.prototype.setBridge = function (bridge) { @@ -22,189 +28,273 @@ PerfPanel.prototype.mount = function () { var self = this; var root = document.createElement('div'); - root.className = 'pw-panel-inner'; + root.className = 'pw-panel-inner pw-perf'; root.style.height = '100%'; root.style.display = 'flex'; root.style.flexDirection = 'column'; root.innerHTML = - '
' + - '性能' + - '
' + + '
' + + '' + '' + '' + '' + - '' + + '' + '' + - '' + - '' + - '
' + - '
' + - '
' + - '
' + - '
' + + '
' + + '
' + + '
' + + '

帧率 FPS

' + + '

总内存 (MB)

' + + '
' + + '
' + + '
' + + '

函数耗时 Top20

' + + '
' + + '
' + + '
' + + '

慢帧堆栈 Top10

' + + '
' + + '
' + + '
' + + '
' + + '

内存资源 Top20

' + + '
' + + '
' + '
'; this.el = root; - var defs = [ - { key: 'fps', title: '帧率 FPS', color: '#89d185' }, - { key: 'frame', title: '帧耗时 (ms)', color: '#3794ff' }, - { key: 'logic', title: '逻辑 (ms)', color: '#4ec9b0' }, - { key: 'render', title: '渲染 (ms)', color: '#f0a020' }, - { key: 'present', title: '呈现 (ms)', color: '#c586c0' }, - { key: 'draws', title: 'DrawCall', color: '#dcdcaa' }, - { key: 'tricount', title: '三角面', color: '#9cdcfe' }, - { key: 'textureMemory', title: '纹理显存 (MB)', color: '#f14c4c' }, - { key: 'bufferMemory', title: '缓冲显存 (MB)', color: '#ce9178' }, - { key: 'jsHeap', title: 'JS 堆 (MB)', color: '#b5cea8' }, - { key: 'assetCount', title: '资源数量', color: '#569cd6' }, - ]; - var chartsEl = root.querySelector('#pw-perf-charts'); - for (var i = 0; i < defs.length; i++) { - var d = defs[i]; - var card = document.createElement('div'); - card.className = 'pw-chart-card'; - card.innerHTML = '

' + d.title + '

'; - chartsEl.appendChild(card); - var canvas = card.querySelector('canvas'); - var chart = new global.PWRingChart(canvas); - chart.color = d.color; - chart.fill = hexToRgba(d.color, 0.15); - this.charts[d.key] = chart; - } + this._chartFps = new global.PWRingChart(root.querySelector('#pw-ch-fps')); + this._chartFps.color = '#3dd68c'; + this._chartFps.fill = 'rgba(61,218,140,0.12)'; + this._chartMem = new global.PWRingChart(root.querySelector('#pw-ch-mem')); + this._chartMem.color = '#5b9fd4'; + this._chartMem.fill = 'rgba(91,159,212,0.12)'; - root.querySelector('#pw-perf-pause').addEventListener('click', function (ev) { + root.querySelector('#pw-perf-pause').addEventListener('click', function () { self._paused = !self._paused; - if (self._bridge) self._bridge.call('perf.setPaused', { paused: self._paused }); - ev.currentTarget.textContent = self._paused ? '继续' : '暂停'; - ev.currentTarget.classList.toggle('active', self._paused); + this.textContent = self._paused ? '继续' : '暂停'; + this.classList.toggle('active', self._paused); }); root.querySelector('#pw-perf-clear').addEventListener('click', function () { - if (self._bridge) self._bridge.call('perf.clear'); - self._data = null; - self.render(); + if (!self._bridge) return; + self._bridge.call('perf.clear'); + self._bridge.call('prof.clear'); + self._fnCache = []; + self._slowCache = []; + self._renderReports({ functions: [], slowFrames: [] }); }); root.querySelector('#pw-perf-mark').addEventListener('click', function () { - var text = root.querySelector('#pw-perf-mark-text').value; - if (self._bridge) self._bridge.call('perf.mark', { text: text }); + if (!self._bridge) return; + var t = root.querySelector('#pw-perf-mark-text'); + self._bridge.call('perf.mark', { text: (t && t.value) || '' }); + if (t) t.value = ''; }); root.querySelector('#pw-perf-csv').addEventListener('click', function () { - var csv = self.toCsv(); - var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); - var a = document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = 'workbench-perf.csv'; - a.click(); - URL.revokeObjectURL(a.href); - }); - root.querySelector('#pw-perf-window').addEventListener('change', function (ev) { - if (self._bridge) self._bridge.call('perf.setCapacity', { n: parseInt(ev.target.value, 10) || 300 }); + if (!self._bridge) return; + self._bridge.call('perf.get').then(function (d) { + var csv = 't,fps,frame,jsHeap,totalMemory\n'; + if (d && d.times) { + for (var i = 0; i < d.times.length; i++) { + var s = d.series || {}; + csv += d.times[i] + ',' + + ((s.fps && s.fps[i]) || 0) + ',' + + ((s.frame && s.frame[i]) || 0) + ',' + + ((s.jsHeap && s.jsHeap[i]) || 0) + ',' + + ((s.textureMemory && s.textureMemory[i] || 0) + (s.bufferMemory && s.bufferMemory[i] || 0) + (s.jsHeap && s.jsHeap[i] || 0)) + '\n'; + } + } + var blob = new Blob([csv], { type: 'text/csv' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'perf.csv'; + a.click(); + }); }); - root.querySelector('#pw-perf-interval').addEventListener('change', function (ev) { - if (self._bridge) self._bridge.call('perf.setInterval', { ms: parseInt(ev.target.value, 10) || 500 }); + root.querySelector('#pw-perf-prof').addEventListener('click', function () { + self._profiling = !self._profiling; + this.classList.toggle('active', self._profiling); + this.textContent = self._profiling ? '剖析中' : '已停止'; + if (!self._bridge) return; + self._bridge.call(self._profiling ? 'prof.start' : 'prof.stop'); }); - document.addEventListener('keydown', function (ev) { - if (ev.altKey && (ev.key === 'm' || ev.key === 'M')) { - if (self._bridge) self._bridge.call('perf.mark', { text: '' }); - } - }); - - // 绘图与采样解耦:最多约 5fps 重绘 - this._renderTimer = setInterval(function () { - if (self._dirty) { - self._dirty = false; - self.render(); - } - }, 200); - + this._startPoll(); return root; }; - PerfPanel.prototype.applySample = function (data) { - this._data = data; - this._dirty = true; + PerfPanel.prototype._startPoll = function () { + var self = this; + if (this._pollTimer) clearInterval(this._pollTimer); + this._pollTimer = setInterval(function () { + if (self._paused || !self._bridge) return; + self._bridge.call('perf.get').then(function (d) { + self._data = d; + self._drawCurves(d); + }).catch(function () { /* */ }); + self._bridge.call('prof.report').then(function (r) { + self._renderReports(r); + }).catch(function () { /* */ }); + }, 500); }; - PerfPanel.prototype.render = function () { - if (!this.el) return; - var s = this._data; - var latest = (s && s.latest) || {}; - var sum = this.el.querySelector('#pw-perf-summary'); - if (sum) { - var fpsArr = (s && s.series && s.series.fps) || []; - var avg = average(fpsArr); - var min = minOf(fpsArr); - var max = maxOf(fpsArr); - var low = 0; - for (var i = 0; i < fpsArr.length; i++) if (fpsArr[i] < 30) low++; - var lowPct = fpsArr.length ? Math.round((low / fpsArr.length) * 100) : 0; - sum.innerHTML = - '帧率 ' + Math.round(latest.fps || 0) + ' 均 ' + Math.round(avg) + ' 最低 ' + Math.round(min) + ' 最高 ' + Math.round(max) + '' + - '<30帧占比 ' + lowPct + '%' + - 'DrawCall ' + Math.round(latest.draws || 0) + '' + - '帧耗时 ' + (Math.round((latest.frame || 0) * 10) / 10) + 'ms' + - '纹理 ' + (Math.round((latest.textureMemory || 0) * 100) / 100) + 'MB' + - '缓冲 ' + (Math.round((latest.bufferMemory || 0) * 100) / 100) + 'MB' + - '堆内存 ' + (Math.round((latest.jsHeap || 0) * 100) / 100) + 'MB'; + PerfPanel.prototype._drawCurves = function (d) { + if (!d) return; + var fpsArr = (d.series && d.series.fps) || []; + var memArr = []; + var ts = d.times || []; + var s = d.series || {}; + for (var i = 0; i < ts.length; i++) { + var t = (s.textureMemory && s.textureMemory[i]) || 0; + var b = (s.bufferMemory && s.bufferMemory[i]) || 0; + var h = (s.jsHeap && s.jsHeap[i]) || 0; + memArr.push(Math.round((t + b + h) * 10) / 10); } - var markEl = this.el.querySelector('#pw-perf-markers'); - if (markEl) { - var marks = (s && s.markers) ? s.markers.slice(-8) : []; - markEl.textContent = marks.length - ? ('标记:' + marks.map(function (m) { return m.label; }).join(' | ')) - : '标记:(自动 scene/bundle + Alt+M 手动)'; + if (this._chartFps) this._chartFps.draw(fpsArr, [], ts); + if (this._chartMem) this._chartMem.draw(memArr, [], ts); + }; + + PerfPanel.prototype._renderReports = function (r) { + if (!this.el || !r) return; + var fnEl = this.el.querySelector('#pw-fn-list'); + var slowEl = this.el.querySelector('#pw-slow-list'); + var fnC = this.el.querySelector('#pw-fn-count'); + var slowC = this.el.querySelector('#pw-slow-count'); + + var fns = r.functions || []; + if (fnC) fnC.textContent = fns.length ? (fns.length + ' 项 · 单位 ms') : ''; + if (fnEl) { + if (!fns.length) { + fnEl.innerHTML = '
点击「剖析中」开始采样(约 8ms 一帧栈)
'; + } else { + var html = ['
函数次数估算ms
']; + for (var i = 0; i < fns.length; i++) { + html.push( + '
' + + '' + escapeHtml(shorten(fns[i].name, 42)) + '' + + '' + fns[i].count + '' + + '' + fns[i].ms + '' + + '
' + ); + } + fnEl.innerHTML = html.join(''); + } } - if (!s || !s.series) return; - for (var key in this.charts) { - if (!Object.prototype.hasOwnProperty.call(this.charts, key)) continue; - this.charts[key].draw(s.series[key] || [], s.markers || [], s.times || []); + + var slows = r.slowFrames || []; + if (slowC) slowC.textContent = slows.length ? (slows.length + ' 次 >20ms') : ''; + if (slowEl) { + if (!slows.length) { + slowEl.innerHTML = '
暂无慢帧(阈值 20ms)
'; + } else { + var sh = ['
耗时堆栈
']; + for (var j = 0; j < slows.length; j++) { + var st = escapeHtml(String(slows[j].stack || '').split('\n').slice(0, 3).join(' | ')); + sh.push( + '
' + + '' + slows[j].ms + 'ms' + + '' + st + '' + + '
' + ); + } + slowEl.innerHTML = sh.join(''); + } } }; - PerfPanel.prototype.toCsv = function () { - var s = this._data; - if (!s || !s.series) return ''; - var keys = Object.keys(s.series); - var lines = ['t,' + keys.join(',')]; - for (var i = 0; i < (s.times || []).length; i++) { - var row = [s.times[i]]; - for (var k = 0; k < keys.length; k++) row.push(s.series[keys[k]][i]); - lines.push(row.join(',')); + /** 内存资源 Top20 */ + PerfPanel.prototype.applyAssets = function (data) { + this._assetRows = (data && data.rows) || []; + this._renderAssets(); + }; + + PerfPanel.prototype._renderAssets = function () { + var el = this.el && this.el.querySelector('#pw-res-list'); + var cnt = this.el && this.el.querySelector('#pw-res-count'); + if (!el) return; + var rows = this._assetRows.slice(); + rows.sort(function (a, b) { return (b.mem || 0) - (a.mem || 0); }); + rows = rows.slice(0, 20); + if (cnt) cnt.textContent = rows.length + ' 项'; + if (!rows.length) { + el.innerHTML = '
打开「资源」面板或稍候自动加载
'; + return; + } + var html = ['
名称 / UUID类型内存操作
']; + for (var i = 0; i < rows.length; i++) { + var r = rows[i]; + html.push( + '
' + + '' + escapeHtml(r.name) + '
' + escapeHtml(r.uuid) + '
' + + '' + escapeHtml(r.type) + '' + + '' + fmtMB(r.mem) + '' + + '' + + '' + + '' + + '' + + '
' + ); } - return lines.join('\n'); + el.innerHTML = html.join(''); + + var self = this; + el.addEventListener('click', function (ev) { + var btn = ev.target.closest ? ev.target.closest('button') : null; + if (!btn) return; + var item = btn.closest('.pw-res-item'); + if (!item) return; + var uuid = item.getAttribute('data-uuid') || ''; + var name = (item.querySelector('b') && item.querySelector('b').textContent) || ''; + if (btn.getAttribute('data-copy') === 'name') copyText(name); + else if (btn.getAttribute('data-copy') === 'uuid') copyText(uuid); + else if (btn.getAttribute('data-preview') && self._bridge) { + self._bridge.call('assets.preview', { uuid: uuid }).then(function (p) { + var box = document.getElementById('pw-asset-preview'); + if (!box) return; + if (p && p.kind === 'image' && p.url) { + box.innerHTML = ''; + } else if (p && p.kind === 'url' && p.url) { + box.innerHTML = ''; + } else { + box.innerHTML = '无预览'; + } + }).catch(function () { /* */ }); + } + }); }; - function average(arr) { - if (!arr.length) return 0; - var t = 0; - for (var i = 0; i < arr.length; i++) t += arr[i]; - return t / arr.length; + function fmtMB(mb) { + var n = Number(mb) || 0; + if (n <= 0) return '0'; + if (n < 10) return (Math.round(n * 100) / 100) + 'MB'; + return (Math.round(n * 10) / 10) + 'MB'; + } + + function shorten(s, n) { + s = String(s || ''); + return s.length > n ? s.slice(0, n - 1) + '…' : s; } - function minOf(arr) { - if (!arr.length) return 0; - var m = arr[0]; - for (var i = 1; i < arr.length; i++) if (arr[i] < m) m = arr[i]; - return m; + + function escapeHtml(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&').replace(//g, '>'); } - function maxOf(arr) { - if (!arr.length) return 0; - var m = arr[0]; - for (var i = 1; i < arr.length; i++) if (arr[i] > m) m = arr[i]; - return m; + function escapeAttr(s) { + return escapeHtml(s).replace(/"/g, '"'); } - function hexToRgba(hex, a) { - var h = hex.replace('#', ''); - var r = parseInt(h.slice(0, 2), 16); - var g = parseInt(h.slice(2, 4), 16); - var b = parseInt(h.slice(4, 6), 16); - return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; + + function copyText(text) { + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(String(text)); + return; + } + } catch (e) { /* */ } + var ta = document.createElement('textarea'); + ta.value = String(text); + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); } catch (e2) { /* */ } + document.body.removeChild(ta); } global.PWPerfPanel = PerfPanel; diff --git a/配置表/excelList/技能表.xlsx b/配置表/excelList/技能表.xlsx index 15c55e7639eae9143105c64cf042e046bf67e6aa..109bcfef86ecd1bde11e805fd7e38f1afd5788f2 100644 GIT binary patch delta 20708 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 K)?~-y(f