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/ai/runtime/AiYingVisualHost.ts

171 lines
5.8 KiB

3 weeks ago
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);
}
}