|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|