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/BattleBuffDigitStatHost.ts

287 lines
12 KiB

import type { BattleSession } from 'db://assets/mx/module/battle';
import { AttrId, AttributeComp, CombatComp, TeamComp } from 'db://assets/mx/module/battle/ecs/Components';
import type { EntityId } from 'db://assets/mx/module/ecs';
import { GEvent } from 'db://assets/mx/module/event/GEvent';
import { TableNames } from '../../game/ConfigTableData';
import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
import { BattleBuffFunctionType } from '../../manager/battleBuff/BattleBuffTypes';
import { BattleBuffTargetParse, parseFunctionParNums } from '../../manager/battleBuff/BattleBuffParamParse';
import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea';
import { isElementHero } from '../config/BattleHeroTable';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
import { getTowerLoadoutService } from './TowerLoadoutService';
import type { TowerCombatStats } from './TowerQueryTypes';
/** modifier source 前缀 */
const DIGIT_STAT_SOURCE = 'battle_buff_digit_';
/** 绑定所需最小战斗句柄(避免 import BattleRuntime 造成循环) */
interface DigitStatBattleRef {
/** 本机 playerId */
localPlayerId: number;
/** 取玩家 */
getPlayer(playerId: number): { deployedUnits: readonly EntityId[] } | null | undefined;
/** 会话 */
session: BattleSession;
}
/**
* 局内 functiontype 101~104:按 target/targetpar 给数字塔加伤/暴击/爆伤/攻速。
*/
export class BattleBuffDigitStatHost {
private _battle: DigitStatBattleRef | null = null;
/** 绑定本局并刷新已部署单位 */
bind(battle: DigitStatBattleRef): void {
this._battle = battle;
this.reapplyPlayer(battle.localPlayerId);
this.reapplyPlayer(AI_PLAYER_ID);
}
/** 解除绑定 */
unbind(): void {
this._battle = null;
}
/**
* 某玩家携带变更后重刷其场上单位。
* @param playerId 玩家
*/
reapplyPlayer(playerId: number): void {
const battle = this._battle;
if (!battle) {
console.warn('[BattleBuffDigitStat] reapply 未 bind', playerId);
return;
}
const pid = Math.max(0, Math.floor(Number(playerId) || 0));
const session = battle.session;
const seen: EntityId[] = [];
const player = battle.getPlayer(pid);
const deployed = player?.deployedUnits ?? [];
for (let i = 0; i < deployed.length; i++) {
const eid = deployed[i];
if (this._hasSeen(seen, eid)) continue;
seen.push(eid);
this.refreshEntity(session, eid, pid);
}
// deployedUnits 与 ECS 偶发不同步时,按 TeamComp+CombatComp 补刷
session.world.view2(CombatComp, TeamComp).forEach((entityId, _combat, team) => {
if (team.ownerId !== pid) return;
if (this._hasSeen(seen, entityId)) return;
seen.push(entityId);
this.refreshEntity(session, entityId, pid);
});
if (this._hasDigitPassives(pid) && seen.length <= 0) {
console.warn('[BattleBuffDigitStat] 有 101~104 但场上无塔可刷', pid);
}
GEvent.Ins.emit(GEvent.UpdateBestPlace);
}
/**
* 未部署/配表预览:把 101~104 叠到快照数值上(与 ECS 通道一致)。
* @param stats 基础战斗数值
* @param weapon1Id Hero.id
* @param level 合成等级
* @param playerId 携带方
*/
previewOntoStats(stats: TowerCombatStats, weapon1Id: number, level: number, playerId: number): TowerCombatStats {
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, weapon1Id);
if (!hero) return stats;
const heroType = this._resolveElementType(weapon1Id, hero);
const lv = this._resolveMatchLevel(hero, level);
const pid = Math.max(0, Math.floor(Number(playerId) || 0));
let attackMul = 1;
let aspdAdd = 0;
let critRateAdd = 0;
let critDmgAdd = 0;
const passives = BattleBuffCarryStore.getPassives(pid);
for (let i = 0; i < passives.length; i++) {
const row = passives[i].row as ITableBattlebuff | undefined;
if (!row) continue;
const ft = Math.floor(Number(row.functiontype) || 0);
if (!this._isDigitFt(ft)) continue;
if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, lv)) continue;
const nums = parseFunctionParNums(row.functionpar);
const wan = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (wan === 0) continue;
if (ft === BattleBuffFunctionType.DigitDamage) attackMul *= 1 + wan / 10000;
else if (ft === BattleBuffFunctionType.DigitCritRate) critRateAdd += wan / 10000;
else if (ft === BattleBuffFunctionType.DigitCritDamage) critDmgAdd += wan / 10000;
else if (ft === BattleBuffFunctionType.DigitAttackSpeed) aspdAdd += wan;
}
if (attackMul === 1 && aspdAdd === 0 && critRateAdd === 0 && critDmgAdd === 0) return stats;
const attackSpeed = stats.attackSpeed * (1 + aspdAdd / 10000);
return {
...stats,
attack: Math.floor(stats.attack * attackMul),
attackSpeed,
attackInterval: attackSpeed > 0 ? 1 / attackSpeed : 0,
critRate: Math.min(1, stats.critRate + critRateAdd),
critDamage: stats.critDamage + critDmgAdd,
};
}
/**
* 单塔刷新 101~104 修正。
* @param session 会话
* @param entityId 实体
* @param ownerPlayerId 归属
*/
refreshEntity(session: BattleSession, entityId: EntityId, ownerPlayerId: number): void {
this._clearDigitMods(session, entityId);
const pid = Math.max(0, Math.floor(Number(ownerPlayerId) || 0));
if (!this._hasDigitPassives(pid)) return;
const loadout = getTowerLoadoutService().getLoadout(entityId);
if (!loadout) return;
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, loadout.weapon1Id);
if (!hero) {
console.warn('[BattleBuffDigitStat] Hero 缺失', loadout.weapon1Id, entityId);
return;
}
const heroType = this._resolveElementType(loadout.weapon1Id, hero);
const level = this._resolveMatchLevel(hero, loadout.level);
const world = session.world;
if (!world.isAlive(entityId)) return;
if (!world.getComponent(entityId, CombatComp)) {
console.warn('[BattleBuffDigitStat] 无 CombatComp', entityId, loadout.weapon1Id);
return;
}
this._applyPassivesToEntity(session, entityId, pid, heroType, level);
}
/**
* 匹配用等级:五行以 Hero.level 为准(weapon1Id 已定级)。
* @param hero Hero 行
* @param loadoutLevel 装载等级
*/
private _resolveMatchLevel(hero: ITableHero, loadoutLevel: number): number {
if (isElementHero(hero)) {
const hlv = Math.floor(Number(hero.level) || 0);
if (hlv > 0) return hlv;
}
return Math.max(1, Math.floor(Number(loadoutLevel) || 0));
}
/**
* @param session 会话
* @param entityId 实体
* @param playerId 携带方
* @param heroType 五行
* @param level 等级
*/
private _applyPassivesToEntity(session: BattleSession, entityId: EntityId, playerId: number, heroType: number, level: number): void {
const passives = BattleBuffCarryStore.getPassives(playerId);
for (let i = 0; i < passives.length; i++) {
const row = passives[i].row as ITableBattlebuff | undefined;
if (!row) continue;
const ft = Math.floor(Number(row.functiontype) || 0);
if (!this._isDigitFt(ft)) continue;
if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, level)) continue;
const nums = parseFunctionParNums(row.functionpar);
const wan = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (wan === 0) {
console.warn('[BattleBuffDigitStat] functionpar 无效', row.id, row.functionpar);
continue;
}
if (wan > 0 && wan < 100) {
console.warn('[BattleBuffDigitStat] functionpar 过小(疑非万分比)', row.id, wan, row.desc);
}
this._applyOne(session, entityId, ft, wan, `${DIGIT_STAT_SOURCE}${row.id}_${i}`);
}
}
private _hasSeen(seen: readonly EntityId[], eid: EntityId): boolean {
for (let i = 0; i < seen.length; i++) {
if (seen[i] === eid) return true;
}
return false;
}
private _hasDigitPassives(playerId: number): boolean {
const passives = BattleBuffCarryStore.getPassives(playerId);
for (let i = 0; i < passives.length; i++) {
const row = passives[i].row as ITableBattlebuff | undefined;
if (!row) continue;
if (this._isDigitFt(Math.floor(Number(row.functiontype) || 0))) return true;
}
return false;
}
private _isDigitFt(ft: number): boolean {
return ft === BattleBuffFunctionType.DigitDamage
|| ft === BattleBuffFunctionType.DigitCritRate
|| ft === BattleBuffFunctionType.DigitCritDamage
|| ft === BattleBuffFunctionType.DigitAttackSpeed;
}
/**
* 五行 type:优先 Text.type,否则 Hero.herotype。
* @param weapon1Id Hero.id
* @param hero Hero 行
*/
private _resolveElementType(weapon1Id: number, hero: ITableHero): number {
const provider = WeaponComposeConfigProvider.instance;
provider.load();
const w2 = provider.getWeapon2IdForWeapon1(weapon1Id);
if (w2 != null) {
const t = Math.floor(Number(provider.getWeapon2Type(w2)) || 0);
if (t >= 1 && t <= 5) return t;
}
return Math.floor(Number(hero.herotype) || 0);
}
private _clearDigitMods(session: BattleSession, entityId: EntityId): void {
const world = session.world;
if (!world.isAlive(entityId)) return;
const attr = world.getComponent(entityId, AttributeComp);
if (!attr) return;
const next = [];
for (let i = 0; i < attr.modifiers.length; i++) {
const src = String(attr.modifiers[i].source ?? '');
if (src.indexOf(DIGIT_STAT_SOURCE) === 0) continue;
next.push(attr.modifiers[i]);
}
attr.modifiers = next;
}
private _applyOne(session: BattleSession, entityId: EntityId, ft: number, wan: number, source: string): void {
const world = session.world;
if (!world.isAlive(entityId)) return;
if (!world.getComponent(entityId, CombatComp)) return;
if (!world.getComponent(entityId, AttributeComp)) {
world.addComponent(entityId, new AttributeComp());
}
const attrSvc = session.attributes;
if (ft === BattleBuffFunctionType.DigitDamage) {
attrSvc.addModifier(entityId, {
attr: AttrId.Attack, op: 'Mul', value: 1 + wan / 10000, source,
});
return;
}
if (ft === BattleBuffFunctionType.DigitCritRate) {
attrSvc.addModifier(entityId, {
attr: AttrId.CritRate, op: 'Add', value: wan / 10000, source,
});
return;
}
if (ft === BattleBuffFunctionType.DigitCritDamage) {
attrSvc.addModifier(entityId, {
attr: AttrId.CritDamage, op: 'Add', value: wan / 10000, source,
});
return;
}
if (ft === BattleBuffFunctionType.DigitAttackSpeed) {
attrSvc.addModifier(entityId, {
attr: 'aspd_add', op: 'Add', value: wan, source,
});
}
}
}
let _host: BattleBuffDigitStatHost | null = null;
/** 局内数字属性 Host */
export function getBattleBuffDigitStatHost(): BattleBuffDigitStatHost {
if (!_host) _host = new BattleBuffDigitStatHost();
return _host;
}