You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
532 lines
21 KiB
532 lines
21 KiB
import type { BattleSession } from 'db://assets/mx/module/battle';
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
import {
|
|
AbilitySetComp,
|
|
AtkHookComp,
|
|
CombatComp,
|
|
UnitMetaComp,
|
|
} from 'db://assets/mx/module/battle/ecs/Components';
|
|
import {
|
|
entityHeroAbilityId,
|
|
entityNormalAbilityId,
|
|
entityWeaponAbilityId,
|
|
normalAbilityId,
|
|
} from '../config/weapon1/Weapon1BattleIds';
|
|
import type { Player } from '../../game/Player';
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
import { deriveTowerSetupStats } from './TowerStatResolver';
|
|
import type { TowerLoadout } from './TowerQueryTypes';
|
|
|
|
export type { TowerLoadout };
|
|
import { SkillAbilityBuilder } from '../config/baseAtk/SkillAbilityBuilder';
|
|
import { heroNormalSkillId, heroUltSkillId, isElementHero } from '../config/BattleHeroTable';
|
|
import { dumpKnifeFinalAttack, logKnifeBaseAttack } from '../debug/AtkDebugLogCore';
|
|
import { isAtkDebugWeapon } from '../debug/AtkDebugConstants';
|
|
import { getPassiveSkillHost } from './PassiveSkillRuntime';
|
|
import { SkillAtkBuffExecutor } from './SkillAtkBuffExecutor';
|
|
import { TableAnomaly } from '../../game/TableAnomaly';
|
|
import { GemBattleEffect } from './GemBattleEffect';
|
|
|
|
/** AI 侧武将武器解析(由 AiBattleBridge start/stop 注册,避免循环 require) */
|
|
let _aiHeroWeaponResolver: ((heroId: number) => number | null) | null = null;
|
|
|
|
/**
|
|
* 注册/清除 AI 武将武器解析器。
|
|
* @param resolver 解析函数;传 null 清除
|
|
*/
|
|
export function setAiHeroWeaponResolver(resolver: ((heroId: number) => number | null) | null): void {
|
|
_aiHeroWeaponResolver = resolver;
|
|
}
|
|
|
|
/**
|
|
* 解析英雄当前战斗武器/技能宝石 id。
|
|
* 五行数字:本机读镶嵌技能宝石;非五行:本机装备 / AI 掷骰。
|
|
*/
|
|
export function resolveHeroBattleWeaponId(heroId: number, player?: Player | null): number {
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
|
|
if (isElementHero(hero)) {
|
|
if (player?.isLocal) {
|
|
return GemBattleEffect.resolve(Math.floor(Number(hero!.herotype)), player).skillGemId;
|
|
}
|
|
return 0;
|
|
}
|
|
if (player && !player.isLocal) {
|
|
if (_aiHeroWeaponResolver) {
|
|
const aiId = _aiHeroWeaponResolver(heroId);
|
|
if (typeof aiId === 'number' && aiId > 0) return aiId;
|
|
}
|
|
return 0;
|
|
}
|
|
try {
|
|
const id = gg.equip?.getHeroDisplayWeaponId?.(heroId);
|
|
if (typeof id === 'number' && id > 0) return id;
|
|
} catch { /* equip 未就绪 */ }
|
|
return 0;
|
|
}
|
|
|
|
export class TowerLoadoutService {
|
|
private readonly _byEntity: { entityId: EntityId; loadout: TowerLoadout }[] = [];
|
|
|
|
private _findIndex(entityId: EntityId): number {
|
|
for (let i = 0; i < this._byEntity.length; i++) {
|
|
if (this._byEntity[i].entityId === entityId) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
initEntity(entityId: EntityId, heroId: number, level: number, player: Player): void {
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
|
|
if (!hero) return;
|
|
let weaponId = 0;
|
|
let weaponSkillId = 0;
|
|
if (isElementHero(hero)) {
|
|
const gem = GemBattleEffect.resolve(Math.floor(Number(hero.herotype)), player);
|
|
weaponId = gem.skillGemId;
|
|
weaponSkillId = gem.skillId;
|
|
} else {
|
|
weaponId = resolveHeroBattleWeaponId(heroId, player);
|
|
const weaponRow = weaponId > 0
|
|
? gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, weaponId)
|
|
: null;
|
|
weaponSkillId = Number(weaponRow?.act_skill) || 0;
|
|
}
|
|
let weaponSkillActive = false;
|
|
if (weaponSkillId > 0) {
|
|
const sk = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, weaponSkillId);
|
|
if (!sk) {
|
|
console.log(`[TowerLoadout] 武器技能缺失 Skill.id=${weaponSkillId} weaponId=${weaponId}`);
|
|
} else {
|
|
weaponSkillActive = Number(sk.type) === 1;
|
|
}
|
|
}
|
|
// 五行每行自带技能数组;非五行仍按合成等级取下标
|
|
const skillLv = isElementHero(hero) ? 1 : level;
|
|
const loadout: TowerLoadout = {
|
|
weapon1Id: heroId,
|
|
level,
|
|
weaponId,
|
|
normalBaseAtkId: heroNormalSkillId(hero, skillLv),
|
|
overAtkType: '',
|
|
overAtkPara: '',
|
|
activeSkillBaseAtkId: heroUltSkillId(hero, skillLv),
|
|
weaponSkillId,
|
|
weaponSkillActive,
|
|
};
|
|
const idx = this._findIndex(entityId);
|
|
if (idx >= 0) this._byEntity[idx].loadout = loadout;
|
|
else this._byEntity.push({ entityId, loadout });
|
|
}
|
|
|
|
removeEntity(entityId: EntityId): void {
|
|
const idx = this._findIndex(entityId);
|
|
if (idx >= 0) this._byEntity.splice(idx, 1);
|
|
getPassiveSkillHost().unbind(entityId);
|
|
}
|
|
|
|
/** 退局清空全部装载(被动/弓由 BattleRuntime.destroy 已 clear) */
|
|
clear(): void {
|
|
this._byEntity.length = 0;
|
|
}
|
|
|
|
getLoadout(entityId: EntityId): TowerLoadout | undefined {
|
|
const idx = this._findIndex(entityId);
|
|
return idx >= 0 ? this._byEntity[idx].loadout : undefined;
|
|
}
|
|
|
|
/** @deprecated 词条替换已移除 */
|
|
replaceActiveSkillForPlayer(
|
|
_session: BattleSession,
|
|
_playerId: number,
|
|
_weapon1Id: number,
|
|
_baseAtkId: number,
|
|
): void {
|
|
/* no-op:词条表已删 */
|
|
}
|
|
|
|
/** @deprecated 词条 hook 已移除 */
|
|
appendEntryAtkHooks(_session: BattleSession, _entityId: EntityId, _entry: ITableSkill): void {
|
|
/* no-op */
|
|
}
|
|
|
|
/** @deprecated */
|
|
applySkillSpecial(_session: BattleSession, _entityId: EntityId, _entry: ITableSkill): void {
|
|
/* no-op */
|
|
}
|
|
|
|
rebuildHeroAbility(entityId: EntityId, loadout: TowerLoadout): void {
|
|
if (loadout.activeSkillBaseAtkId <= 0) return;
|
|
const row = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, loadout.activeSkillBaseAtkId);
|
|
if (!row) {
|
|
console.log(`[TowerLoadout] 英雄技能缺失 Skill.id=${loadout.activeSkillBaseAtkId}`);
|
|
return;
|
|
}
|
|
const abilityId = entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId);
|
|
const def = SkillAbilityBuilder.buildHero(row, entityId);
|
|
def.id = abilityId;
|
|
if (def.costs?.[0]?.params) def.costs[0].params.abilityId = abilityId;
|
|
gg.battle.config.registerAbility(abilityId, def);
|
|
this._stampLoadoutPresentation(def, loadout, 'hero');
|
|
this.patchActiveSkillCooldown(null, entityId, abilityId, loadout.activeSkillBaseAtkId);
|
|
}
|
|
|
|
rebuildWeaponAbility(entityId: EntityId, loadout: TowerLoadout): void {
|
|
if (!loadout.weaponSkillActive || loadout.weaponSkillId <= 0) return;
|
|
const row = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, loadout.weaponSkillId);
|
|
if (!row) {
|
|
console.log(`[TowerLoadout] 武器主动技能缺失 Skill.id=${loadout.weaponSkillId}`);
|
|
return;
|
|
}
|
|
const abilityId = entityWeaponAbilityId(loadout.weaponSkillId, entityId);
|
|
const def = SkillAbilityBuilder.buildWeapon(row, entityId);
|
|
def.id = abilityId;
|
|
if (def.costs?.[0]?.params) def.costs[0].params.abilityId = abilityId;
|
|
gg.battle.config.registerAbility(abilityId, def);
|
|
this._stampLoadoutPresentation(def, loadout, 'weapon');
|
|
this.patchActiveSkillCooldown(null, entityId, abilityId, loadout.weaponSkillId);
|
|
}
|
|
|
|
/**
|
|
* 被动 result 10:覆盖宿主普攻 atk_type / atk_para 并重建 Ability。
|
|
* 不改共享 Skill 表,按实体隔离,避免同技能其他塔串台。
|
|
* @param session 战斗会话
|
|
* @param entityId 触发宿主
|
|
* @param atkType 新 atk_type
|
|
* @param atkPara 新 atk_para
|
|
* @param sourceSkillId 被动 Skill.id
|
|
*/
|
|
applyNormalAtkOverride(session: BattleSession, entityId: EntityId, atkType: string, atkPara: string, sourceSkillId: number): void {
|
|
const loadout = this.getLoadout(entityId);
|
|
if (!loadout) {
|
|
console.log(`[TowerLoadout] result 10 无装载 skill=${sourceSkillId} entity=${entityId}`);
|
|
return;
|
|
}
|
|
if (!(loadout.normalBaseAtkId > 0)) {
|
|
console.log(`[TowerLoadout] result 10 无普攻 skill=${sourceSkillId} entity=${entityId}`);
|
|
return;
|
|
}
|
|
loadout.overAtkType = atkType;
|
|
loadout.overAtkPara = atkPara;
|
|
const nextId = this._registerNormalAbility(entityId, loadout);
|
|
if (!nextId) return;
|
|
const set = session.world.getComponent(entityId, AbilitySetComp);
|
|
if (!set) {
|
|
console.log(`[TowerLoadout] result 10 无 AbilitySet skill=${sourceSkillId} entity=${entityId}`);
|
|
return;
|
|
}
|
|
this._replaceNormalAbilityId(set, loadout.normalBaseAtkId, nextId);
|
|
session.abilities.bindEntity(entityId, { preserveRuntime: true });
|
|
}
|
|
|
|
/**
|
|
* 按装载(含覆盖)注册普攻 Ability。
|
|
* @param entityId 塔实体
|
|
* @param loadout 三槽装载
|
|
* @returns Ability id;普攻缺失时为空
|
|
*/
|
|
private _registerNormalAbility(entityId: EntityId, loadout: TowerLoadout): string {
|
|
const skillId = loadout.normalBaseAtkId;
|
|
const row = this._normalSkillRow(loadout);
|
|
if (!row) {
|
|
console.log(`[TowerLoadout] 普攻缺失 Skill.id=${skillId}`);
|
|
return '';
|
|
}
|
|
const abilityId = loadout.overAtkType
|
|
? entityNormalAbilityId(skillId, entityId)
|
|
: normalAbilityId(skillId);
|
|
const def = SkillAbilityBuilder.buildNormal(row, abilityId);
|
|
this._stampLoadoutPresentation(def, loadout, 'normal');
|
|
gg.battle.config.registerAbility(abilityId, def);
|
|
return abilityId;
|
|
}
|
|
|
|
/**
|
|
* 普攻 Skill 行;有覆盖时拷贝并改 atk_type / atk_para。
|
|
* @param loadout 三槽装载
|
|
*/
|
|
private _normalSkillRow(loadout: TowerLoadout): ITableSkill | null {
|
|
const row = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, loadout.normalBaseAtkId);
|
|
if (!row) return null;
|
|
if (!loadout.overAtkType) return row;
|
|
return Object.assign({}, row, {
|
|
atk_type: loadout.overAtkType,
|
|
atk_para: loadout.overAtkPara,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 把实体普攻 Ability id 换成覆盖后的实体 id。
|
|
* @param set 技能集
|
|
* @param skillId 普攻 Skill.id
|
|
* @param nextId 新 Ability id
|
|
*/
|
|
private _replaceNormalAbilityId(set: AbilitySetComp, skillId: number, nextId: string): void {
|
|
const shared = normalAbilityId(skillId);
|
|
const prefix = `ability_norm_${skillId}_e`;
|
|
let found = false;
|
|
for (let i = 0; i < set.abilityIds.length; i++) {
|
|
const id = set.abilityIds[i];
|
|
if (id === shared || id === nextId || id.indexOf(prefix) === 0) {
|
|
set.abilityIds[i] = nextId;
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found) {
|
|
console.log(`[TowerLoadout] result 10 未找到普攻 Ability skill=${skillId}`);
|
|
set.abilityIds.unshift(nextId);
|
|
}
|
|
const prevCd = set.cooldowns[shared];
|
|
if (prevCd != null && set.cooldowns[nextId] == null) {
|
|
set.cooldowns[nextId] = prevCd;
|
|
}
|
|
}
|
|
|
|
/** 写入表现对接字段:heroId / weaponId / composeLevel / target 快照由 cue 时补 */
|
|
private _stampLoadoutPresentation(
|
|
def: import('db://assets/mx/module/battle').AbilityDef,
|
|
loadout: TowerLoadout,
|
|
skillKind: 'normal' | 'hero' | 'weapon',
|
|
): void {
|
|
for (const ev of def.timeline) {
|
|
for (const fx of ev.effects) {
|
|
if (fx.type !== 'PlayPresentation' || !fx.params) continue;
|
|
fx.params.heroId = loadout.weapon1Id;
|
|
fx.params.weaponId = loadout.weaponId;
|
|
fx.params.composeLevel = loadout.level;
|
|
fx.params.skillKind = skillKind;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 按 Skill.cd 写 OnTick / Cooldown(不再叠局外角色冷却) */
|
|
patchActiveSkillCooldown(
|
|
session: BattleSession | null,
|
|
entityId: EntityId,
|
|
abilityId: string,
|
|
skillId: number,
|
|
): void {
|
|
if (skillId <= 0 || !abilityId) return;
|
|
const skillRow = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId);
|
|
const baseCd = skillRow?.cd ?? 0;
|
|
const def = gg.battle.config.getAbility(abilityId);
|
|
if (!def) return;
|
|
if (baseCd <= 0) {
|
|
TableAnomaly.warn('Skill', skillId, 'cd', '主动技能 cd≤0,不编造连放间隔', baseCd);
|
|
def.costs = [];
|
|
return;
|
|
}
|
|
let cd = baseCd;
|
|
if (session) cd = session.attributes.getFinalCooldown(entityId, cd);
|
|
if (def.triggers[0]) def.triggers[0].interval = cd;
|
|
def.costs = [{ type: 'Cooldown', params: { cd, abilityId } }];
|
|
}
|
|
|
|
/** @deprecated 词条改参已移除 */
|
|
rebuildUltAbility(entityId: EntityId, loadout: TowerLoadout): void {
|
|
this.rebuildHeroAbility(entityId, loadout);
|
|
}
|
|
|
|
/** @deprecated */
|
|
buildEntityUltAbility(session: BattleSession, entityId: EntityId, _entries: readonly ITableSkill[]): void {
|
|
const loadout = this.getLoadout(entityId);
|
|
if (!loadout) return;
|
|
this.rebuildHeroAbility(entityId, loadout);
|
|
this.rebuildWeaponAbility(entityId, loadout);
|
|
session.abilities.bindEntity(entityId, { preserveRuntime: true });
|
|
}
|
|
|
|
/** @deprecated */
|
|
reapplySkillChanges(session: BattleSession, entityId: EntityId, entries: readonly ITableSkill[]): void {
|
|
this.buildEntityUltAbility(session, entityId, entries);
|
|
}
|
|
|
|
applyAbilities(session: BattleSession, entityId: EntityId, loadout: TowerLoadout): void {
|
|
const world = session.world;
|
|
if (!world.isAlive(entityId)) return;
|
|
|
|
if (loadout.activeSkillBaseAtkId > 0) this.rebuildHeroAbility(entityId, loadout);
|
|
if (loadout.weaponSkillActive) this.rebuildWeaponAbility(entityId, loadout);
|
|
|
|
const abilityIds: string[] = [];
|
|
if (loadout.normalBaseAtkId > 0) {
|
|
const nid = this._registerNormalAbility(entityId, loadout);
|
|
if (nid) abilityIds.push(nid);
|
|
}
|
|
if (loadout.activeSkillBaseAtkId > 0) {
|
|
abilityIds.push(entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId));
|
|
}
|
|
if (loadout.weaponSkillActive && loadout.weaponSkillId > 0) {
|
|
abilityIds.push(entityWeaponAbilityId(loadout.weaponSkillId, entityId));
|
|
}
|
|
|
|
let set = world.getComponent(entityId, AbilitySetComp);
|
|
const isRefresh = !!set;
|
|
const prevCd = set ? { ...set.cooldowns } : {};
|
|
if (!set) {
|
|
set = new AbilitySetComp(abilityIds, {});
|
|
world.addComponent(entityId, set);
|
|
} else {
|
|
set.abilityIds = abilityIds;
|
|
const nextCd: Record<string, number> = {};
|
|
for (const id of abilityIds) {
|
|
if (prevCd[id] != null) nextCd[id] = prevCd[id];
|
|
}
|
|
set.cooldowns = nextCd;
|
|
}
|
|
|
|
if (loadout.activeSkillBaseAtkId > 0) {
|
|
this.patchActiveSkillCooldown(
|
|
session, entityId,
|
|
entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId),
|
|
loadout.activeSkillBaseAtkId,
|
|
);
|
|
}
|
|
if (loadout.weaponSkillActive && loadout.weaponSkillId > 0) {
|
|
this.patchActiveSkillCooldown(
|
|
session, entityId,
|
|
entityWeaponAbilityId(loadout.weaponSkillId, entityId),
|
|
loadout.weaponSkillId,
|
|
);
|
|
}
|
|
session.abilities.bindEntity(entityId, { preserveRuntime: isRefresh });
|
|
|
|
// 被动:武器 type=2
|
|
const passives = getPassiveSkillHost();
|
|
passives.bindAtkOverride(this);
|
|
passives.bindFromLoadout(session, entityId, loadout);
|
|
}
|
|
|
|
setupCombat(session: BattleSession, entityId: EntityId, weapon1Id: number, level: number, player: Player): void {
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, weapon1Id);
|
|
if (!hero) return;
|
|
|
|
const world = session.world;
|
|
const isRefresh = !!world.getComponent(entityId, AbilitySetComp);
|
|
const battleWeaponId = resolveHeroBattleWeaponId(weapon1Id, player);
|
|
const setup = deriveTowerSetupStats(hero, level, battleWeaponId);
|
|
const lv = setup.effectiveLevel;
|
|
|
|
if (world.getComponent(entityId, CombatComp)) {
|
|
world.removeComponent(entityId, CombatComp);
|
|
}
|
|
world.addComponent(entityId, new CombatComp(
|
|
setup.attack,
|
|
setup.attackSpeed,
|
|
setup.range,
|
|
setup.critRate,
|
|
setup.critDamage,
|
|
setup.maxAttackSpeed,
|
|
));
|
|
|
|
const meta = world.getComponent(entityId, UnitMetaComp);
|
|
if (meta) meta.level = lv;
|
|
|
|
session.attributes.syncCombatToAttributes(entityId);
|
|
SkillAtkBuffExecutor.clearRangeModifiers(session, entityId);
|
|
|
|
this.initEntity(entityId, weapon1Id, lv, player);
|
|
const loadout = this.getLoadout(entityId)!;
|
|
if (isElementHero(hero)) {
|
|
const gem = GemBattleEffect.resolve(Math.floor(Number(hero.herotype)), player);
|
|
GemBattleEffect.apply(session, entityId, gem);
|
|
} else {
|
|
GemBattleEffect.clear(session, entityId);
|
|
}
|
|
this.applyAbilities(session, entityId, loadout);
|
|
|
|
if (!isRefresh && loadout.weaponSkillId > 0 && !loadout.weaponSkillActive) {
|
|
const sk = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, loadout.weaponSkillId);
|
|
if (sk) {
|
|
SkillAtkBuffExecutor.applyLoadoutBuffs(session, entityId, sk.atk_buff1 ?? '', sk.atk_buff2 ?? '');
|
|
}
|
|
}
|
|
this._applyWeaponDistDmg(session, entityId, loadout);
|
|
|
|
if (isAtkDebugWeapon(weapon1Id)) {
|
|
logKnifeBaseAttack({
|
|
entityId,
|
|
weapon1Id,
|
|
composeLevel: level,
|
|
effectiveLevel: setup.effectiveLevel,
|
|
heroAtk: hero.atk,
|
|
weaponAtk: Math.max(0, setup.attack - hero.atk),
|
|
combatAttack: setup.attack,
|
|
});
|
|
dumpKnifeFinalAttack(session, entityId, '部署', weapon1Id);
|
|
}
|
|
|
|
if (!isRefresh) {
|
|
const set = world.getComponent(entityId, AbilitySetComp);
|
|
if (set) {
|
|
if (loadout.activeSkillBaseAtkId > 0) {
|
|
set.cooldowns[entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId)] = 0;
|
|
}
|
|
if (loadout.weaponSkillActive && loadout.weaponSkillId > 0) {
|
|
set.cooldowns[entityWeaponAbilityId(loadout.weaponSkillId, entityId)] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resetAtkHooks(session: BattleSession, entityId: EntityId): void {
|
|
const world = session.world;
|
|
let hook = world.getComponent(entityId, AtkHookComp);
|
|
if (!hook) {
|
|
hook = new AtkHookComp();
|
|
world.addComponent(entityId, hook);
|
|
} else {
|
|
hook.before = [];
|
|
hook.after = [];
|
|
hook.ultBefore = [];
|
|
hook.ultAfter = [];
|
|
hook.ultCanCrit = false;
|
|
hook.specials = [];
|
|
hook.distPerPx = 0;
|
|
hook.distAddWan = 0;
|
|
hook.distMaxWan = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 武器被动 Skill.skill 类型8:写入 AtkHookComp,普攻命中按距离加伤。
|
|
* @param session 战斗会话
|
|
* @param entityId 塔实体
|
|
* @param loadout 三槽装载
|
|
*/
|
|
private _applyWeaponDistDmg(session: BattleSession, entityId: EntityId, loadout: TowerLoadout): void {
|
|
const world = session.world;
|
|
let hook = world.getComponent(entityId, AtkHookComp);
|
|
if (!hook) {
|
|
hook = new AtkHookComp();
|
|
world.addComponent(entityId, hook);
|
|
}
|
|
hook.distPerPx = 0;
|
|
hook.distAddWan = 0;
|
|
hook.distMaxWan = 0;
|
|
if (!(loadout.weaponSkillId > 0) || loadout.weaponSkillActive) return;
|
|
const sk = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, loadout.weaponSkillId);
|
|
if (!sk) return;
|
|
const parts = String(sk.skill ?? '').trim().split(',').map(Number);
|
|
if ((parts[0] ?? 0) !== 8) return;
|
|
const perPx = parts[1];
|
|
const addWan = parts[2];
|
|
const maxWan = parts[3];
|
|
if (!(perPx > 0) || !(addWan > 0) || !(maxWan > 0)) {
|
|
console.log('[TowerLoadout] skill 类型8参数无效', sk.id, sk.skill);
|
|
return;
|
|
}
|
|
hook.distPerPx = perPx;
|
|
hook.distAddWan = addWan;
|
|
hook.distMaxWan = maxWan;
|
|
}
|
|
}
|
|
|
|
let _instance: TowerLoadoutService | null = null;
|
|
|
|
export function getTowerLoadoutService(): TowerLoadoutService {
|
|
if (!_instance) _instance = new TowerLoadoutService();
|
|
return _instance;
|
|
}
|
|
|
|
export function resetTowerLoadoutServiceForTests(): void {
|
|
_instance = null;
|
|
}
|
|
|