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 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(); private _actorsByAnchor = new Map(); 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(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; } } /** * 按 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(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; } }