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

402 lines
15 KiB

3 weeks ago
import type { BattleSession } from 'db://assets/mx/module/battle';
import { AttrId, AbilitySetComp, CombatComp, UnitMetaComp } from 'db://assets/mx/module/battle/ecs/Components';
import type { ComposedUnit } from 'db://assets/mx/module/merge';
2 weeks ago
import { Weapon2TileType, isElementGlyphType } from 'db://assets/mx/module/merge';
3 weeks ago
import type { EntityId } from 'db://assets/mx/module/ecs';
import type { Player } from '../../game/Player';
import { TableNames } from '../../game/ConfigTableData';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
import { heroCharacterIds, heroNormalSkillId, heroUltSkillId } from '../config/BattleHeroTable';
import type { DeploySync } from '../runtime/DeploySync';
import {
buildDeployedTowerInfo,
findCompositionForEntity,
findEntityIdByGridIndex as queryEntityByGrid,
} from '../runtime/DeployEntityQuery';
import { getTowerLoadoutService, resolveHeroBattleWeaponId } from './TowerLoadoutService';
3 days ago
import { getBattleBuffDigitStatHost } from './BattleBuffDigitStatHost';
3 weeks ago
import { deriveTowerSetupStats } from './TowerStatResolver';
import { parseEntryBaseAttr } from '../skill/SkillEntryParse';
import { entriesAffectingWeapon } from '../skill/GlobalSkillEffects';
import type {
TileBindRecord,
TileEntitySnapshot,
TileLocation,
TileQueryResult,
TowerCombatStats,
TowerConfigSnapshot,
TowerRuntimeSnapshot,
} from './TowerQueryTypes';
2 weeks ago
const TILE_TYPE_NAMES: Record<number, string> = {
1: 'Element1',
2: 'Element2',
1 week ago
/** type=3 与遗留 Grid 同值;新产品按五行 3 读 */
[Weapon2TileType.Grid]: 'Element3OrGrid',
2 weeks ago
4: 'Element4Fire',
5: 'Element5',
3 weeks ago
};
/** 文字 / 塔数据查询(战斗层唯一读入口) */
export class TowerQueryService {
private static _instance: TowerQueryService | null = null;
static get instance(): TowerQueryService {
if (!this._instance) this._instance = new TowerQueryService();
return this._instance;
}
/**
* 构建文字实体完整数据。
* @param location 背包 / 地图位置
* @param record 字块记录
* @param options 合成、玩家
*/
buildTileEntity(
location: TileLocation,
record: TileBindRecord,
options?: { composition?: ComposedUnit | null; player?: Player | null },
): TileEntitySnapshot | null {
const cardKind = Math.floor(Number(record.battleCardKind) || 0);
if (cardKind > 0) {
return this._buildBattleCardEntity(location, record, cardKind);
}
3 weeks ago
WeaponComposeConfigProvider.instance.load();
const provider = WeaponComposeConfigProvider.instance;
const tile = provider.getWeapon2Config(record.weapon2Id);
if (!tile) return null;
const tileType = provider.getWeapon2Type(record.weapon2Id) as Weapon2TileType;
const composition = options?.composition ?? null;
const player = options?.player ?? gg.game.localPlayer ?? null;
const level = record.level ?? composition?.level ?? 1;
const weapon1Id = this._resolveWeapon1Id(record, composition, tileType);
const tower = weapon1Id > 0 ? this.buildConfigSnapshot(weapon1Id, level, player) : null;
return {
location,
tile,
tileType,
tileTypeName: TILE_TYPE_NAMES[tileType] ?? '',
level,
weapon1Id,
tower,
composition,
source: record.source ?? null,
selectedEntries: weapon1Id > 0
? entriesAffectingWeapon(player?.selectedSkills ?? [], weapon1Id)
: [],
};
}
/**
* 倍率卡/属性卡合成实体快照(无 Text 配表行)。
* @param location 位置
* @param record 槽记录
* @param cardKind BattleBagCardKind
*/
private _buildBattleCardEntity(
location: TileLocation,
record: TileBindRecord,
cardKind: number,
): TileEntitySnapshot {
const name = cardKind === 1 ? '倍率卡' : '属性卡';
const tile = {
id: record.weapon2Id,
name,
icon: name,
type: 0,
level: 0,
} as unknown as ITableText;
return {
location,
tile,
tileType: 0,
tileTypeName: name,
level: 1,
weapon1Id: 0,
tower: null,
composition: null,
source: record.source ?? null,
selectedEntries: [],
battleCardKind: cardKind,
};
}
3 weeks ago
/**
* 按地图格一站式查询。
* @param location 地图格位置
* @param record 字块记录
* @param options 合成、玩家、部署、session
*/
queryByMapGrid(
location: Extract<TileLocation, { kind: 'map' }>,
record: TileBindRecord,
options?: {
composition?: ComposedUnit | null;
player?: Player | null;
deploySync?: DeploySync | null;
session?: BattleSession | null;
},
): TileQueryResult | null {
const entity = this.buildTileEntity(location, record, options);
if (!entity) return null;
const session = options?.session ?? gg.game.CurentBattle?.session ?? null;
const runtime = session
? this.getRuntimeByGridIndex(location.gridIndex, session, options?.deploySync ?? null)
: null;
return { entity, runtime };
}
/**
* 塔配置快照(setupCombat 同源推导)。
* @param weapon1Id Weapon1 id
* @param level 等级
* @param player 玩家(主动技能替换)
*/
buildConfigSnapshot(
weapon1Id: number,
level: number,
player?: Player | null,
): TowerConfigSnapshot | null {
const w1 = gg.data.table.getTableData<ITableHero>(TableNames.Hero, weapon1Id);
if (!w1) return null;
const provider = WeaponComposeConfigProvider.instance;
const p = player ?? gg.game.localPlayer ?? null;
const battleWeaponId = resolveHeroBattleWeaponId(weapon1Id, p);
const setup = deriveTowerSetupStats(w1, level, battleWeaponId);
const entries = p?.selectedSkills
? entriesAffectingWeapon(p.selectedSkills, weapon1Id)
: [];
3 days ago
const withSkills = this._applySelectedSkillsPreview(setup, entries);
const pid = p?.playerId ?? gg.game.CurentBattle?.localPlayerId ?? 0;
const stats = getBattleBuffDigitStatHost().previewOntoStats(
withSkills, weapon1Id, setup.effectiveLevel, pid,
);
3 weeks ago
const normalSkillId = heroNormalSkillId(w1, setup.effectiveLevel);
const activeSkillId = p?.getActiveSkillBaseAtkId(weapon1Id, setup.effectiveLevel)
?? heroUltSkillId(w1, setup.effectiveLevel);
const ultRow = activeSkillId > 0
? gg.data.table.getTableData<ITableSkill>(TableNames.Skill, activeSkillId)
: null;
const baseCd = ultRow?.cd ?? 0;
const activeCd = baseCd > 0
? this._previewFinalCooldown(baseCd, entries)
: baseCd;
return {
config: w1,
level,
effectiveLevel: setup.effectiveLevel,
levelLimit: provider.getMaxWeaponLevel(weapon1Id),
stats: stats,
groupTiles: heroCharacterIds(w1)
.map(id => provider.getWeapon2Config(id))
.filter((row): row is ITableText => row != null),
normalAttack: normalSkillId > 0
? gg.data.table.getTableData<ITableSkill>(TableNames.Skill, normalSkillId)
: null,
activeSkill: {
config: activeSkillId > 0 ? ultRow : null,
cd: activeCd,
fromPlayerReplace: p?.hasActiveSkillOverride(weapon1Id) ?? false,
},
};
}
/**
* 已部署实体实时快照(AttributeSystem 最终值)。
* @param session 战斗 session
* @param entityId 实体 id
* @param deploySync 部署同步
*/
getRuntimeSnapshot(
session: BattleSession,
entityId: EntityId,
deploySync?: DeploySync | null,
): TowerRuntimeSnapshot | null {
const world = session.world;
if (!world.isAlive(entityId)) return null;
const loadout = getTowerLoadoutService().getLoadout(entityId);
if (!loadout) return null;
const stats = this._readRuntimeStats(session, entityId);
const set = world.getComponent(entityId, AbilitySetComp);
const meta = world.getComponent(entityId, UnitMetaComp);
const player = gg.game.localPlayer;
const map = gg.game.CurentBattle?.Map;
let deploy = null;
if (player && map) {
const comp = findCompositionForEntity(entityId, player, session, map);
if (comp) deploy = buildDeployedTowerInfo(comp);
}
return {
entityId,
loadout,
stats,
abilities: {
ids: set?.abilityIds ?? [],
cooldowns: set?.cooldowns ?? {},
},
level: meta?.level ?? loadout.level,
deploy,
};
}
/**
* 已部署实体最终战斗数值(含攻速符 aspd_add);无 Combat 时 null。
* @param session 战斗 session
* @param entityId 实体 id
*/
readLiveStats(session: BattleSession, entityId: EntityId): TowerCombatStats | null {
const world = session.world;
if (!world.isAlive(entityId)) return null;
if (!world.getComponent(entityId, CombatComp)) return null;
return this._readRuntimeStats(session, entityId);
}
/**
* 按地图格 index 查实时快照。
* @param gridIndex 地图格 index
* @param session 战斗 session
* @param deploySync 部署同步
*/
getRuntimeByGridIndex(
gridIndex: number,
session?: BattleSession | null,
deploySync?: DeploySync | null,
): TowerRuntimeSnapshot | null {
const s = session ?? gg.game.CurentBattle?.session ?? null;
if (!s) return null;
const entityId = this.findEntityIdByGridIndex(gridIndex, deploySync ?? null, s);
if (entityId == null) return null;
return this.getRuntimeSnapshot(s, entityId, deploySync);
}
/**
* 地图格 → ECS 实体 id。
* @param gridIndex 地图格 index
* @param deploySync 部署同步
* @param session 战斗 session
*/
findEntityIdByGridIndex(
gridIndex: number,
_deploySync: DeploySync | null,
session?: BattleSession | null,
): EntityId | null {
const s = session ?? gg.game.CurentBattle?.session ?? null;
const map = gg.game.CurentBattle?.Map;
const player = gg.game.localPlayer;
if (!s || !map) return null;
3 days ago
const fromDeploy = queryEntityByGrid(gridIndex, s, map, player);
if (fromDeploy != null) return fromDeploy;
// DeployedUnits 对账滞后时回退 GridMap.occupant
const g = map.getMapGridByIndex(gridIndex);
if (!g) return null;
const occ = s.grid.getCell({ row: g.Row, col: g.Col })?.occupant ?? null;
if (occ == null || !s.world.isAlive(occ)) return null;
return occ;
3 weeks ago
}
/** 从 AttributeSystem 读取最终战斗数值 */
private _readRuntimeStats(session: BattleSession, entityId: EntityId): TowerCombatStats {
const attrs = session.attributes;
const combat = session.world.getComponent(entityId, CombatComp);
const attackSpeed = attrs.getFinalAttackSpeed(entityId);
return {
attack: attrs.getAttr(entityId, AttrId.Attack),
attackSpeed,
attackInterval: attackSpeed > 0 ? 1 / attackSpeed : 0,
range: attrs.getAttr(entityId, AttrId.Range),
critRate: attrs.getAttr(entityId, AttrId.CritRate),
critDamage: attrs.getAttr(entityId, AttrId.CritDamage),
maxAttackSpeed: combat?.maxAttackSpeed ?? 0,
};
}
/** 未部署预览:叠 Player.selectedSkills 的 base_attr(与 AttributeSystem 乘算一致) */
private _applySelectedSkillsPreview(
stats: TowerCombatStats,
entries: readonly ITableSkill[],
): TowerCombatStats {
let attackMul = 1;
let aspdAdd = 0;
let critRateAdd = 0;
let critDmgAdd = 0;
let forceCrit = false;
for (const entry of entries) {
for (const { type, value } of parseEntryBaseAttr((entry as ITableSkill & { base_attr?: string }).base_attr ?? '')) {
switch (type) {
case 1: attackMul *= 1 + value / 10000; break;
case 2: aspdAdd += value; break;
case 3: critRateAdd += value / 10000; break;
case 4: critDmgAdd += value / 10000; break;
case 7: forceCrit = true; break;
default: break;
}
}
}
const attackSpeed = stats.attackSpeed * (1 + aspdAdd / 10000);
let critRate = Math.min(1, stats.critRate + critRateAdd);
let critDamage = stats.critDamage + critDmgAdd;
if (forceCrit) {
critDamage += critRate * 2;
critRate = 1;
}
return {
...stats,
attack: Math.floor(stats.attack * attackMul),
attackSpeed,
attackInterval: attackSpeed > 0 ? 1 / attackSpeed : 0,
critRate,
critDamage,
};
}
/** 备战预览:base_attr type5 与 AttributeSystem.getFinalCooldown 一致 */
private _previewFinalCooldown(baseCd: number, entries: readonly ITableSkill[]): number {
let cdAdd = 0;
for (const entry of entries) {
for (const { type, value } of parseEntryBaseAttr((entry as ITableSkill & { base_attr?: string }).base_attr ?? '')) {
if (type === 5) cdAdd += value;
}
}
return cdAdd > 0 ? baseCd / (1 + cdAdd / 10000) : baseCd;
}
private _resolveWeapon1Id(
record: TileBindRecord,
composition: ComposedUnit | null,
tileType: Weapon2TileType,
): number {
if (record.weapon1Id != null && record.weapon1Id > 0) return record.weapon1Id;
if (composition) return composition.weapon1Id;
if (record.weapon2Id <= 0) return 0;
2 weeks ago
const provider = WeaponComposeConfigProvider.instance;
// 五行数字:按 type + 显示等级解析 Hero
if (isElementGlyphType(Number(tileType))) {
const level = Math.max(1, Math.floor(Number(record.level) || 1));
return provider.getElementHeroId(Number(tileType), level) ?? 0;
3 weeks ago
}
2 weeks ago
// 蟠桃等非五行工具字:单 id 解析
return provider.getWeapon1ByGroupSequence([record.weapon2Id]) ?? 0;
3 weeks ago
}
}
/** @deprecated 使用 TowerQueryService.instance.buildTileEntity */
export function buildTextGridEntityData(
location: TileLocation,
record: TileBindRecord,
options?: { composition?: ComposedUnit | null; player?: Player | null },
): TileEntitySnapshot | null {
return TowerQueryService.instance.buildTileEntity(location, record, options);
}
export function resetTowerQueryServiceForTests(): void {
TowerQueryService['_instance'] = null;
}