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.
675 lines
27 KiB
675 lines
27 KiB
|
3 weeks ago
|
import { Director, director } from 'cc';
|
||
|
|
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
|
||
|
|
import { createMembershipSet, membershipAdd, membershipDelete, type MembershipSet } from '../../../mx/tools/MembershipSet';
|
||
|
|
import { MergeEventType } from 'db://assets/mx/module/merge/events/MergeEventType';
|
||
|
|
import type { MergeEvent, MergeEventSubscription, MergeEventListener } from 'db://assets/mx/module/merge';
|
||
|
|
import type { ComposedUnit } from 'db://assets/mx/module/merge';
|
||
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
||
|
|
import type { BattleRuntime } from './BattleRuntime';
|
||
|
|
import type { MapManager } from '../map/legacy/MapManager';
|
||
|
|
import { getTowerLoadoutService } from '../combat/TowerLoadoutService';
|
||
|
|
import { refreshWangMuFieldAtkSteal } from '../skill/WangMuConfig';
|
||
|
|
import { setupTowerCombat } from '../combat/TowerCombatSetup';
|
||
|
|
import { getTowerExpHost } from '../combat/TowerExpHost';
|
||
|
|
import { HeroActorHost } from '../heros/HeroActorHost';
|
||
|
|
import { isHeroActorWeapon1 } from '../heros/HeroActorRes';
|
||
|
|
import { mapGridToLogicCell } from '../map/BattleMapSpec';
|
||
|
|
import { findCompositionForEntity, findEntityIdByGridIndex, type DeployedTowerInfo } from './DeployEntityQuery';
|
||
|
|
import { TeamComp, TransformComp, GridOccupancyComp } from 'db://assets/mx/module/battle';
|
||
|
|
import type { Player } from '../../game/Player';
|
||
|
|
import type { BattleSession } from 'db://assets/mx/module/battle';
|
||
|
|
import type { TowerLoadoutService } from '../combat/TowerLoadoutService';
|
||
|
|
|
||
|
|
export { composeKey } from './ComposeUnitKey';
|
||
|
|
|
||
|
|
/** 未部署时先逐帧重试的次数,之后改为间隔慢重试 */
|
||
|
|
const UNDEPLOY_FAST_RETRY = 5;
|
||
|
|
/** 慢重试间隔(帧) */
|
||
|
|
const UNDEPLOY_SLOW_GAP = 30;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 合成态 → ECS 部署 reconciler。
|
||
|
|
* 唯一期望态:MergeSession.compositions;ECS 以 PlayerSlot.deployedUnits + GridMap 为实际态。
|
||
|
|
* 不维护平行注册表 / 快照缓存。
|
||
|
|
*/
|
||
|
|
export class DeploySync {
|
||
|
|
private _runtime: BattleRuntime | null = null;
|
||
|
|
private _map: MapManager | null = null;
|
||
|
|
private _bagSession: import('../bag/BattleBagSession').BattleBagSession | null = null;
|
||
|
|
private _playerId = 0;
|
||
|
|
private _mergeListener: ((e: MergeEvent) => void) | null = null;
|
||
|
|
/** false:不订阅 ComposeChanged(AI 由 tick 延后 reconcile,避免与物理同帧叠 Actor) */
|
||
|
|
private _autoReconcileOnCompose = true;
|
||
|
|
/** 未部署合成体的重试次数 */
|
||
|
|
private _undeployRetry = 0;
|
||
|
|
/** 已排队下一帧重试 */
|
||
|
|
private _undeployRetryQueued = false;
|
||
|
|
/** 慢重试剩余等待帧 */
|
||
|
|
private _undeployWait = 0;
|
||
|
|
/** 已打过慢重试日志 */
|
||
|
|
private _undeploySlowLogged = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 绑定运行时与背包。
|
||
|
|
* @param runtime 战斗运行时
|
||
|
|
* @param map 地图
|
||
|
|
* @param bagSession 指定 bag;缺省用 runtime.bagSession
|
||
|
|
* @param playerId 部署归属玩家
|
||
|
|
* @param autoReconcileOnCompose 是否在 ComposeChanged 时即时对账;AI 传 false
|
||
|
|
*/
|
||
|
|
bind(runtime: BattleRuntime, map?: MapManager, bagSession?: import('../bag/BattleBagSession').BattleBagSession | null, playerId = 0, autoReconcileOnCompose = true): void {
|
||
|
|
this.unbind();
|
||
|
|
this._runtime = runtime;
|
||
|
|
this._map = map ?? runtime.Map ?? null;
|
||
|
|
this._playerId = Math.max(0, Math.floor(Number(playerId) || 0));
|
||
|
|
this._bagSession = bagSession ?? runtime.bagSession;
|
||
|
|
this._autoReconcileOnCompose = !!autoReconcileOnCompose;
|
||
|
|
const bag = this._bagSession;
|
||
|
|
if (!bag) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!this._map) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (this._autoReconcileOnCompose) {
|
||
|
|
this._mergeListener = () => {
|
||
|
|
this.reconcileFromMerge('ComposeChanged');
|
||
|
|
};
|
||
|
|
bag.on(MergeEventType.ComposeChanged as unknown as MergeEventSubscription, this._mergeListener as MergeEventListener);
|
||
|
|
}
|
||
|
|
this.reconcileFromMerge('bind');
|
||
|
|
}
|
||
|
|
|
||
|
|
unbind(): void {
|
||
|
|
const bag = this._bagSession;
|
||
|
|
if (bag && this._mergeListener) {
|
||
|
|
bag.off(MergeEventType.ComposeChanged as unknown as MergeEventSubscription, this._mergeListener as MergeEventListener);
|
||
|
|
}
|
||
|
|
this._mergeListener = null;
|
||
|
|
this._runtime = null;
|
||
|
|
this._map = null;
|
||
|
|
this._bagSession = null;
|
||
|
|
this._playerId = 0;
|
||
|
|
this._autoReconcileOnCompose = true;
|
||
|
|
this._undeployRetry = 0;
|
||
|
|
this._undeployRetryQueued = false;
|
||
|
|
this._undeployWait = 0;
|
||
|
|
this._undeploySlowLogged = false;
|
||
|
|
director.off(Director.EVENT_AFTER_UPDATE, this._onUndeployRetry);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @deprecated 使用 reconcileFromMerge */
|
||
|
|
sync(_compositions?: readonly ComposedUnit[], caller = 'unknown'): void {
|
||
|
|
this.reconcileFromMerge(caller);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 以 MergeSession.compositions 为唯一期望态,对账 ECS 部署 */
|
||
|
|
reconcileFromMerge(caller = 'unknown'): void {
|
||
|
|
try {
|
||
|
|
this._reconcileFromMergeImpl(caller);
|
||
|
|
} catch (e) {
|
||
|
|
ClientErrorReporter.report('DeploySync_reconcile', `${caller}|${String(e)}`);
|
||
|
|
this._queueUndeployRetry();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 对账实现 */
|
||
|
|
private _reconcileFromMergeImpl(caller: string): void {
|
||
|
|
if (!this._runtime || !this._map) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!this._map.gridsInitialized) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const bag = this._bagSession;
|
||
|
|
const compositions = bag?.merge.compositions ?? [];
|
||
|
|
const session = this._runtime.session;
|
||
|
|
const player = this._runtime.getPlayer(this._playerId);
|
||
|
|
const keep = createMembershipSet<EntityId>();
|
||
|
|
const loadoutSvc = getTowerLoadoutService();
|
||
|
|
const pid = this._playerId;
|
||
|
|
|
||
|
|
// 按 Weapon1 收集已部署实体,移动时复用(禁止卖了再建导致 CD/大招被清)
|
||
|
|
const poolByWeapon = new Map<number, EntityId[]>();
|
||
|
|
if (player) {
|
||
|
|
for (const eid of player.deployedUnits) {
|
||
|
|
if (!session.world.isAlive(eid)) continue;
|
||
|
|
const w1 = loadoutSvc.getLoadout(eid)?.weapon1Id;
|
||
|
|
if (w1 == null) continue;
|
||
|
|
let list = poolByWeapon.get(w1);
|
||
|
|
if (!list) {
|
||
|
|
list = [];
|
||
|
|
poolByWeapon.set(w1, list);
|
||
|
|
}
|
||
|
|
list.push(eid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 第一遍:本格已有同 weapon1 实体优先绑定(升级/多同名塔不会互相抢走)
|
||
|
|
const pending: ComposedUnit[] = [];
|
||
|
|
for (let c = 0; c < compositions.length; c++) {
|
||
|
|
const comp = compositions[c];
|
||
|
|
const cell = this._cellOf(comp);
|
||
|
|
if (!cell) continue;
|
||
|
|
this._markComposeCellBuildable(comp, cell);
|
||
|
|
const onGrid = this._findEntityOnCompGrids(comp, player, session, loadoutSvc);
|
||
|
|
if (onGrid == null) {
|
||
|
|
pending.push(comp);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let taken = this._takeDeployFromPool(poolByWeapon, comp.weapon1Id, onGrid);
|
||
|
|
if (taken == null) taken = onGrid;
|
||
|
|
if (!this._isAliveDeployEntity(taken, comp.weapon1Id, session, loadoutSvc)) {
|
||
|
|
pending.push(comp);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
this._bindDeployEntity(comp, cell, taken, keep, caller, pid, player, session, loadoutSvc);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 第二遍:剩余合成体从同 weapon1 池复用,再不行新建
|
||
|
|
for (let i = 0; i < pending.length; i++) {
|
||
|
|
const comp = pending[i];
|
||
|
|
const cell = this._cellOf(comp);
|
||
|
|
if (!cell) continue;
|
||
|
|
|
||
|
|
let atCell = session.grid.getCell(cell)?.occupant ?? null;
|
||
|
|
if (atCell != null && !session.world.isAlive(atCell)) {
|
||
|
|
session.grid.clearOccupant(atCell);
|
||
|
|
atCell = null;
|
||
|
|
}
|
||
|
|
const prefer = (atCell != null && loadoutSvc.getLoadout(atCell)?.weapon1Id === comp.weapon1Id)
|
||
|
|
? atCell
|
||
|
|
: null;
|
||
|
|
let entityId = this._takeDeployFromPool(poolByWeapon, comp.weapon1Id, prefer);
|
||
|
|
if (entityId == null && prefer != null) entityId = prefer;
|
||
|
|
if (entityId != null && this._isAliveDeployEntity(entityId, comp.weapon1Id, session, loadoutSvc)) {
|
||
|
|
this._bindDeployEntity(comp, cell, entityId, keep, caller, pid, player, session, loadoutSvc);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (this._bindOccupyingSameHero(comp, cell, keep, caller, pid, player, session, loadoutSvc)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._evictBlockingOccupantsOnComposition(comp, keep, null);
|
||
|
|
this._markComposeCellBuildable(comp, cell);
|
||
|
|
|
||
|
|
const unitId = gg.battle.config.getUnitIdByWeapon1(comp.weapon1Id);
|
||
|
|
const compCells = this._compositionLogicCells(comp);
|
||
|
|
const preCheck = unitId
|
||
|
|
? this._canPlaceCompositionAfterClear(pid, unitId, cell, compCells)
|
||
|
|
: { ok: false as const, reason: 'unknown_unit' as const };
|
||
|
|
if (preCheck.ok === false) {
|
||
|
|
if (!this._undeploySlowLogged) {
|
||
|
|
console.error(
|
||
|
|
'[DeploySync] 合成体无法部署',
|
||
|
|
caller,
|
||
|
|
'weapon1=',
|
||
|
|
comp.weapon1Id,
|
||
|
|
'reason=',
|
||
|
|
preCheck.reason,
|
||
|
|
'grids=',
|
||
|
|
comp.gridIndices.join(','),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const ok = this._runtime.deploy({
|
||
|
|
playerId: pid,
|
||
|
|
weapon1Id: comp.weapon1Id,
|
||
|
|
row: cell.row,
|
||
|
|
col: cell.col,
|
||
|
|
level: comp.level,
|
||
|
|
});
|
||
|
|
if (ok && this._runtime.lastDeployedEntity != null) {
|
||
|
|
const eid = this._runtime.lastDeployedEntity;
|
||
|
|
membershipAdd(keep, eid);
|
||
|
|
this._syncCompositionOccupancy(eid, cell, comp);
|
||
|
|
this._syncHeroActor(eid, comp);
|
||
|
|
this._restoreAndSnapshotHeroBind(eid, comp, player, session);
|
||
|
|
} else if (!this._undeploySlowLogged) {
|
||
|
|
console.error('[DeploySync] deploy 失败', caller, 'weapon1=', comp.weapon1Id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (player) {
|
||
|
|
for (const entityId of Array.from(player.deployedUnits)) {
|
||
|
|
if (keep.has(entityId)) continue;
|
||
|
|
if (!session.world.isAlive(entityId)) continue;
|
||
|
|
const sold = session.deploy.sell(pid, entityId);
|
||
|
|
if (sold) {
|
||
|
|
loadoutSvc.removeEntity(entityId);
|
||
|
|
HeroActorHost.instance.destroy(entityId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
refreshWangMuFieldAtkSteal(session);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._finishUndeployRetry(compositions, player, session, loadoutSvc);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 部署前清掉合成体占地格上的阻挡实体(含 keep 里 weapon1 不符的误绑塔)。
|
||
|
|
* @param comp 合成体
|
||
|
|
* @param keep 本轮已绑定实体
|
||
|
|
* @param exceptEntity 保留占位的实体(搬家/sync 时自身)
|
||
|
|
*/
|
||
|
|
private _evictBlockingOccupantsOnComposition(
|
||
|
|
comp: ComposedUnit,
|
||
|
|
keep: MembershipSet<EntityId>,
|
||
|
|
exceptEntity: EntityId | null,
|
||
|
|
): void {
|
||
|
|
const session = this._runtime?.session;
|
||
|
|
if (!session) return;
|
||
|
|
const loadoutSvc = getTowerLoadoutService();
|
||
|
|
const cells = this._compositionLogicCells(comp);
|
||
|
|
for (let i = 0; i < cells.length; i++) {
|
||
|
|
const cell = cells[i];
|
||
|
|
let occ = session.grid.getCell(cell)?.occupant ?? null;
|
||
|
|
if (occ == null || occ === exceptEntity) continue;
|
||
|
|
if (!session.world.isAlive(occ)) {
|
||
|
|
session.grid.clearOccupant(occ);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const w1 = loadoutSvc.getLoadout(occ)?.weapon1Id;
|
||
|
|
if (keep.has(occ)) {
|
||
|
|
if (w1 === comp.weapon1Id) continue;
|
||
|
|
membershipDelete(keep, occ);
|
||
|
|
}
|
||
|
|
this._reconcileUntrackedOccupant(cell);
|
||
|
|
occ = session.grid.getCell(cell)?.occupant ?? null;
|
||
|
|
if (occ != null && occ !== exceptEntity && session.world.isAlive(occ)) {
|
||
|
|
session.grid.clearOccupant(occ);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 合成体全部逻辑格是否可放置(清格后须覆盖多格神将,不仅 unitDef 单格 shape) */
|
||
|
|
private _canPlaceCompositionAfterClear(
|
||
|
|
playerId: number,
|
||
|
|
unitId: string,
|
||
|
|
anchor: { row: number; col: number },
|
||
|
|
compCells: { row: number; col: number }[],
|
||
|
|
): { ok: true; cells: { row: number; col: number }[] } | { ok: false; reason: string } {
|
||
|
|
const session = this._runtime?.session;
|
||
|
|
if (!session) return { ok: false, reason: 'unknown_unit' };
|
||
|
|
if (compCells.length > 0 && !session.grid.isEmptyForDeploy(compCells)) {
|
||
|
|
return { ok: false, reason: 'cell_occupied' };
|
||
|
|
}
|
||
|
|
const check = session.deploy.canPlace(playerId, unitId, anchor);
|
||
|
|
if (!check.ok) return { ok: false, reason: check.reason ?? 'can_place' };
|
||
|
|
return { ok: true, cells: compCells.length > 0 ? compCells : (check.cells ?? [anchor]) };
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 实体是否存活且 weapon1 与合成体一致 */
|
||
|
|
private _isAliveDeployEntity(
|
||
|
|
entityId: EntityId,
|
||
|
|
weapon1Id: number,
|
||
|
|
session: BattleSession,
|
||
|
|
loadoutSvc: TowerLoadoutService,
|
||
|
|
): boolean {
|
||
|
|
if (!session.world.isAlive(entityId)) return false;
|
||
|
|
return loadoutSvc.getLoadout(entityId)?.weapon1Id === weapon1Id;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 合成体占用的全部 ECS 逻辑格 */
|
||
|
|
private _compositionLogicCells(comp: ComposedUnit): { row: number; col: number }[] {
|
||
|
|
const session = this._runtime?.session;
|
||
|
|
const map = this._map;
|
||
|
|
if (!session || !map) return [];
|
||
|
|
const cells: { row: number; col: number }[] = [];
|
||
|
|
for (let i = 0; i < comp.gridIndices.length; i++) {
|
||
|
|
const mg = map.getMapGridByIndex(comp.gridIndices[i]);
|
||
|
|
if (!mg) continue;
|
||
|
|
const c = mapGridToLogicCell(mg.Row, mg.Col, session.grid.rows, session.grid.cols);
|
||
|
|
if (c) cells.push(c);
|
||
|
|
}
|
||
|
|
return cells;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 合成体仍无塔实体时继续对账。快重试之后改为慢重试,避免占格晚放开后永久不攻击。
|
||
|
|
* @param compositions 当前合成态
|
||
|
|
* @param player 玩家槽
|
||
|
|
* @param session 战斗会话
|
||
|
|
* @param loadoutSvc 配装
|
||
|
|
*/
|
||
|
|
private _finishUndeployRetry(
|
||
|
|
compositions: readonly ComposedUnit[],
|
||
|
|
player: Player | null | undefined,
|
||
|
|
session: BattleSession,
|
||
|
|
loadoutSvc: TowerLoadoutService,
|
||
|
|
): void {
|
||
|
|
let missing = false;
|
||
|
|
for (let i = 0; i < compositions.length; i++) {
|
||
|
|
const comp = compositions[i];
|
||
|
|
if (this._cellOf(comp) == null) continue;
|
||
|
|
if (this._findEntityOnCompGrids(comp, player, session, loadoutSvc) != null) continue;
|
||
|
|
missing = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (!missing) {
|
||
|
|
this._undeployRetry = 0;
|
||
|
|
this._undeployWait = 0;
|
||
|
|
this._undeploySlowLogged = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._queueUndeployRetry();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 排队重试对账;超过快重试次数后隔帧再试,不放弃 */
|
||
|
|
private _queueUndeployRetry(): void {
|
||
|
|
if (this._undeployRetryQueued) return;
|
||
|
|
this._undeployRetry++;
|
||
|
|
if (this._undeployRetry > UNDEPLOY_FAST_RETRY) {
|
||
|
|
this._undeployWait = UNDEPLOY_SLOW_GAP;
|
||
|
|
if (!this._undeploySlowLogged) {
|
||
|
|
this._undeploySlowLogged = true;
|
||
|
|
console.error('[DeploySync] 合成体未部署,改为慢重试');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this._undeployRetryQueued = true;
|
||
|
|
director.once(Director.EVENT_AFTER_UPDATE, this._onUndeployRetry);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 下一帧重试对账;慢重试先空等剩余帧 */
|
||
|
|
private _onUndeployRetry = (): void => {
|
||
|
|
this._undeployRetryQueued = false;
|
||
|
|
if (!this._runtime) return;
|
||
|
|
if (this._undeployWait > 0) {
|
||
|
|
this._undeployWait--;
|
||
|
|
this._undeployRetryQueued = true;
|
||
|
|
director.once(Director.EVENT_AFTER_UPDATE, this._onUndeployRetry);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.reconcileFromMerge('retry');
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 脚印上已有同名塔、且不属于另一座存活组合时搬家复用。
|
||
|
|
* 不拆本轮已绑到别的组合上的塔,避免两座英雄来回抢实体。
|
||
|
|
* @param comp 合成体
|
||
|
|
* @param cell 锚点逻辑格
|
||
|
|
* @param keep 本轮保留集
|
||
|
|
* @param caller 调用来源
|
||
|
|
* @param pid 玩家
|
||
|
|
* @param player 玩家槽
|
||
|
|
* @param session 会话
|
||
|
|
* @param loadoutSvc 配装
|
||
|
|
* @returns 是否已复用
|
||
|
|
*/
|
||
|
|
private _bindOccupyingSameHero(comp: ComposedUnit, cell: { row: number; col: number }, keep: MembershipSet<EntityId>, caller: string, pid: number, player: Player | null | undefined, session: BattleSession, loadoutSvc: TowerLoadoutService): boolean {
|
||
|
|
const cells = this._compositionLogicCells(comp);
|
||
|
|
for (let i = 0; i < cells.length; i++) {
|
||
|
|
const occ = session.grid.getCell(cells[i])?.occupant ?? null;
|
||
|
|
if (occ == null || !this._isAliveDeployEntity(occ, comp.weapon1Id, session, loadoutSvc)) continue;
|
||
|
|
if (player && this._map) {
|
||
|
|
const owner = findCompositionForEntity(occ, player, session, this._map);
|
||
|
|
if (owner && !this._sameComposeGrids(owner, comp)) continue;
|
||
|
|
}
|
||
|
|
this._bindDeployEntity(comp, cell, occ, keep, caller, pid, player, session, loadoutSvc);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 两套合成体是否同一组地图格。
|
||
|
|
* @param a 合成体
|
||
|
|
* @param b 合成体
|
||
|
|
*/
|
||
|
|
private _sameComposeGrids(a: ComposedUnit, b: ComposedUnit): boolean {
|
||
|
|
if (a.gridIndices.length !== b.gridIndices.length) return false;
|
||
|
|
for (let i = 0; i < a.gridIndices.length; i++) {
|
||
|
|
if (b.gridIndices.indexOf(a.gridIndices[i]) < 0) return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 从同 weapon1 池取出实体;prefer 命中则优先。
|
||
|
|
* @param poolByWeapon 池
|
||
|
|
* @param weapon1Id Hero id
|
||
|
|
* @param prefer 优先实体
|
||
|
|
*/
|
||
|
|
private _takeDeployFromPool(poolByWeapon: Map<number, EntityId[]>, weapon1Id: number, prefer: EntityId | null): EntityId | null {
|
||
|
|
const list = poolByWeapon.get(weapon1Id);
|
||
|
|
if (!list?.length) return null;
|
||
|
|
if (prefer != null) {
|
||
|
|
const i = list.indexOf(prefer);
|
||
|
|
if (i >= 0) {
|
||
|
|
list.splice(i, 1);
|
||
|
|
return prefer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return list.shift() ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 合成体占用格上已有同 weapon1 的部署实体。
|
||
|
|
* @param comp 合成体
|
||
|
|
* @param player 玩家槽
|
||
|
|
* @param session 战斗会话
|
||
|
|
* @param loadoutSvc 配装
|
||
|
|
*/
|
||
|
|
private _findEntityOnCompGrids(
|
||
|
|
comp: ComposedUnit,
|
||
|
|
player: Player | null | undefined,
|
||
|
|
session: BattleSession,
|
||
|
|
loadoutSvc: TowerLoadoutService,
|
||
|
|
): EntityId | null {
|
||
|
|
if (!player || !this._map) return null;
|
||
|
|
for (let i = 0; i < comp.gridIndices.length; i++) {
|
||
|
|
const eid = findEntityIdByGridIndex(comp.gridIndices[i], session, this._map, player);
|
||
|
|
if (eid == null || !session.world.isAlive(eid)) continue;
|
||
|
|
if (loadoutSvc.getLoadout(eid)?.weapon1Id !== comp.weapon1Id) continue;
|
||
|
|
return eid;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 将已有塔实体绑定到合成体锚点(搬家/升等级,不新建)。
|
||
|
|
* @param comp 合成体
|
||
|
|
* @param cell 逻辑格
|
||
|
|
* @param entityId 实体
|
||
|
|
* @param keep 本轮保留集
|
||
|
|
* @param caller 调用来源
|
||
|
|
* @param pid 玩家
|
||
|
|
* @param player 玩家槽
|
||
|
|
* @param session 会话
|
||
|
|
* @param loadoutSvc 配装
|
||
|
|
*/
|
||
|
|
private _bindDeployEntity(
|
||
|
|
comp: ComposedUnit,
|
||
|
|
cell: { row: number; col: number },
|
||
|
|
entityId: EntityId,
|
||
|
|
keep: MembershipSet<EntityId>,
|
||
|
|
caller: string,
|
||
|
|
pid: number,
|
||
|
|
player: Player | null | undefined,
|
||
|
|
session: BattleSession,
|
||
|
|
loadoutSvc: TowerLoadoutService,
|
||
|
|
): void {
|
||
|
|
let atCell = session.grid.getCell(cell)?.occupant ?? null;
|
||
|
|
if (atCell != null && !session.world.isAlive(atCell)) {
|
||
|
|
session.grid.clearOccupant(atCell);
|
||
|
|
atCell = null;
|
||
|
|
}
|
||
|
|
this._evictBlockingOccupantsOnComposition(comp, keep, entityId);
|
||
|
|
if (atCell != null && atCell !== entityId) {
|
||
|
|
session.grid.clearOccupant(atCell);
|
||
|
|
}
|
||
|
|
this._relocateTower(entityId, cell, comp);
|
||
|
|
membershipAdd(keep, entityId);
|
||
|
|
this._syncHeroActor(entityId, comp);
|
||
|
|
const loadout = loadoutSvc.getLoadout(entityId);
|
||
|
|
if (loadout && loadout.level !== comp.level && player) {
|
||
|
|
setupTowerCombat(session, entityId, comp.weapon1Id, comp.level, player);
|
||
|
|
}
|
||
|
|
this._restoreAndSnapshotHeroBind(entityId, comp, player, session);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 同 Weapon1 塔改锚点:只搬家,不销毁实体(保留 CD / 进行中技能) */
|
||
|
|
private _relocateTower(
|
||
|
|
entityId: EntityId,
|
||
|
|
anchor: { row: number; col: number },
|
||
|
|
comp: ComposedUnit,
|
||
|
|
): void {
|
||
|
|
const session = this._runtime?.session;
|
||
|
|
if (!session) return;
|
||
|
|
|
||
|
|
const tf = session.world.getComponent(entityId, TransformComp);
|
||
|
|
if (tf) {
|
||
|
|
tf.row = anchor.row;
|
||
|
|
tf.col = anchor.col;
|
||
|
|
}
|
||
|
|
this._syncCompositionOccupancy(entityId, anchor, comp);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 新方案:塔实体对账完成后立刻建/刷新 heroTemplate(格子皮已藏,不能等晚一帧 SkillController) */
|
||
|
|
private _syncHeroActor(entityId: EntityId, comp: ComposedUnit): void {
|
||
|
|
if (!isHeroActorWeapon1(comp.weapon1Id) || !this._map) return;
|
||
|
|
HeroActorHost.instance.syncFromMap(entityId, comp, this._map, this._playerId !== 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 重组后按字绑定恢复经验/Buff,再把当前状态写回所有字。
|
||
|
|
* @param entityId 塔实体
|
||
|
|
* @param comp 组合
|
||
|
|
* @param player 玩家
|
||
|
|
* @param session 战斗会话
|
||
|
|
*/
|
||
|
|
private _restoreAndSnapshotHeroBind(
|
||
|
|
entityId: EntityId,
|
||
|
|
comp: ComposedUnit,
|
||
|
|
player: Player | null | undefined,
|
||
|
|
session: BattleSession,
|
||
|
|
): void {
|
||
|
|
if (!player || !this._map) return;
|
||
|
|
const merge = player.bagSession?.merge;
|
||
|
|
if (!merge) return;
|
||
|
|
const bind = merge.readCompositionHeroBind(comp);
|
||
|
|
if (bind) {
|
||
|
|
getTowerExpHost().restoreBindToEntity(session, entityId, bind);
|
||
|
|
}
|
||
|
|
getTowerExpHost().syncBindToGlyphs(session, player, this._map, entityId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 合成多格占地:同步 GridMap.occupy 与 GridOccupancyComp.cells */
|
||
|
|
private _syncCompositionOccupancy(
|
||
|
|
entityId: EntityId,
|
||
|
|
anchor: { row: number; col: number },
|
||
|
|
comp: ComposedUnit,
|
||
|
|
): void {
|
||
|
|
const session = this._runtime?.session;
|
||
|
|
const map = this._map;
|
||
|
|
if (!session || !map) return;
|
||
|
|
|
||
|
|
const cells: { row: number; col: number }[] = [];
|
||
|
|
for (const gridIndex of comp.gridIndices) {
|
||
|
|
const mg = map.getMapGridByIndex(gridIndex);
|
||
|
|
if (!mg) continue;
|
||
|
|
const c = mapGridToLogicCell(mg.Row, mg.Col, session.grid.rows, session.grid.cols);
|
||
|
|
if (c) cells.push(c);
|
||
|
|
}
|
||
|
|
if (cells.length === 0) cells.push(anchor);
|
||
|
|
|
||
|
|
for (let i = 0; i < cells.length; i++) {
|
||
|
|
const c = cells[i];
|
||
|
|
const occ = session.grid.getCell(c)?.occupant ?? null;
|
||
|
|
if (occ == null || occ === entityId) continue;
|
||
|
|
if (!session.world.isAlive(occ)) {
|
||
|
|
session.grid.clearOccupant(occ);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const team = session.world.getComponent(occ, TeamComp);
|
||
|
|
if (team?.ownerId === this._playerId && session.deploy.sell(this._playerId, occ)) {
|
||
|
|
getTowerLoadoutService().removeEntity(occ);
|
||
|
|
HeroActorHost.instance.destroy(occ);
|
||
|
|
} else {
|
||
|
|
session.grid.clearOccupant(occ);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
session.grid.clearOccupant(entityId);
|
||
|
|
session.grid.occupy(cells, entityId);
|
||
|
|
|
||
|
|
const occ = session.world.getComponent(entityId, GridOccupancyComp);
|
||
|
|
if (occ) {
|
||
|
|
occ.anchor = { ...anchor };
|
||
|
|
occ.cells = cells.map(c => ({ ...c }));
|
||
|
|
}
|
||
|
|
|
||
|
|
session.spatial.updateEntity(entityId, anchor);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @deprecated 查询请用 DeployEntityQuery + Player.compositions */
|
||
|
|
getDeployed(_entityId: number): DeployedTowerInfo | null {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 当前全部已部署实体(PlayerSlot.deployedUnits) */
|
||
|
|
getDeployedEntityIds(): readonly number[] {
|
||
|
|
return this._runtime?.getPlayer(0)?.deployedUnits ?? [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @deprecated 查询请用 DeployEntityQuery.findEntityIdByGridIndex */
|
||
|
|
getEntityIdByGridIndex(_gridIndex: number): number | null {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @deprecated 战斗结束由 session 销毁,无需清缓存 */
|
||
|
|
clear(): void {}
|
||
|
|
|
||
|
|
private _cellOf(comp: ComposedUnit): { row: number; col: number } | null {
|
||
|
|
if (!this._map || !this._runtime) return null;
|
||
|
|
// 锚点取最小 gridIndex,避免 compositions 数组顺序抖动导致来回改位
|
||
|
|
let gridIndex = -1;
|
||
|
|
for (let i = 0; i < comp.gridIndices.length; i++) {
|
||
|
|
const g = comp.gridIndices[i];
|
||
|
|
if (gridIndex < 0 || g < gridIndex) gridIndex = g;
|
||
|
|
}
|
||
|
|
if (gridIndex < 0) return null;
|
||
|
|
const g = this._map.getMapGridByIndex(gridIndex);
|
||
|
|
if (!g) return null;
|
||
|
|
return mapGridToLogicCell(g.Row, g.Col, this._runtime.session.grid.rows, this._runtime.session.grid.cols);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _markComposeCellBuildable(comp: ComposedUnit, cell: { row: number; col: number }): void {
|
||
|
|
const session = this._runtime?.session;
|
||
|
|
if (!session || !this._map) return;
|
||
|
|
session.grid.setBuildable(cell, true);
|
||
|
|
for (const gridIndex of comp.gridIndices) {
|
||
|
|
const mg = this._map.getMapGridByIndex(gridIndex);
|
||
|
|
if (!mg) continue;
|
||
|
|
const c = mapGridToLogicCell(mg.Row, mg.Col, session.grid.rows, session.grid.cols);
|
||
|
|
if (c) session.grid.setBuildable(c, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _reconcileUntrackedOccupant(cell: { row: number; col: number }): void {
|
||
|
|
const session = this._runtime?.session;
|
||
|
|
if (!session) return;
|
||
|
|
const state = session.grid.getCell(cell);
|
||
|
|
const occ = state?.occupant;
|
||
|
|
if (occ == null) return;
|
||
|
|
|
||
|
|
if (!session.world.isAlive(occ)) {
|
||
|
|
session.grid.clearOccupant(occ);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const team = session.world.getComponent(occ, TeamComp);
|
||
|
|
if (team?.ownerId !== this._playerId) return;
|
||
|
|
if (session.deploy.sell(this._playerId, occ)) {
|
||
|
|
getTowerLoadoutService().removeEntity(occ);
|
||
|
|
HeroActorHost.instance.destroy(occ);
|
||
|
|
refreshWangMuFieldAtkSteal(session);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export type { DeployedTowerInfo } from './DeployEntityQuery';
|