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.
170 lines
6.5 KiB
170 lines
6.5 KiB
|
3 weeks ago
|
import { createMembershipSet, membershipAdd } from '../../../mx/tools/MembershipSet';
|
||
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
||
|
|
import type { IBattleRuntimeHandle } from '../types/ProjectBattleTypes';
|
||
|
|
import type { Player } from '../../game/Player';
|
||
|
|
import type { TextGrid } from '../view/TextGrid';
|
||
|
|
import { parseSkillGroup } from './BondConfigParser';
|
||
|
|
import { isPartnerWeaponActiveOnMap } from './BondEvaluator';
|
||
|
|
import { applyBondAttrsToEntity, removeBondModifiers } from './BondEffectApplier';
|
||
|
|
import { entriesAffectingWeapon } from '../skill/GlobalSkillEffects';
|
||
|
|
import { getTowerLoadoutService } from '../combat/TowerLoadoutService';
|
||
|
|
|
||
|
|
/** 局内羁绊宿主:按伙伴激活态动态物化 group 属性 */
|
||
|
|
export class InBattleBondHost {
|
||
|
|
private _runtime: IBattleRuntimeHandle | null = null;
|
||
|
|
private _mapGrids: ReadonlyMap<number, TextGrid> | undefined;
|
||
|
|
/** group2 伙伴上次是否在 map 攻击激活 */
|
||
|
|
private _group2PartnerActive = new Map<number, boolean>();
|
||
|
|
|
||
|
|
bindRuntime(runtime: IBattleRuntimeHandle): void {
|
||
|
|
this._runtime = runtime;
|
||
|
|
}
|
||
|
|
|
||
|
|
bindMapGrids(mapGrids: ReadonlyMap<number, TextGrid> | undefined): void {
|
||
|
|
this._mapGrids = mapGrids;
|
||
|
|
}
|
||
|
|
|
||
|
|
get mapGrids(): ReadonlyMap<number, TextGrid> | undefined {
|
||
|
|
return this._mapGrids;
|
||
|
|
}
|
||
|
|
|
||
|
|
unbind(): void {
|
||
|
|
this._runtime = null;
|
||
|
|
this._mapGrids = undefined;
|
||
|
|
this._group2PartnerActive.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* group2 羁绊:伙伴 Weapon1 在场且攻击激活时,词条效果翻倍(×2)。
|
||
|
|
* 配表 group2 为伙伴 Hero.id。
|
||
|
|
*/
|
||
|
|
getGroup2StackMultiplier(entry: ITableSkill): number {
|
||
|
|
const partnerId = Number((entry as ITableSkill & { group2?: number }).group2);
|
||
|
|
if (!Number.isFinite(partnerId) || partnerId <= 0) return 1;
|
||
|
|
if (!this._runtime) return 1;
|
||
|
|
return isPartnerWeaponActiveOnMap(this._runtime, partnerId, this._mapGrids) ? 2 : 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 组合激活态变化 / 词条物化后刷新 */
|
||
|
|
refreshPlayer(player: Player): void {
|
||
|
|
const runtime = this._runtime;
|
||
|
|
if (!runtime) return;
|
||
|
|
const session = runtime.session;
|
||
|
|
const loadoutSvc = getTowerLoadoutService();
|
||
|
|
const byWeapon = new Map<number, EntityId[]>();
|
||
|
|
|
||
|
|
for (const entityId of player.deployedUnits) {
|
||
|
|
if (!session.world.isAlive(entityId)) continue;
|
||
|
|
const weapon1Id = loadoutSvc.getLoadout(entityId)?.weapon1Id;
|
||
|
|
if (!weapon1Id) continue;
|
||
|
|
if (!byWeapon.has(weapon1Id)) byWeapon.set(weapon1Id, []);
|
||
|
|
byWeapon.get(weapon1Id)!.push(entityId);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const [weapon1Id, entities] of byWeapon) {
|
||
|
|
const entries = entriesAffectingWeapon(player.selectedSkills, weapon1Id);
|
||
|
|
for (const entityId of entities) {
|
||
|
|
this._refreshEntity(entityId, entries);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this._rematerializeGroup2WeaponsIfPartnerChanged(player);
|
||
|
|
}
|
||
|
|
|
||
|
|
refreshEntity(entityId: EntityId, player: Player): void {
|
||
|
|
const loadout = getTowerLoadoutService().getLoadout(entityId);
|
||
|
|
if (!loadout) return;
|
||
|
|
const entries = entriesAffectingWeapon(player.selectedSkills, loadout.weapon1Id);
|
||
|
|
this._refreshEntity(entityId, entries);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _refreshEntity(
|
||
|
|
entityId: EntityId,
|
||
|
|
entries: readonly ITableSkill[],
|
||
|
|
): void {
|
||
|
|
const runtime = this._runtime;
|
||
|
|
if (!runtime) return;
|
||
|
|
const session = runtime.session;
|
||
|
|
if (!session.world.isAlive(entityId)) return;
|
||
|
|
|
||
|
|
removeBondModifiers(session, entityId);
|
||
|
|
|
||
|
|
const countById = new Map<number, number>();
|
||
|
|
for (const entry of entries) {
|
||
|
|
countById.set(entry.id, (countById.get(entry.id) ?? 0) + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
const seen = createMembershipSet<number>();
|
||
|
|
for (const entry of entries) {
|
||
|
|
if (seen.has(entry.id)) continue;
|
||
|
|
membershipAdd(seen, entry.id);
|
||
|
|
|
||
|
|
const groupRaw = String(entry.group ?? '').trim();
|
||
|
|
if (!groupRaw) continue;
|
||
|
|
const parsed = parseSkillGroup(groupRaw);
|
||
|
|
if (!parsed || parsed.targetType !== 1) continue;
|
||
|
|
if (!isPartnerWeaponActiveOnMap(runtime, parsed.partnerWeapon1Id, this._mapGrids)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const stack = countById.get(entry.id) ?? 1;
|
||
|
|
applyBondAttrsToEntity(
|
||
|
|
session,
|
||
|
|
entityId,
|
||
|
|
parsed.attrs,
|
||
|
|
`bond_entry_${entry.id}`,
|
||
|
|
stack,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 伙伴上下场后重算带 group2 词条的武器 */
|
||
|
|
private _rematerializeGroup2WeaponsIfPartnerChanged(player: Player): void {
|
||
|
|
const runtime = this._runtime;
|
||
|
|
if (!runtime) return;
|
||
|
|
const session = runtime.session;
|
||
|
|
|
||
|
|
const partnerIds = createMembershipSet<number>();
|
||
|
|
for (const entry of player.selectedSkills) {
|
||
|
|
const partnerId = Number((entry as ITableSkill & { group2?: number }).group2);
|
||
|
|
if (Number.isFinite(partnerId) && partnerId > 0) membershipAdd(partnerIds, partnerId);
|
||
|
|
}
|
||
|
|
if (partnerIds.size === 0) return;
|
||
|
|
|
||
|
|
let changed = false;
|
||
|
|
for (const partnerId of partnerIds.keys()) {
|
||
|
|
const active = isPartnerWeaponActiveOnMap(runtime, partnerId, this._mapGrids);
|
||
|
|
if (this._group2PartnerActive.get(partnerId) === active) continue;
|
||
|
|
this._group2PartnerActive.set(partnerId, active);
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
if (!changed) return;
|
||
|
|
|
||
|
|
const weapons = createMembershipSet<number>();
|
||
|
|
for (const entry of player.selectedSkills) {
|
||
|
|
const partnerId = Number((entry as ITableSkill & { group2?: number }).group2);
|
||
|
|
if (partnerId > 0 && partnerIds.has(partnerId)) {
|
||
|
|
membershipAdd(weapons, Number((entry as ITableSkill & { weapon?: number }).weapon) || 0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (weapons.size === 0) return;
|
||
|
|
|
||
|
|
const loadoutSvc = getTowerLoadoutService();
|
||
|
|
for (const entityId of player.deployedUnits) {
|
||
|
|
if (!session.world.isAlive(entityId)) continue;
|
||
|
|
const loadout = loadoutSvc.getLoadout(entityId);
|
||
|
|
if (!loadout || !weapons.has(loadout.weapon1Id)) continue;
|
||
|
|
this.refreshEntity(entityId, player);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let _host: InBattleBondHost | null = null;
|
||
|
|
|
||
|
|
export function getInBattleBondHost(): InBattleBondHost {
|
||
|
|
if (!_host) _host = new InBattleBondHost();
|
||
|
|
return _host;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resetInBattleBondHostForTests(): void {
|
||
|
|
_host = null;
|
||
|
|
}
|