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.
463 lines
16 KiB
463 lines
16 KiB
|
3 weeks ago
|
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();
|
||
|
|
}
|