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/heros/HeroActorHost.ts

526 lines
19 KiB

3 weeks ago
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;
}
/**
2 weeks ago
* 英雄皮是否已盖住该格。
* 本项目不加载 prefab/hero,格子字形由 TextGrid 显示;无 Spine 体时返回 false。
* 飞行隐藏不当成未盖住,否则落地会误判未就绪、连打 ensureVisual。
3 weeks ago
*/
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;
}
}
1 week ago
/**
* 该格所属 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;
}
3 weeks ago
/**
* 按 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;
}
}