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

593 lines
24 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,
parseBattleBuffSkillListMul,
parseFunctionParNums,
} from '../../manager/battleBuff/BattleBuffParamParse';
import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea';
import { isElementHero } from '../config/BattleHeroTable';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { getTowerLoadoutService } from './TowerLoadoutService';
import type { TowerCombatStats } from './TowerQueryTypes';
import { getPassiveSkillHost } from './PassiveSkillRuntime';
/** modifier source 前缀 */
const DIGIT_STAT_SOURCE = 'battle_buff_digit_';
/** 单塔战斗侧额外效果(105+) */
export interface DigitCombatFlags {
/** 107/113:技能伤害倍数(113 为 Hero.skill_id) */
skillMuls: { skillIds: number[]; mul: number }[];
/** 110:技能必暴(convert=暴击率转爆伤) */
forceCritSkills: { skillIds: number[]; convert: boolean }[];
/** 111:概率多倍伤 */
chanceMul: { wan: number; mul: number } | null;
/** 112:条件必暴 */
condCrit: 'poison' | 'slow' | 'always_convert' | null;
/** 108:穿透加值 */
pierceAdd: number;
/** 109:额外箭 */
extraArrow: number;
}
/** 绑定所需最小战斗句柄 */
interface DigitStatBattleRef {
/** 本机 playerId */
localPlayerId: number;
/** 取玩家 */
getPlayer(playerId: number): { deployedUnits: readonly EntityId[] } | null | undefined;
/** 会话 */
session: BattleSession;
}
/** 场上单位匹配快照 */
interface DigitUnitSnap {
/** 实体 */
entityId: EntityId;
/** 五行 */
heroType: number;
/** 匹配等级 */
level: number;
/** 同五行排名(1=最高) */
rank: number;
}
/**
* 局内 functiontype 101~113:按 target/targetpar 给数字塔属性/技能/战斗标记。
*/
export class BattleBuffDigitStatHost {
private _battle: DigitStatBattleRef | null = null;
private readonly _flags: { entityId: EntityId; flags: DigitCombatFlags }[] = [];
/** 绑定本局并刷新已部署单位 */
bind(battle: DigitStatBattleRef): void {
this._battle = battle;
this.reapplyPlayer(battle.localPlayerId);
this.reapplyPlayer(AI_PLAYER_ID);
}
/** 解除绑定 */
unbind(): void {
this._battle = null;
this._flags.length = 0;
}
/**
* 取塔战斗标记(伤害管线 / 弓箭用)。
* @param entityId 塔
*/
getFlags(entityId: EntityId): DigitCombatFlags | null {
for (let i = 0; i < this._flags.length; i++) {
if (this._flags[i].entityId === entityId) return this._flags[i].flags;
}
return 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);
}
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] 有数字 buff 但场上无塔可刷', 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;
let rangeAdd = 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._isStatFt(ft) && ft !== BattleBuffFunctionType.DigitRange) continue;
if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, lv, 1)) continue;
const nums = parseFunctionParNums(row.functionpar);
const wan = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (ft === BattleBuffFunctionType.DigitDamage) {
if (wan === 0) continue;
attackMul *= 1 + wan / 10000;
} else if (ft === BattleBuffFunctionType.DigitCritRate) {
if (wan === 0) continue;
critRateAdd += wan / 10000;
} else if (ft === BattleBuffFunctionType.DigitCritDamage) {
if (wan === 0) continue;
critDmgAdd += wan / 10000;
} else if (ft === BattleBuffFunctionType.DigitAttackSpeed) {
if (wan === 0) continue;
aspdAdd += wan;
} else if (ft === BattleBuffFunctionType.DigitRange) {
if (wan === 0) continue;
rangeAdd += Math.abs(wan) <= 16 ? wan * BattleMapSpec.CELL_SIZE : wan;
}
}
if (attackMul === 1 && aspdAdd === 0 && critRateAdd === 0 && critDmgAdd === 0 && rangeAdd === 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,
range: stats.range + rangeAdd,
};
}
/**
* 单塔刷新 101~112。
* @param session 会话
* @param entityId 实体
* @param ownerPlayerId 归属
*/
refreshEntity(session: BattleSession, entityId: EntityId, ownerPlayerId: number): void {
this._clearDigitMods(session, entityId);
this._clearFlags(entityId);
const pid = Math.max(0, Math.floor(Number(ownerPlayerId) || 0));
const loadout = getTowerLoadoutService().getLoadout(entityId);
if (!loadout) {
getPassiveSkillHost().syncBuffSkills(session, entityId, []);
return;
}
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, loadout.weapon1Id);
if (!hero) {
console.warn('[BattleBuffDigitStat] Hero 缺失', loadout.weapon1Id, entityId);
getPassiveSkillHost().syncBuffSkills(session, 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);
getPassiveSkillHost().syncBuffSkills(session, entityId, []);
return;
}
const snaps = this._buildSnaps(session, pid);
let rank = 0;
for (let i = 0; i < snaps.length; i++) {
if (snaps[i].entityId === entityId) {
rank = snaps[i].rank;
break;
}
}
const grantSkills: number[] = [];
const flags = this._emptyFlags();
const defaultSkillId = Math.floor(Number(hero.skill_id) || 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;
if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, level, rank)) continue;
const ft = Math.floor(Number(row.functiontype) || 0);
const ft1 = Math.floor(Number(row.functiontype1) || 0);
this._applyPrimary(session, entityId, row, ft, i, grantSkills, flags, defaultSkillId);
this._applySecondary(row, ft1, flags, defaultSkillId);
}
this._setFlags(entityId, flags);
getPassiveSkillHost().syncBuffSkills(session, entityId, grantSkills);
}
private _applyPrimary(
session: BattleSession,
entityId: EntityId,
row: ITableBattlebuff,
ft: number,
index: number,
grantSkills: number[],
flags: DigitCombatFlags,
defaultSkillId: number,
): void {
const source = `${DIGIT_STAT_SOURCE}${row.id}_${index}`;
const nums = parseFunctionParNums(row.functionpar);
if (ft === BattleBuffFunctionType.DigitDamage
|| ft === BattleBuffFunctionType.DigitCritRate
|| ft === BattleBuffFunctionType.DigitCritDamage
|| ft === BattleBuffFunctionType.DigitAttackSpeed) {
const wan = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (wan === 0) {
console.warn('[BattleBuffDigitStat] functionpar 无效', row.id, row.functionpar);
return;
}
this._applyStat(session, entityId, ft, wan, source);
return;
}
if (ft === BattleBuffFunctionType.DigitGrantSkill) {
const skillId = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (!(skillId > 0)) {
console.warn('[BattleBuffDigitStat] ft105 缺 Skill.id', row.id, row.functionpar);
return;
}
grantSkills.push(skillId);
return;
}
if (ft === BattleBuffFunctionType.DigitRange) {
const raw = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (raw === 0) {
console.warn('[BattleBuffDigitStat] ft106 缺距离', row.id, row.functionpar);
return;
}
const addPx = Math.abs(raw) <= 16 ? raw * BattleMapSpec.CELL_SIZE : raw;
this._ensureAttr(session, entityId);
session.attributes.addModifier(entityId, {
attr: AttrId.Range, op: 'Add', value: addPx, source,
});
return;
}
if (ft === BattleBuffFunctionType.DigitPierce) {
const n = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (!(n > 0)) {
console.warn('[BattleBuffDigitStat] ft108 缺穿透', row.id, row.functionpar);
return;
}
flags.pierceAdd += n;
return;
}
if (ft === BattleBuffFunctionType.DigitExtraArrow) {
const n = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (!(n > 0)) {
console.warn('[BattleBuffDigitStat] ft109 缺箭数', row.id, row.functionpar);
return;
}
flags.extraArrow += n;
return;
}
if (ft === BattleBuffFunctionType.DigitChanceMulDmg) {
if (nums.length < 2) {
console.warn('[BattleBuffDigitStat] ft111 需 概率,倍数', row.id, row.functionpar);
return;
}
flags.chanceMul = { wan: Math.floor(nums[0]), mul: Math.floor(nums[1]) };
return;
}
if (ft === BattleBuffFunctionType.DigitCondForceCrit) {
const t = Math.floor(Number(row.target) || 0);
if (t === 2) flags.condCrit = 'poison';
else if (t === 3) flags.condCrit = 'slow';
else if (t === 4) flags.condCrit = 'always_convert';
else console.warn('[BattleBuffDigitStat] ft112 无法从 target 推断模式', row.id, t);
return;
}
if (ft === BattleBuffFunctionType.DigitSkillDmgMul) {
const parsed = parseBattleBuffSkillListMul(row.functionpar);
if (!parsed) {
console.warn('[BattleBuffDigitStat] ft107 functionpar 无效', row.id, row.functionpar);
return;
}
flags.skillMuls.push({ skillIds: this._expandWeaponSkillIds(parsed.skillIds), mul: parsed.mul });
return;
}
if (ft === BattleBuffFunctionType.DigitDefaultSkillDmgMul) {
this._pushDefaultSkillMul(row, row.functionpar, flags, defaultSkillId);
}
}
/**
* @param row Battlebuff 行
* @param ft1 functiontype1
* @param flags 塔战斗标记
* @param defaultSkillId 该数字 Hero.skill_id
*/
private _applySecondary(row: ITableBattlebuff, ft1: number, flags: DigitCombatFlags, defaultSkillId: number): void {
if (!(ft1 > 0)) return;
if (ft1 === BattleBuffFunctionType.DigitDefaultSkillDmgMul) {
this._pushDefaultSkillMul(row, row.functionpar1, flags, defaultSkillId);
return;
}
const parsed = parseBattleBuffSkillListMul(row.functionpar1);
if (!parsed) {
console.warn('[BattleBuffDigitStat] functiontype1 参数无效', row.id, ft1, row.functionpar1);
return;
}
const skillIds = this._expandWeaponSkillIds(parsed.skillIds);
if (ft1 === BattleBuffFunctionType.DigitSkillDmgMul) {
flags.skillMuls.push({ skillIds, mul: parsed.mul });
return;
}
if (ft1 === BattleBuffFunctionType.DigitSkillForceCrit) {
flags.forceCritSkills.push({ skillIds, convert: true });
return;
}
console.warn('[BattleBuffDigitStat] 未知 functiontype1', row.id, ft1);
}
/**
* 113:对锁定数字的 Hero.skill_id 技能施加伤害倍数。
* @param row Battlebuff 行
* @param raw functionpar / functionpar1(倍数)
* @param flags 塔战斗标记
* @param defaultSkillId Hero.skill_id
*/
private _pushDefaultSkillMul(
row: ITableBattlebuff,
raw: string | number | null | undefined,
flags: DigitCombatFlags,
defaultSkillId: number,
): void {
const nums = parseFunctionParNums(raw);
const mul = nums.length > 0 ? Math.floor(nums[0]) : 0;
if (!(mul > 0)) {
console.warn('[BattleBuffDigitStat] ft113 倍数无效', row.id, raw);
return;
}
if (!(defaultSkillId > 0)) {
console.warn('[BattleBuffDigitStat] ft113 目标数字无 skill_id', row.id);
return;
}
flags.skillMuls.push({ skillIds: [defaultSkillId], mul });
}
/**
* Weapon.id 列表展开为 act_skill(Skill.id),便于伤害管线按 Ability 匹配。
* @param weaponIds Weapon.id
*/
private _expandWeaponSkillIds(weaponIds: readonly number[]): number[] {
const out: number[] = [];
for (let i = 0; i < weaponIds.length; i++) {
const wid = weaponIds[i];
const w = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, wid);
if (!w) {
console.warn('[BattleBuffDigitStat] 技能列表缺 Weapon', wid);
continue;
}
const sid = Math.floor(Number(w.act_skill) || 0);
if (!(sid > 0)) {
console.warn('[BattleBuffDigitStat] Weapon 无 act_skill', wid);
continue;
}
if (out.indexOf(sid) < 0) out.push(sid);
}
return out;
}
private _buildSnaps(session: BattleSession, playerId: number): DigitUnitSnap[] {
const battle = this._battle;
const out: DigitUnitSnap[] = [];
const seen: EntityId[] = [];
const push = (entityId: EntityId): void => {
if (this._hasSeen(seen, entityId)) return;
const loadout = getTowerLoadoutService().getLoadout(entityId);
if (!loadout) return;
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, loadout.weapon1Id);
if (!hero) return;
if (!session.world.isAlive(entityId)) return;
if (!session.world.getComponent(entityId, CombatComp)) return;
seen.push(entityId);
out.push({
entityId,
heroType: this._resolveElementType(loadout.weapon1Id, hero),
level: this._resolveMatchLevel(hero, loadout.level),
rank: 0,
});
};
const player = battle?.getPlayer(playerId);
const deployed = player?.deployedUnits ?? [];
for (let i = 0; i < deployed.length; i++) push(deployed[i]);
session.world.view2(CombatComp, TeamComp).forEach((entityId, _c, team) => {
if (team.ownerId !== playerId) return;
push(entityId);
});
for (let elem = 1; elem <= 5; elem++) {
const group: DigitUnitSnap[] = [];
for (let i = 0; i < out.length; i++) {
if (out[i].heroType === elem) group.push(out[i]);
}
group.sort((a, b) => b.level - a.level || a.entityId - b.entityId);
for (let i = 0; i < group.length; i++) group[i].rank = i + 1;
}
return out;
}
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));
}
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;
const ft = Math.floor(Number(row.functiontype) || 0);
const ft1 = Math.floor(Number(row.functiontype1) || 0);
if (this._isDigitFt(ft) || ft1 > 0) return true;
}
return false;
}
private _isStatFt(ft: number): boolean {
return ft === BattleBuffFunctionType.DigitDamage
|| ft === BattleBuffFunctionType.DigitCritRate
|| ft === BattleBuffFunctionType.DigitCritDamage
|| ft === BattleBuffFunctionType.DigitAttackSpeed;
}
private _isDigitFt(ft: number): boolean {
return this._isStatFt(ft)
|| ft === BattleBuffFunctionType.DigitGrantSkill
|| ft === BattleBuffFunctionType.DigitRange
|| ft === BattleBuffFunctionType.DigitPierce
|| ft === BattleBuffFunctionType.DigitExtraArrow
|| ft === BattleBuffFunctionType.DigitChanceMulDmg
|| ft === BattleBuffFunctionType.DigitCondForceCrit
|| ft === BattleBuffFunctionType.DigitSkillDmgMul
|| ft === BattleBuffFunctionType.DigitSkillForceCrit
|| ft === BattleBuffFunctionType.DigitDefaultSkillDmgMul;
}
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 _ensureAttr(session: BattleSession, entityId: EntityId): void {
if (!session.world.getComponent(entityId, AttributeComp)) {
session.world.addComponent(entityId, new AttributeComp());
}
}
private _applyStat(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;
this._ensureAttr(session, entityId);
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,
});
}
}
private _emptyFlags(): DigitCombatFlags {
return {
skillMuls: [],
forceCritSkills: [],
chanceMul: null,
condCrit: null,
pierceAdd: 0,
extraArrow: 0,
};
}
private _clearFlags(entityId: EntityId): void {
for (let i = this._flags.length - 1; i >= 0; i--) {
if (this._flags[i].entityId === entityId) this._flags.splice(i, 1);
}
}
private _setFlags(entityId: EntityId, flags: DigitCombatFlags): void {
this._clearFlags(entityId);
const empty = flags.skillMuls.length <= 0
&& flags.forceCritSkills.length <= 0
&& !flags.chanceMul
&& !flags.condCrit
&& flags.pierceAdd <= 0
&& flags.extraArrow <= 0;
if (empty) return;
this._flags.push({ entityId, flags });
}
}
let _host: BattleBuffDigitStatHost | null = null;
/** 局内数字属性 Host */
export function getBattleBuffDigitStatHost(): BattleBuffDigitStatHost {
if (!_host) _host = new BattleBuffDigitStatHost();
return _host;
}