import type { BattleSession } from 'db://assets/mx/module/battle'; import { TeamComp, UnitMetaComp } from 'db://assets/mx/module/battle/ecs/Components'; import type { GlyphHeroBind } from 'db://assets/mx/module/merge'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { ConstNumType } from '../../game/GameDataEnums'; import { TableNames } from '../../game/ConfigTableData'; import type { Player } from '../../game/Player'; import { parseLegacyMonsterId, parseMonsterUnitDefId } from '../config/MonsterHpCalculator'; import { findCompositionForEntity, findGridIndexForEntity } from '../runtime/DeployEntityQuery'; import type { ProjectBattleHandle } from '../runtime/BattleRuntime'; import type { MapManager } from '../map/legacy/MapManager'; import { setupTowerCombat } from './TowerCombatSetup'; import { getTowerLoadoutService } from './TowerLoadoutService'; import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider'; import { getBattleGameGlobalAttackSpeedHost } from './BattleGameGlobalAttackSpeedHost'; import { getBattleGameGlobalAttackRangeHost } from './BattleGameGlobalAttackRangeHost'; /** 某怪物上造成过伤害的塔 */ interface VictimHits { /** 受伤怪物 */ victim: EntityId; /** 打过该怪的塔(去重) */ casters: EntityId[]; } /** 塔当前累计经验 */ interface TowerExp { /** 塔实体 */ entityId: EntityId; /** 当前经验(可结余) */ exp: number; } /** * 已部署攻击单位的击杀/助攻经验与升级。 * 上限走 Hero.level_limit;所需经验走 Hero.exp。 */ export class TowerExpHost { private static _inst: TowerExpHost | null = null; private readonly _hits: VictimHits[] = []; private readonly _exps: TowerExp[] = []; /** 单例 */ static get(): TowerExpHost { if (!TowerExpHost._inst) TowerExpHost._inst = new TowerExpHost(); return TowerExpHost._inst; } /** 开局/结束清空 */ clear(): void { this._hits.length = 0; this._exps.length = 0; } /** * 记录塔对怪物造成的伤害(任意伤害类型)。 * @param caster 伤害来源 * @param target 受伤目标 * @param amount 实际伤害 */ onDamage(caster: EntityId, target: EntityId, amount: number): void { if (!(amount > 0) || !(caster > 0) || !(target > 0)) return; if (!getTowerLoadoutService().getLoadout(caster)) return; const hits = this._hitsOf(target); if (this._indexOfId(hits.casters, caster) >= 0) return; hits.casters.push(caster); } /** * 怪物死亡:击杀塔加 Monster.exp,其余打过该怪的塔加助攻经验。 * @param battle 对局句柄 * @param killer 致命一击来源(可为 0) * @param victim 死亡怪物 */ onKilled(battle: ProjectBattleHandle, killer: EntityId, victim: EntityId): void { if (!(victim > 0)) return; const killExp = this._monsterKillExp(battle, victim); const assistExp = this._assistExp(); const hits = this._takeHits(victim); if (killer > 0 && getTowerLoadoutService().getLoadout(killer) && killExp > 0) { this._addExp(battle, killer, killExp); } if (!(assistExp > 0)) { return; } for (let i = 0; i < hits.length; i++) { const caster = hits[i]; if (caster === killer) continue; if (!getTowerLoadoutService().getLoadout(caster)) continue; this._addExp(battle, caster, assistExp); } } /** * 实体回收:清伤害归属与经验。 * @param entityId 塔或怪物 */ forget(entityId: EntityId): void { if (!(entityId > 0)) return; this._removeHitsVictim(entityId); this._stripCaster(entityId); const expIdx = this._expIndex(entityId); if (expIdx >= 0) this._exps.splice(expIdx, 1); } /** * 给塔加经验并尝试连升。 * @param battle 对局句柄 * @param entityId 塔 * @param amount 本次经验 */ private _addExp(battle: ProjectBattleHandle, entityId: EntityId, amount: number): void { const loadout = getTowerLoadoutService().getLoadout(entityId); if (!loadout) return; const hero = gg.data.table.getTableData(TableNames.Hero, loadout.weapon1Id); if (!hero) { console.log('[TowerExp] Hero 缺失', loadout.weapon1Id); return; } const max = WeaponComposeConfigProvider.instance.getMaxWeaponLevel(loadout.weapon1Id); if (loadout.level >= max) return; const need = this._needExp(hero, loadout.level); if (!(need > 0)) return; const rec = this._expOf(entityId); rec.exp += amount; let level = loadout.level; while (level < max) { const step = this._needExp(hero, level); if (!(step > 0) || rec.exp < step) break; // MapUpgraded → DeploySync 会按字绑定 restoreExp;须先写回再升级,升级后按升级前快照扣经验 const expBefore = rec.exp; this._syncBindFromBattle(battle, entityId); if (!this._tryLevelUp(battle, entityId)) { rec.exp = expBefore; break; } rec.exp = expBefore - step; const next = getTowerLoadoutService().getLoadout(entityId); level = next?.level ?? level + 1; } this._syncBindFromBattle(battle, entityId); } /** * 写棋盘等级并重建战斗数值。 * @param battle 对局句柄 * @param entityId 塔 */ private _tryLevelUp(battle: ProjectBattleHandle, entityId: EntityId): boolean { const session = battle.session; if (!session.world.isAlive(entityId)) return false; const loadout = getTowerLoadoutService().getLoadout(entityId); if (!loadout) return false; const team = session.world.getComponent(entityId, TeamComp); const player = battle.getPlayer(team?.ownerId ?? 0); const map = battle.Map; if (!player || !map) { console.log('[TowerExp] 升级缺少玩家或地图', entityId); return false; } const gridIndex = findGridIndexForEntity(entityId, player, session, map); if (gridIndex == null) { console.log('[TowerExp] 升级找不到格子', entityId); return false; } const merge = player.bagSession?.merge; if (!merge) { console.log('[TowerExp] 升级缺少 MergeSession', entityId, player.playerId); return false; } const reason = merge.applyBattleBuffUnitLevelUp(gridIndex, 1); if (reason != null) { console.log('[TowerExp] 升级被拒绝', entityId, reason); return false; } const refreshed = findCompositionForEntity(entityId, player, session, map); const newLevel = refreshed?.level ?? loadout.level + 1; setupTowerCombat(session, entityId, loadout.weapon1Id, newLevel, player); return true; } /** * 补足当前等级升下一级剩余经验(已满则加 0 并走统一升级扣经验)。 * @param battle 对局句柄 * @param entityId 塔实体 */ grantRemainingExpForNextLevel(battle: ProjectBattleHandle, entityId: EntityId): boolean { const loadout = getTowerLoadoutService().getLoadout(entityId); if (!loadout) return false; const hero = gg.data.table.getTableData(TableNames.Hero, loadout.weapon1Id); if (!hero) { console.log('[TowerExp] Hero 缺失', loadout.weapon1Id); return false; } const max = WeaponComposeConfigProvider.instance.getMaxWeaponLevel(loadout.weapon1Id); if (loadout.level >= max) return false; const need = this._needExp(hero, loadout.level); if (!(need > 0)) return false; const cur = this.getExp(entityId); // 已满也走 _addExp(加 0),保证扣经验与 MapUpgraded 对账一致 this._addExp(battle, entityId, cur < need ? need - cur : 0); return true; } /** * 查询塔当前累计经验(未记录则 0)。 * @param entityId 塔实体 */ getExp(entityId: EntityId): number { const idx = this._expIndex(entityId); return idx >= 0 ? this._exps[idx].exp : 0; } /** * 清空塔累计经验(授仙箓降级组合字武将等)。 * @param entityId 塔实体 */ clearExp(entityId: EntityId): void { if (!(entityId > 0)) return; const expIdx = this._expIndex(entityId); if (expIdx >= 0) this._exps.splice(expIdx, 1); } /** * 恢复拆开前的经验(重组后新实体)。 * @param entityId 塔实体 * @param exp 绑定快照经验 */ restoreExp(entityId: EntityId, exp: number): void { if (!(entityId > 0) || !(exp > 0)) return; this._expOf(entityId).exp = exp; } /** * 把当前英雄状态写到组合内每个字上。 * @param session 战斗会话 * @param player 玩家 * @param map 地图 * @param entityId 塔实体 */ syncBindToGlyphs(session: BattleSession, player: Player, map: MapManager, entityId: EntityId): void { const compose = findCompositionForEntity(entityId, player, session, map); if (!compose) return; const bind = this.captureBind(session, entityId); if (!bind) return; player.bagSession?.merge.writeCompositionHeroBind(compose, bind); } /** * 按字上的绑定恢复经验与 Buff。 * @param session 战斗会话 * @param entityId 新实体 * @param bind 绑定快照 */ restoreBindToEntity(session: BattleSession, entityId: EntityId, bind: GlyphHeroBind): void { this.restoreExp(entityId, bind.exp); getBattleGameGlobalAttackSpeedHost().restore(session, entityId, bind.aspd); getBattleGameGlobalAttackRangeHost().restore(session, entityId, bind.range); } /** * 从已部署实体采集绑定快照。 * @param session 战斗会话 * @param entityId 塔实体 */ captureBind(session: BattleSession, entityId: EntityId): GlyphHeroBind | null { const loadout = getTowerLoadoutService().getLoadout(entityId); if (!loadout) return null; if (!session.world.isAlive(entityId)) return null; return { weapon1Id: loadout.weapon1Id, level: loadout.level, exp: this.getExp(entityId), aspd: getBattleGameGlobalAttackSpeedHost().snapshot(entityId), range: getBattleGameGlobalAttackRangeHost().snapshot(entityId), }; } /** * 从对局句柄同步绑定到字。 * @param battle 对局 * @param entityId 塔实体 */ private _syncBindFromBattle(battle: ProjectBattleHandle, entityId: EntityId): void { const session = battle.session; if (!session.world.isAlive(entityId)) return; const team = session.world.getComponent(entityId, TeamComp); const player = battle.getPlayer(team?.ownerId ?? 0); const map = battle.Map; if (!player || !map) return; this.syncBindToGlyphs(session, player, map, entityId); } /** * 当前等级升下一级所需经验。 * @param hero Hero 行 * @param level 当前等级 */ private _needExp(hero: ITableHero, level: number): number { return needTowerExpForLevel(hero, level); } /** * 怪物击杀经验。 * @param battle 对局句柄 * @param victim 死亡怪物 */ private _monsterKillExp(battle: ProjectBattleHandle, victim: EntityId): number { const meta = battle.session.world.getComponent(victim, UnitMetaComp); if (!meta?.unitDefId) { console.log('[TowerExp] 击杀目标无 unitDefId', victim); return 0; } const parsed = parseMonsterUnitDefId(meta.unitDefId); const monsterId = parsed?.monsterId ?? parseLegacyMonsterId(meta.unitDefId); if (!(monsterId > 0)) { console.log('[TowerExp] 无法解析怪物 id', meta.unitDefId); return 0; } const row = gg.data.table.getTableData(TableNames.Monster, monsterId); if (!row) { console.log('[TowerExp] Monster 缺失', monsterId); return 0; } const exp = Number(row.exp); if (!Number.isFinite(exp) || exp < 0) { console.log('[TowerExp] Monster.exp 非法', monsterId, row.exp); return 0; } return exp; } /** 常量表助攻经验 */ private _assistExp(): number { const row = gg.data.table.getConstData(ConstNumType.AssistExp); if (!row) { console.log('[TowerExp] Const 助攻经验缺失', ConstNumType.AssistExp); return 0; } const exp = Number(row.para); if (!Number.isFinite(exp) || exp < 0) { console.log('[TowerExp] Const 助攻经验非法', row.para); return 0; } return exp; } /** * 取并移除某怪的伤害名单。 * @param victim 怪物 */ private _takeHits(victim: EntityId): EntityId[] { const idx = this._hitsIndex(victim); if (idx < 0) return []; const casters = this._hits[idx].casters; this._hits.splice(idx, 1); return casters; } /** * 取得或创建某怪的伤害名单。 * @param victim 怪物 */ private _hitsOf(victim: EntityId): VictimHits { const idx = this._hitsIndex(victim); if (idx >= 0) return this._hits[idx]; const rec: VictimHits = { victim, casters: [] }; this._hits.push(rec); return rec; } /** * 取得或创建塔经验。 * @param entityId 塔 */ private _expOf(entityId: EntityId): TowerExp { const idx = this._expIndex(entityId); if (idx >= 0) return this._exps[idx]; const rec: TowerExp = { entityId, exp: 0 }; this._exps.push(rec); return rec; } /** * 移除某怪的伤害名单。 * @param victim 怪物 */ private _removeHitsVictim(victim: EntityId): void { const idx = this._hitsIndex(victim); if (idx >= 0) this._hits.splice(idx, 1); } /** * 从所有伤害名单去掉某塔。 * @param caster 塔 */ private _stripCaster(caster: EntityId): void { for (let i = 0; i < this._hits.length; i++) { const list = this._hits[i].casters; const idx = this._indexOfId(list, caster); if (idx >= 0) list.splice(idx, 1); } } /** * 伤害名单下标。 * @param victim 怪物 */ private _hitsIndex(victim: EntityId): number { for (let i = 0; i < this._hits.length; i++) { if (this._hits[i].victim === victim) return i; } return -1; } /** * 塔经验下标。 * @param entityId 塔 */ private _expIndex(entityId: EntityId): number { for (let i = 0; i < this._exps.length; i++) { if (this._exps[i].entityId === entityId) return i; } return -1; } /** * 线性查找实体 id。 * @param list 列表 * @param id 目标 */ private _indexOfId(list: readonly EntityId[], id: EntityId): number { for (let i = 0; i < list.length; i++) { if (list[i] === id) return i; } return -1; } } /** * 当前等级升下一级所需经验(Hero.exp)。 * @param hero Hero 行 * @param level 当前等级 */ export function needTowerExpForLevel(hero: ITableHero, level: number): number { const arr = hero.exp; if (!arr || arr.length === 0) return 0; const need = Number(arr[level - 1]); if (!Number.isFinite(need) || need <= 0) { console.log('[TowerExp] Hero.exp 缺级', hero.id, level); return 0; } return need; } /** 局内经验宿主 */ export function getTowerExpHost(): TowerExpHost { return TowerExpHost.get(); }