import type { EntityId, World } from 'db://assets/mx/module/ecs'; import { GridOccupancyComp, TagComp, UnitMetaComp } from 'db://assets/mx/module/battle/ecs/Components'; import { isElementGlyphType } from 'db://assets/mx/module/merge'; import { TableNames } from '../../game/ConfigTableData'; import { PEACH_TREE_WEAPON2_ID } from '../bag/BagPrepConfig'; /** 塔 unitDefId 前缀 */ const TOWER_DEF_PREFIX = 'tower_w1_'; /** 塔 Hero 标签前缀 */ const WEAPON1_TAG_PREFIX = 'weapon1_'; /** * 英雄对应 Text.id(五行:type+level;蟠桃:102)。 * @param hero Hero 行 */ export function heroCharacterIds(hero: ITableHero): readonly number[] { const ht = Math.floor(Number(hero.herotype)); if (ht >= 1 && ht <= 5) { const lv = Math.floor(Number(hero.level)); const texts = gg.data.table.getTableList(TableNames.Text) ?? []; for (let i = 0; i < texts.length; i++) { const row = texts[i]; if (Math.floor(Number(row.type)) !== ht) continue; if (Math.floor(Number(row.level)) !== lv) continue; const id = Math.floor(Number(row.id)); if (id > 0) return [id]; } console.log('[BattleHeroTable] 五行 Text 缺失', hero.id, ht, lv); return []; } if (Math.floor(Number(hero.id)) === PEACH_TREE_WEAPON2_ID) { return [PEACH_TREE_WEAPON2_ID]; } return []; } /** 指定合成等级对应的普攻技能 id */ export function heroNormalSkillId(hero: ITableHero, composeLevel: number): number { return _skillIdAtLevel(hero.atk_id, composeLevel); } /** * 英雄技能 id(`Hero.skill_id`;0=无)。 * 主动/被动由 Skill.type 决定,挂载见 TowerLoadoutService。 * @param hero Hero 行 * @param _composeLevel 保留签名(skill_id 不按等级取下标) */ export function heroUltSkillId(hero: ITableHero | null | undefined, _composeLevel = 1): number { if (!hero) return 0; return Math.floor(Number(hero.skill_id) || 0); } /** 是否基础单字英雄 */ export function isBasicHero(hero: ITableHero): boolean { return Number(hero.herotype) === 1; } /** * 五行字落下即可攻击来敌(对应 Hero 有普攻)。 * @param textId Text.id */ export function isAttackGlyph(textId: number): boolean { const id = Number(textId); if (!(id > 0)) return false; const text = gg.data.table.getTableData(TableNames.Text, id); if (!text) return false; const type = Math.floor(Number(text.type)); if (!isElementGlyphType(type)) return false; const level = Math.max(1, Math.floor(Number(text.level) || 1)); const heroes = gg.data.table.getTableList(TableNames.Hero) ?? []; for (let i = 0; i < heroes.length; i++) { const hero = heroes[i]; if (Math.floor(Number(hero.herotype)) !== type) continue; if (Math.floor(Number(hero.level)) !== level) continue; return heroNormalSkillId(hero, 1) > 0; } return false; } /** 是否五行数字英雄(herotype 1–5) */ export function isElementHero(hero: ITableHero | null | undefined): boolean { if (!hero) return false; const t = Math.floor(Number(hero.herotype)); return t >= 1 && t <= 5; } /** * Hero.id 是否为五行战斗单位(有普攻)。 * @param heroId Hero.id */ export function isElementCombatHeroId(heroId: number): boolean { const id = Math.floor(Number(heroId) || 0); if (!(id > 0)) return false; const hero = gg.data.table.getTableData(TableNames.Hero, id); if (!isElementHero(hero)) return false; return heroNormalSkillId(hero!, 1) > 0; } /** * 从塔实体解析 Hero.id(unitDefId / weapon1_ 标签)。 * @param world 战斗世界 * @param entityId 实体 */ export function resolveTowerHeroId(world: World, entityId: EntityId): number { const defId = world.getComponent(entityId, UnitMetaComp)?.unitDefId ?? ''; if (defId.startsWith(TOWER_DEF_PREFIX)) { const id = Number(defId.slice(TOWER_DEF_PREFIX.length)); if (id > 0) return id; } const tags = world.getComponent(entityId, TagComp)?.tags; if (tags) { for (let i = 0; i < tags.length; i++) { const t = tags[i]; if (!t.startsWith(WEAPON1_TAG_PREFIX)) continue; const id = Number(t.slice(WEAPON1_TAG_PREFIX.length)); if (id > 0) return id; } } return 0; } /** * 从塔实体解析 Hero 行。 * @param world 战斗世界 * @param entityId 实体 */ function resolveTowerHero(world: World, entityId: EntityId): ITableHero | null { const heroId = resolveTowerHeroId(world, entityId); if (!(heroId > 0)) return null; const hero = gg.data.table.getTableData(TableNames.Hero, heroId); if (!hero) { console.log('[BattleHeroTable] Hero 缺失', heroId); return null; } return hero; } /** * 塔合成/显示等级(优先 UnitMetaComp.level,否则 Hero.level)。 * @param world 战斗世界 * @param entityId 实体 */ export function resolveTowerComposeLevel(world: World, entityId: EntityId): number { const fromMeta = Math.floor(Number(world.getComponent(entityId, UnitMetaComp)?.level) || 0); if (fromMeta > 0) return fromMeta; const hero = resolveTowerHero(world, entityId); return Math.max(0, Math.floor(Number(hero?.level) || 0)); } /** * 怪物技能 target_stage=1 的默认敌人:敌方阵营五行单字(herotype 1–5 金木水火土),排除多格组合。 * 新表 herotype 表示五行,不再用旧口径「herotype≠2=非组合神将」。 * @param world 战斗世界 * @param entityId 候选目标 */ export function isMonsterDefaultEnemyTarget(world: World, entityId: EntityId): boolean { if (isPeachTreeTower(world, entityId)) return false; const hero = resolveTowerHero(world, entityId); if (!isElementHero(hero)) return false; const cells = world.getComponent(entityId, GridOccupancyComp)?.cells; return !(cells && cells.length > 1); } /** * 是否蟠桃树塔(Hero/Text id=102,herotype=3)。 * @param world 战斗世界 * @param entityId 候选目标 */ export function isPeachTreeTower(world: World, entityId: EntityId): boolean { return resolveTowerHeroId(world, entityId) === PEACH_TREE_WEAPON2_ID; } /** * atk_buff1 类型 12:锁五行单字(非多格)。损毁态由调用方判定,本表不依赖 TowerRuinHost。 * @param world 战斗世界 * @param entityId 候选目标 */ export function isMonsterBasicHeroTarget(world: World, entityId: EntityId): boolean { return isMonsterDefaultEnemyTarget(world, entityId); } /** * atk_type=9 按将 id 索敌(Hero.weapon 已删,不再匹配)。 * @param _world 战斗世界 * @param _entityId 候选目标 * @param _weaponType 将类型 */ export function isHeroWeaponTarget(_world: World, _entityId: EntityId, _weaponType: number): boolean { return false; } /** * Text 对应 Hero 行(五行 type+level;蟠桃 id=102)。 * @param text Text 行 */ export function heroForTextRow(text: ITableText | null | undefined): ITableHero | null { if (!text) return null; const textId = Math.floor(Number(text.id)); if (textId === PEACH_TREE_WEAPON2_ID) { return gg.data.table.getTableData(TableNames.Hero, PEACH_TREE_WEAPON2_ID); } const type = Math.floor(Number(text.type)); if (!isElementGlyphType(type)) return null; const level = Math.max(1, Math.floor(Number(text.level) || 1)); const heroes = gg.data.table.getTableList(TableNames.Hero) ?? []; for (let i = 0; i < heroes.length; i++) { const hero = heroes[i]; if (Math.floor(Number(hero.herotype)) !== type) continue; if (Math.floor(Number(hero.level)) !== level) continue; return hero; } return null; } /** 文字块 Spine 皮肤名(Hero.res) */ export function textIconName(text: ITableText): string { const hero = heroForTextRow(text); const res = (hero?.res ?? '').trim(); if (!res) { console.log('[BattleHeroTable] textIconName 无 Hero.res', text?.id); } return res; } /** 文字块显示名(Hero.name / number) */ export function textDisplayName(text: ITableText): string { const hero = heroForTextRow(text); if (hero) { const name = (hero.name ?? '').trim(); if (name) return name; const num = Math.floor(Number(hero.number)); if (num > 0) return String(num); } return textIconName(text); } /** 数组按下标取技能 id;composeLevel 从 1 起,越界 clamp */ function _skillIdAtLevel(ids: readonly number[] | null | undefined, composeLevel: number): number { if (!ids || ids.length <= 0) return 0; const idx = Math.max(0, Math.min(composeLevel - 1, ids.length - 1)); return ids[idx] ?? 0; }