|
|
|
|
import { EventTouch, Node, Tween, UIOpacity, Vec3 } from 'cc';
|
|
|
|
|
import { mapValuesToArray } from '../../../mx/tools/MembershipSet';
|
|
|
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
|
|
|
import type { ComposedUnit } from 'db://assets/mx/module/merge';
|
|
|
|
|
import { GBundle, GPath } from '../../game/ConfigRes';
|
|
|
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
|
|
|
import type { MapManager } from '../map/legacy/MapManager';
|
|
|
|
|
import { Skill } from '../skill/Skill';
|
|
|
|
|
import { HeroSkin } from '../view/HeroSkin';
|
|
|
|
|
import { HeroActor } from './HeroActor';
|
|
|
|
|
import {
|
|
|
|
|
GONG_WEAPON1_ID,
|
|
|
|
|
GUN_WEAPON1_ID,
|
|
|
|
|
QIANG_WEAPON1_ID,
|
|
|
|
|
isHeroActorWeapon1,
|
|
|
|
|
} from './HeroActorRes';
|
|
|
|
|
import { Gong } from './Gong';
|
|
|
|
|
import { Gun } from './Gun';
|
|
|
|
|
import { Qiang } from './Qiang';
|
|
|
|
|
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
|
|
|
|
|
|
|
|
|
|
interface ActorRec {
|
|
|
|
|
node: Node;
|
|
|
|
|
gridIndices: readonly number[];
|
|
|
|
|
/** 组合锚点格 index */
|
|
|
|
|
anchorGrid: number;
|
|
|
|
|
/** Hero.id,复用时必须与 composition.weapon1Id 一致 */
|
|
|
|
|
weapon1Id: number;
|
|
|
|
|
entityId: EntityId | null;
|
|
|
|
|
/** 敌方阵营皮肤(后缀 2 / 皮肤字2_*) */
|
|
|
|
|
enemyCamp: boolean;
|
|
|
|
|
/** 地图交换飞行:透明隐藏中(不关节点,避免 Gong.onDisable 拆掉待机/攻效) */
|
|
|
|
|
flightHidden: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 仅节点结构特殊的英雄保留子类;其余一律 HeroActor。
|
|
|
|
|
* 特殊表现走 skillId Presenter。
|
|
|
|
|
*/
|
|
|
|
|
const SPECIAL_CTORS: Readonly<Record<number, new () => Skill>> = {
|
|
|
|
|
[QIANG_WEAPON1_ID]: Qiang,
|
|
|
|
|
[GUN_WEAPON1_ID]: Gun,
|
|
|
|
|
[GONG_WEAPON1_ID]: Gong,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function resolveActorCtor(heroId: number): (new () => Skill) | null {
|
|
|
|
|
if (!isHeroActorWeapon1(heroId)) return null;
|
|
|
|
|
return SPECIAL_CTORS[heroId] ?? HeroActor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 新方案战斗实体:heroTemplate + HeroActor;与 Hero 表对齐 */
|
|
|
|
|
export class HeroActorHost {
|
|
|
|
|
private static _instance: HeroActorHost | null = null;
|
|
|
|
|
|
|
|
|
|
static get instance(): HeroActorHost {
|
|
|
|
|
if (!this._instance) this._instance = new HeroActorHost();
|
|
|
|
|
return this._instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _actorsByEntity = new Map<EntityId, ActorRec>();
|
|
|
|
|
private _actorsByAnchor = new Map<number, ActorRec>();
|
|
|
|
|
|
|
|
|
|
getActor(entityId: EntityId): Skill | null {
|
|
|
|
|
const node = this._actorsByEntity.get(entityId)?.node;
|
|
|
|
|
if (!node?.isValid) return null;
|
|
|
|
|
const skill = node.getComponent(Skill);
|
|
|
|
|
return skill?.isValid ? skill : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 按组合锚点取英雄皮肤(不依赖 ECS entityId) */
|
|
|
|
|
getVisualByComposition(composition: ComposedUnit): HeroSkin | null {
|
|
|
|
|
const anchor = this._anchorGridIndex(composition);
|
|
|
|
|
if (anchor < 0) return null;
|
|
|
|
|
const node = this._actorsByAnchor.get(anchor)?.node;
|
|
|
|
|
if (!node?.isValid) return null;
|
|
|
|
|
const skin = node.getComponent(HeroSkin);
|
|
|
|
|
return skin?.isValid ? skin : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 英雄皮是否可见(不含 Skill/武器;对账完成须另查 getActor) */
|
|
|
|
|
isDisplayVisible(composition: ComposedUnit): boolean {
|
|
|
|
|
const skin = this.getVisualByComposition(composition);
|
|
|
|
|
const node = skin?.node;
|
|
|
|
|
const display = skin?.displayNode;
|
|
|
|
|
return !!node?.isValid && node.active && !!display?.isValid && display.active;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 英雄皮是否已盖住该格。
|
|
|
|
|
* 本项目不加载 prefab/hero,格子字形由 TextGrid 显示;无 Spine 体时返回 false。
|
|
|
|
|
* 飞行隐藏不当成未盖住,否则落地会误判未就绪、连打 ensureVisual。
|
|
|
|
|
*/
|
|
|
|
|
isDisplayVisibleAtGrid(composition: ComposedUnit, gridIndex: number): boolean {
|
|
|
|
|
if (!(gridIndex >= 0) || !this._listHasIndex(composition.gridIndices, gridIndex)) return false;
|
|
|
|
|
const rec = this._actorsByAnchor.get(this._anchorGridIndex(composition));
|
|
|
|
|
if (!rec?.node?.isValid || !rec.node.active) return false;
|
|
|
|
|
if (!this._listHasIndex(rec.gridIndices, gridIndex)) return false;
|
|
|
|
|
const skin = rec.node.getComponent(HeroSkin);
|
|
|
|
|
const display = skin?.displayNode;
|
|
|
|
|
if (!display?.isValid) return false;
|
|
|
|
|
if (rec.flightHidden) return true;
|
|
|
|
|
if (!skin?.anim?.isValid) return false;
|
|
|
|
|
const skill = rec.node.getComponent(Skill);
|
|
|
|
|
if (skill?.isValid && skill.isPresentationBusy()) return true;
|
|
|
|
|
return skin.isGlyphBodyVisible();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 列表是否含格号 */
|
|
|
|
|
private _listHasIndex(list: readonly number[], gridIndex: number): boolean {
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
if (list[i] === gridIndex) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 确保英雄展示存在;entityId 可选,晚到时会补挂 Skill */
|
|
|
|
|
ensureVisual(composition: ComposedUnit, map: MapManager, entityId: EntityId | null = null, enemyCamp = false): void {
|
|
|
|
|
const anchor = this._anchorGridIndex(composition);
|
|
|
|
|
if (anchor < 0) return;
|
|
|
|
|
const parent = map.ensureHeroParent();
|
|
|
|
|
const worldPos = this._worldCenter(composition, map);
|
|
|
|
|
if (!parent?.isValid || !worldPos) return;
|
|
|
|
|
const campEnemy = !!enemyCamp;
|
|
|
|
|
|
|
|
|
|
if (entityId != null) {
|
|
|
|
|
const byEntity = this._actorsByEntity.get(entityId);
|
|
|
|
|
if (byEntity?.node?.isValid) {
|
|
|
|
|
if (byEntity.weapon1Id === composition.weapon1Id) {
|
|
|
|
|
byEntity.enemyCamp = campEnemy;
|
|
|
|
|
this._refresh(byEntity, composition, worldPos);
|
|
|
|
|
if (!byEntity.node.getComponent(Skill)?.isValid) {
|
|
|
|
|
this._attachEntity(byEntity, entityId, composition);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._destroyRec(byEntity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const byAnchor = this._actorsByAnchor.get(anchor);
|
|
|
|
|
if (byAnchor?.node?.isValid) {
|
|
|
|
|
if (byAnchor.weapon1Id === composition.weapon1Id) {
|
|
|
|
|
byAnchor.enemyCamp = campEnemy;
|
|
|
|
|
this._refresh(byAnchor, composition, worldPos);
|
|
|
|
|
if (entityId != null) this._attachEntity(byAnchor, entityId, composition);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._destroyRec(byAnchor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._createVisual(composition, parent, worldPos, anchor, entityId, campEnemy);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按地图合成体同步 HeroActor。
|
|
|
|
|
* @param entityId 塔实体
|
|
|
|
|
* @param composition 合成体
|
|
|
|
|
* @param map 地图
|
|
|
|
|
* @param enemyCamp 敌方阵营(皮肤后缀 2)
|
|
|
|
|
*/
|
|
|
|
|
syncFromMap(entityId: EntityId, composition: ComposedUnit, map: MapManager, enemyCamp = false): void {
|
|
|
|
|
this.ensureVisual(composition, map, entityId, enemyCamp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destroy(entityId: EntityId): void {
|
|
|
|
|
const rec = this._actorsByEntity.get(entityId);
|
|
|
|
|
if (!rec) return;
|
|
|
|
|
this._destroyRec(rec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destroyAtGrid(gridIndex: number): void {
|
|
|
|
|
for (const rec of this._actorsByAnchor.values()) {
|
|
|
|
|
if (!rec.gridIndices.includes(gridIndex)) continue;
|
|
|
|
|
this._destroyRec(rec);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 该格是否挂有 HeroActor(含多格组合的任一格) */
|
|
|
|
|
hasActorAtGrid(gridIndex: number): boolean {
|
|
|
|
|
for (const rec of this._actorsByAnchor.values()) {
|
|
|
|
|
if (rec.gridIndices.includes(gridIndex)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 盖住该格的 HeroActor 占地格。无则空数组。
|
|
|
|
|
* @param gridIndex 地图格
|
|
|
|
|
*/
|
|
|
|
|
gridIndicesCovering(gridIndex: number): readonly number[] {
|
|
|
|
|
if (!(gridIndex >= 0)) return [];
|
|
|
|
|
for (const rec of this._actorsByAnchor.values()) {
|
|
|
|
|
if (this._listHasIndex(rec.gridIndices, gridIndex)) return rec.gridIndices;
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 该格当前可见字形节点(displayNode 优先)。
|
|
|
|
|
* 损毁倒下必须按格取,禁止按 composition / weapon1Id 误伤另一座同名英雄。
|
|
|
|
|
* @param gridIndex 地图格
|
|
|
|
|
*/
|
|
|
|
|
getDisplayAtGrid(gridIndex: number): Node | null {
|
|
|
|
|
if (!(gridIndex >= 0)) return null;
|
|
|
|
|
for (const rec of this._actorsByAnchor.values()) {
|
|
|
|
|
if (!this._listHasIndex(rec.gridIndices, gridIndex)) continue;
|
|
|
|
|
const skin = rec.node?.isValid ? rec.node.getComponent(HeroSkin) : null;
|
|
|
|
|
const n = skin?.displayNode ?? rec.node ?? null;
|
|
|
|
|
return n?.isValid ? n : null;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destroyAll(): void {
|
|
|
|
|
const recs = mapValuesToArray(this._actorsByAnchor);
|
|
|
|
|
for (let i = 0; i < recs.length; i++) {
|
|
|
|
|
this._destroyRec(recs[i]);
|
|
|
|
|
}
|
|
|
|
|
this._actorsByEntity.clear();
|
|
|
|
|
this._actorsByAnchor.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _refresh(rec: ActorRec, composition: ComposedUnit, worldPos: Vec3): void {
|
|
|
|
|
rec.gridIndices = composition.gridIndices;
|
|
|
|
|
const newAnchor = this._anchorGridIndex(composition);
|
|
|
|
|
if (newAnchor >= 0 && newAnchor !== rec.anchorGrid) {
|
|
|
|
|
const occupant = this._actorsByAnchor.get(newAnchor);
|
|
|
|
|
if (occupant && occupant !== rec) {
|
|
|
|
|
this._destroyRec(occupant);
|
|
|
|
|
}
|
|
|
|
|
this._actorsByAnchor.delete(rec.anchorGrid);
|
|
|
|
|
rec.anchorGrid = newAnchor;
|
|
|
|
|
this._actorsByAnchor.set(newAnchor, rec);
|
|
|
|
|
}
|
|
|
|
|
if (rec.node?.isValid) {
|
|
|
|
|
rec.node.active = true;
|
|
|
|
|
rec.node.setWorldPosition(worldPos);
|
|
|
|
|
this._applyFlightHide(rec);
|
|
|
|
|
}
|
|
|
|
|
const skin = rec.node.getComponent(HeroSkin);
|
|
|
|
|
if (!skin) return;
|
|
|
|
|
if (Number(skin.Data?.id) !== composition.weapon1Id) {
|
|
|
|
|
const w1 = gg.data.table.getTableData<ITableHero>(TableNames.Hero, composition.weapon1Id);
|
|
|
|
|
if (w1) {
|
|
|
|
|
skin.setData(w1);
|
|
|
|
|
rec.weapon1Id = composition.weapon1Id;
|
|
|
|
|
} else {
|
|
|
|
|
console.log('[HeroActorHost] refresh 缺 Hero 表', composition.weapon1Id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
skin.setSkinByLevelAndQuality(composition.level, rec.enemyCamp);
|
|
|
|
|
skin.ensureShadowDefaultVisible();
|
|
|
|
|
const skill = rec.node.getComponent(Skill);
|
|
|
|
|
if (skill?.isActive) {
|
|
|
|
|
skill.onHeroGlyphSkinRefreshed();
|
|
|
|
|
if (
|
|
|
|
|
rec.entityId != null
|
|
|
|
|
&& skill instanceof HeroActor
|
|
|
|
|
&& !(skill instanceof Gong)
|
|
|
|
|
&& !skill.isPresentationBusy()
|
|
|
|
|
) {
|
|
|
|
|
skill.visualChannel.setCaster(rec.entityId);
|
|
|
|
|
skill.visualChannel.tryShowIdleWeapon();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 拖放飞行期间隐藏该格 HeroActor,避免与飞字叠影。
|
|
|
|
|
* 只改透明度,不关节点:弓 onDisable 会 teardown 且不回待机,落地后空约 1 秒直到下一刀。
|
|
|
|
|
* @param gridIndex 地图格
|
|
|
|
|
* @param visible 是否显示
|
|
|
|
|
*/
|
|
|
|
|
setVisibleAtGrid(gridIndex: number, visible: boolean): void {
|
|
|
|
|
for (const rec of this._actorsByAnchor.values()) {
|
|
|
|
|
if (!rec.gridIndices.includes(gridIndex)) continue;
|
|
|
|
|
rec.flightHidden = !visible;
|
|
|
|
|
this._applyFlightHide(rec);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 该格所属 HeroActor 是否处于飞行透明隐藏。
|
|
|
|
|
* 隐藏期间须露格子字皮,否则拖回背包只剩 ebg 阴影。
|
|
|
|
|
* @param gridIndex 地图格
|
|
|
|
|
*/
|
|
|
|
|
isFlightHiddenAtGrid(gridIndex: number): boolean {
|
|
|
|
|
if (!(gridIndex >= 0)) return false;
|
|
|
|
|
for (const rec of this._actorsByAnchor.values()) {
|
|
|
|
|
if (!this._listHasIndex(rec.gridIndices, gridIndex)) continue;
|
|
|
|
|
return !!rec.flightHidden;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按 flightHidden 设置 heroTemplate 透明度;回池/新建须先清标记再调。
|
|
|
|
|
* @param rec 英雄记录
|
|
|
|
|
*/
|
|
|
|
|
private _applyFlightHide(rec: ActorRec): void {
|
|
|
|
|
if (!rec.node?.isValid) return;
|
|
|
|
|
const uo = rec.node.getComponent(UIOpacity) ?? rec.node.addComponent(UIOpacity);
|
|
|
|
|
uo.opacity = rec.flightHidden ? 0 : 255;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _createVisual(
|
|
|
|
|
composition: ComposedUnit,
|
|
|
|
|
parent: Node,
|
|
|
|
|
worldPos: Vec3,
|
|
|
|
|
anchorGrid: number,
|
|
|
|
|
entityId: EntityId | null,
|
|
|
|
|
enemyCamp = false,
|
|
|
|
|
): void {
|
|
|
|
|
const w1 = gg.data.table.getTableData<ITableHero>(TableNames.Hero, composition.weapon1Id);
|
|
|
|
|
const Ctor = resolveActorCtor(composition.weapon1Id);
|
|
|
|
|
if (!w1 || !Ctor || !parent?.isValid) {
|
|
|
|
|
console.log('[HeroActorHost] 无法创建英雄', composition.weapon1Id, !!w1, !!Ctor, parent?.isValid);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const occupant = this._actorsByAnchor.get(anchorGrid);
|
|
|
|
|
if (occupant) this._destroyRec(occupant);
|
|
|
|
|
|
|
|
|
|
let node: Node | null = null;
|
|
|
|
|
try {
|
|
|
|
|
node = gg.res.getNode(GPath.Prefab, 'heroTemplate', GBundle.Heros);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ClientErrorReporter.report('HeroActorHost_heroTemplatePool', `${composition.weapon1Id}|${String(e)}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!node?.isValid) {
|
|
|
|
|
console.error('[HeroActorHost] heroTemplate 未加载');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
node.parent = parent;
|
|
|
|
|
node.active = true;
|
|
|
|
|
this._resetAngle(node);
|
|
|
|
|
node.setWorldPosition(worldPos);
|
|
|
|
|
node.setSiblingIndex(parent.children.length - 1);
|
|
|
|
|
this._passTouchToMapGrids(node);
|
|
|
|
|
this._stripSkill(node);
|
|
|
|
|
|
|
|
|
|
const heroSkin = node.getComponent(HeroSkin);
|
|
|
|
|
if (!heroSkin) {
|
|
|
|
|
gg.res.putNode(node);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
heroSkin.setData(w1);
|
|
|
|
|
heroSkin.setSkinByLevelAndQuality(composition.level, !!enemyCamp);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ClientErrorReporter.report('HeroActorHost_createVisual', `${composition.weapon1Id}|${String(e)}`);
|
|
|
|
|
if (node?.isValid) gg.res.putNode(node);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rec: ActorRec = {
|
|
|
|
|
node,
|
|
|
|
|
gridIndices: composition.gridIndices,
|
|
|
|
|
anchorGrid,
|
|
|
|
|
weapon1Id: composition.weapon1Id,
|
|
|
|
|
entityId: null,
|
|
|
|
|
enemyCamp: !!enemyCamp,
|
|
|
|
|
flightHidden: false,
|
|
|
|
|
};
|
|
|
|
|
this._applyFlightHide(rec);
|
|
|
|
|
this._actorsByAnchor.set(anchorGrid, rec);
|
|
|
|
|
if (entityId != null) this._attachEntity(rec, entityId, composition);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _attachEntity(rec: ActorRec, entityId: EntityId, composition: ComposedUnit): void {
|
|
|
|
|
if (rec.entityId != null && rec.entityId !== entityId) {
|
|
|
|
|
this._actorsByEntity.delete(rec.entityId);
|
|
|
|
|
}
|
|
|
|
|
rec.entityId = entityId;
|
|
|
|
|
this._actorsByEntity.set(entityId, rec);
|
|
|
|
|
|
|
|
|
|
const node = rec.node;
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
try {
|
|
|
|
|
const Ctor = resolveActorCtor(composition.weapon1Id);
|
|
|
|
|
if (!Ctor) return;
|
|
|
|
|
|
|
|
|
|
const prev = node.getComponent(Skill);
|
|
|
|
|
if (prev?.isValid) {
|
|
|
|
|
if (prev instanceof Ctor) {
|
|
|
|
|
prev.bindEntity(entityId);
|
|
|
|
|
if (prev instanceof HeroActor) {
|
|
|
|
|
prev.setHeroId(composition.weapon1Id);
|
|
|
|
|
}
|
|
|
|
|
prev.onSetActiveAtk(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._teardownSkill(prev);
|
|
|
|
|
node.removeComponent(Skill);
|
|
|
|
|
}
|
|
|
|
|
const skill = node.addComponent(Ctor);
|
|
|
|
|
skill.bindEntity(entityId);
|
|
|
|
|
if (skill instanceof HeroActor) {
|
|
|
|
|
skill.setHeroId(composition.weapon1Id);
|
|
|
|
|
}
|
|
|
|
|
skill.onSetActiveAtk(true);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ClientErrorReporter.report('HeroActorHost_attachEntity', `${composition.weapon1Id}|${String(e)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _destroyRec(rec: ActorRec): void {
|
|
|
|
|
this._recycle(rec.node);
|
|
|
|
|
this._actorsByAnchor.delete(rec.anchorGrid);
|
|
|
|
|
if (rec.entityId != null) this._actorsByEntity.delete(rec.entityId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _recycle(node: Node): void {
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
const uo = node.getComponent(UIOpacity);
|
|
|
|
|
if (uo?.isValid) uo.opacity = 255;
|
|
|
|
|
this._stripSkill(node);
|
|
|
|
|
const skin = node.getComponent(HeroSkin);
|
|
|
|
|
this._resetAngle(skin?.displayNode ?? null);
|
|
|
|
|
this._resetAngle(node);
|
|
|
|
|
skin?.releaseDisplay();
|
|
|
|
|
gg.res.putNode(node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 回池/出池清掉损毁残留角与 ebg 补偿位移。
|
|
|
|
|
* @param node 英雄节点或 displayNode
|
|
|
|
|
*/
|
|
|
|
|
private _resetAngle(node: Node | null): void {
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
const ebg = node.getChildByName('ebg');
|
|
|
|
|
if (ebg?.isValid) {
|
|
|
|
|
this._restoreDecorLocal(ebg, node.angle);
|
|
|
|
|
}
|
|
|
|
|
Tween.stopAllByTarget(node);
|
|
|
|
|
node.angle = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 父节点仍带着损毁角时,把 ebg 局部坐标从补偿值还原回未旋转。
|
|
|
|
|
* @param ebg 阴影节点
|
|
|
|
|
* @param parentAngle 当前父节点角
|
|
|
|
|
*/
|
|
|
|
|
private _restoreDecorLocal(ebg: Node, parentAngle: number): void {
|
|
|
|
|
Tween.stopAllByTarget(ebg);
|
|
|
|
|
if (Math.abs(parentAngle) > 1) {
|
|
|
|
|
const rad = parentAngle * Math.PI / 180;
|
|
|
|
|
const c = Math.cos(rad);
|
|
|
|
|
const s = Math.sin(rad);
|
|
|
|
|
const p = ebg.position;
|
|
|
|
|
ebg.setPosition(p.x * c - p.y * s, p.x * s + p.y * c, p.z);
|
|
|
|
|
}
|
|
|
|
|
ebg.angle = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _stripSkill(node: Node): void {
|
|
|
|
|
const skill = node.getComponent(Skill);
|
|
|
|
|
if (!skill?.isValid) return;
|
|
|
|
|
this._teardownSkill(skill);
|
|
|
|
|
node.removeComponent(Skill);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _teardownSkill(skill: Skill): void {
|
|
|
|
|
skill.beginPresentationTeardown();
|
|
|
|
|
skill.abortPresentation();
|
|
|
|
|
skill.onSetActiveAtk(false);
|
|
|
|
|
skill.detachPooledFx();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** heroTemplate 根节点 279×279,须透传触摸,否则会挡住相邻 MapGrid 的拖放 */
|
|
|
|
|
private _passTouchToMapGrids(node: Node): void {
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
const types = [
|
|
|
|
|
Node.EventType.TOUCH_START,
|
|
|
|
|
Node.EventType.TOUCH_MOVE,
|
|
|
|
|
Node.EventType.TOUCH_END,
|
|
|
|
|
Node.EventType.TOUCH_CANCEL,
|
|
|
|
|
];
|
|
|
|
|
for (const type of types) {
|
|
|
|
|
node.off(type, this._relayTouchToMap, this);
|
|
|
|
|
node.on(type, this._relayTouchToMap, this, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _relayTouchToMap(e: EventTouch): void {
|
|
|
|
|
e.preventSwallow = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _worldCenter(
|
|
|
|
|
composition: ComposedUnit,
|
|
|
|
|
map: { getMapGridByIndex(index: number): { node?: Node | null } | null | undefined },
|
|
|
|
|
): Vec3 | null {
|
|
|
|
|
let sx = 0;
|
|
|
|
|
let sy = 0;
|
|
|
|
|
let sz = 0;
|
|
|
|
|
let n = 0;
|
|
|
|
|
for (const idx of composition.gridIndices) {
|
|
|
|
|
const node = map.getMapGridByIndex(idx)?.node;
|
|
|
|
|
if (!node?.isValid) continue;
|
|
|
|
|
const p = 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 与 DeploySync 一致:锚点取最小 gridIndex,避免 compositions 数组顺序抖动导致双 Actor 叠位。
|
|
|
|
|
* @param composition 合成体
|
|
|
|
|
*/
|
|
|
|
|
private _anchorGridIndex(composition: ComposedUnit): number {
|
|
|
|
|
let anchor = -1;
|
|
|
|
|
for (let i = 0; i < composition.gridIndices.length; i++) {
|
|
|
|
|
const g = composition.gridIndices[i];
|
|
|
|
|
if (anchor < 0 || g < anchor) anchor = g;
|
|
|
|
|
}
|
|
|
|
|
return anchor;
|
|
|
|
|
}
|
|
|
|
|
}
|