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'; import { Weapon2TileType, bagToolPrefabName, isBagToolTextType, isElementGlyphType } from 'db://assets/mx/module/merge'; 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'; import { getBattleBuffDigitStatHost } from './BattleBuffDigitStatHost'; 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'; const TILE_TYPE_NAMES: Record = { 1: 'Element1', 2: 'Element2', /** type=3 与遗留 Grid 同值;新产品按五行 3 读 */ [Weapon2TileType.Grid]: 'Element3OrGrid', 4: 'Element4Fire', 5: 'Element5', }; /** 文字 / 塔数据查询(战斗层唯一读入口) */ 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); } 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; if (isBagToolTextType(Number(tileType))) { const toolName = bagToolPrefabName(Number(tileType)) ?? ''; return { location, tile, tileType, tileTypeName: toolName, level: Math.max(0, Math.floor(Number(record.level) || 0)), weapon1Id: 0, tower: null, composition: null, source: record.source ?? null, selectedEntries: [], }; } 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, }; } /** * 按地图格一站式查询。 * @param location 地图格位置 * @param record 字块记录 * @param options 合成、玩家、部署、session */ queryByMapGrid( location: Extract, 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(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) : []; const withSkills = this._applySelectedSkillsPreview(setup, entries); const pid = p?.playerId ?? gg.game.CurentBattle?.localPlayerId ?? 0; const stats = getBattleBuffDigitStatHost().previewOntoStats( withSkills, weapon1Id, setup.effectiveLevel, pid, ); 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(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(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; 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; } /** 从 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; 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; } // 蟠桃等非五行工具字:单 id 解析 return provider.getWeapon1ByGroupSequence([record.weapon2Id]) ?? 0; } } /** @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; }