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.
287 lines
12 KiB
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;
|
|
}
|
|
|