import { Node, Vec3, sp } from 'cc'; import { aiLog } from './AiLog'; import type { ComposedUnit } from 'db://assets/mx/module/merge'; import type { MapManager } from '../../map/legacy/MapManager'; import { GBundle, GPath } from '../../../game/ConfigRes'; import { YING_WEAPON1_ID } from '../../heros/HeroActorRes'; import { GLYPH_ACTIVE_SKIN_BUCKETS, GLYPH_SKIN_CAMP_ENEMY, applyGlyphSpineSkin, resolveActiveGlyphSkin, } from '../../view/GlyphSpineSkin'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; /** 单个营展示记录 */ interface YingVisualRec { /** 合成锚点格 */ anchorGrid: number; /** 展示节点 */ node: Node; /** 合成等级(切皮) */ level: number; } /** * AI 营字展示:营无 HeroActor,玩家侧靠 TextGrid;AI 无头 bag 需独立挂 营.prefab。 * 敌方皮肤后缀 / 阵营位固定为 2(皮肤字2_*)。 */ export class AiYingVisualHost { private readonly _recs: YingVisualRec[] = []; private _parent: Node | null = null; /** * 按 AI compositions 对账营展示。 * @param compositions AI 合成体 * @param map 地图 */ sync(compositions: readonly ComposedUnit[], map: MapManager): void { const parent = map.ensureHeroParent(); if (!parent?.isValid) { aiLog('[AiYingVisualHost] heroParent missing'); return; } this._parent = parent; const keep: number[] = []; for (let i = 0; i < compositions.length; i++) { const comp = compositions[i]; if (comp.weapon1Id !== YING_WEAPON1_ID) continue; const anchor = this._anchorGrid(comp); if (anchor < 0) continue; keep.push(anchor); const worldPos = this._worldCenter(comp, map); if (!worldPos) continue; this._ensure(anchor, worldPos, parent, comp.level ?? 1); } for (let i = this._recs.length - 1; i >= 0; i--) { if (keep.indexOf(this._recs[i].anchorGrid) >= 0) continue; this._destroyAt(i); } } /** 清空展示 */ clear(): void { for (let i = this._recs.length - 1; i >= 0; i--) { this._destroyAt(i); } this._parent = null; } private _anchorGrid(comp: ComposedUnit): number { let min = -1; for (let i = 0; i < comp.gridIndices.length; i++) { const g = comp.gridIndices[i]; if (min < 0 || g < min) min = g; } return min; } private _worldCenter(comp: ComposedUnit, map: MapManager): Vec3 | null { let sx = 0; let sy = 0; let sz = 0; let n = 0; for (let i = 0; i < comp.gridIndices.length; i++) { const mg = map.getMapGridByIndex(comp.gridIndices[i]); if (!mg?.node?.isValid) continue; const p = mg.node.worldPosition; sx += p.x; sy += p.y; sz += p.z; n++; } if (n <= 0) return null; return new Vec3(sx / n, sy / n, sz / n); } private _ensure(anchor: number, worldPos: Vec3, parent: Node, level: number): void { for (let i = 0; i < this._recs.length; i++) { if (this._recs[i].anchorGrid !== anchor) continue; const rec = this._recs[i]; const node = rec.node; if (node?.isValid) { node.active = true; node.setWorldPosition(worldPos); if (rec.level !== level) { rec.level = level; this._applyEnemyCampSkin(node, level); } return; } this._recs.splice(i, 1); break; } const node = gg.res.getNode(GPath.HeroPrefab(), '营', GBundle.BattleHero); if (!node?.isValid) { aiLog('[AiYingVisualHost] 营 prefab missing'); return; } node.parent = parent; node.active = true; node.setWorldPosition(worldPos); this._applyEnemyCampSkin(node, level); this._recs.push({ anchorGrid: anchor, node, level }); } /** * 敌方营皮:皮肤字2_{档}。 * @param root 营节点 * @param level 合成等级 */ private _applyEnemyCampSkin(root: Node, level: number): void { const anim = this._findAnim(root); if (!anim?.isValid) { aiLog('[AiYingVisualHost] 营 Spine 未就绪'); return; } const camp = GLYPH_SKIN_CAMP_ENEMY; const preferred = resolveActiveGlyphSkin(level, camp); const extras: string[] = []; for (let i = 0; i < GLYPH_ACTIVE_SKIN_BUCKETS.length; i++) { const name = `皮肤字${camp}_${GLYPH_ACTIVE_SKIN_BUCKETS[i]}`; if (name !== preferred) extras.push(name); } if (!applyGlyphSpineSkin(anim, preferred, extras)) { aiLog('[AiYingVisualHost] 敌方营皮未套上', preferred); } if (anim.skeletonData) ensureSpineSkeletonDataRuntime(anim.skeletonData); const runtime = anim.skeletonData?.getRuntimeData?.(true); if (runtime?.findAnimation?.('待机')) { anim.setAnimation(0, '待机', true); } } private _findAnim(root: Node): sp.Skeleton | null { const animNode = root.getChildByName('动画'); return animNode?.getComponent(sp.Skeleton) ?? root.getComponent(sp.Skeleton) ?? root.getComponentInChildren(sp.Skeleton); } private _destroyAt(index: number): void { const rec = this._recs[index]; if (!rec) return; if (rec.node?.isValid) { if (rec.node['resKey']) gg.res.putNode(rec.node); else rec.node.destroy(); } this._recs.splice(index, 1); } }