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.
 
 
 
 
AdLike2048/assets/script/battle/combat/TowerExpHost.ts

462 lines
16 KiB

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<ITableHero>(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<ITableHero>(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<ITableMonster>(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();
}