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.
416 lines
16 KiB
416 lines
16 KiB
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<number, string> = {
|
|
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<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)
|
|
: [];
|
|
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<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;
|
|
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;
|
|
}
|
|
|