refactor(map/hero): 重构英雄位置管理逻辑,移除lane相关字段

重构了英雄分路排位的旧实现,改用硬编码的点位数组管理英雄站位,移除了HeroAttrsComp中的lane和lane_index字段,简化了英雄位置分配、UI面板绑定的逻辑,提升代码可维护性。
This commit is contained in:
walkpan
2026-05-13 23:48:58 +08:00
parent e3a9d447ba
commit 3f47df2682
5 changed files with 105 additions and 265 deletions

View File

@@ -64,8 +64,6 @@ export class HeroAttrsComp extends ecs.Comp {
minSkillDistance: number = 0; // 最近技能攻击距离缓存不受MP影响用于停止位置判断
// ==================== 阵型位置 ====================
lane: number = -1; // 所在分路0上路, 1中路, 2下路
lane_index: number = -1; // 所在路中的排位0前排, 1后排
// ==================== 标记状态 ====================
is_dead: boolean = false;
@@ -312,8 +310,6 @@ export class HeroAttrsComp extends ecs.Comp {
this.maxSkillDistance = 0;
this.minSkillDistance = 0;
this.lane = -1;
this.lane_index = -1;
this.is_dead = false;
this.is_count_dead = false;

View File

@@ -46,6 +46,8 @@ interface MoveFacConfig {
retreatBackX: number;
}
import { MissionHeroCompComp } from "../map/MissionHeroComp";
@ecs.register('MoveSystem')
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
private readonly heroFrontAnchorX = -200;
@@ -249,20 +251,12 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const slotIndex = Math.max(0, allAllies.findIndex(entity => entity === self));
const lanePriority = [1, 0, 2]; // 中路优先,其次上路,最后下路
const laneOffsets = [100, 0, -100];
// 优先中前(1) -> 上前(0) -> 下前(2) -> 中后(4) -> 上后(3) -> 下后(5)
const slotPriority = [1, 0, 2, 4, 3, 5];
const posIndex = slotPriority[slotIndex % 6];
const pos = MissionHeroCompComp.HERO_POSITIONS[posIndex];
const col = Math.floor(slotIndex / 3);
const laneIdx = lanePriority[slotIndex % 3];
// 动态更新英雄位置数据,为战斗面板排序提供依据
model.lane = laneIdx;
model.lane_index = col;
const targetY = BoxSet.GAME_LINE + laneOffsets[laneIdx];
const targetX = this.heroFrontAnchorX - col * this.heroAllySpacingX;
return { targetX, targetY };
return { targetX: pos.x, targetY: pos.y };
}
private moveToSlot(view: HeroViewComp, move: MoveComp, model: HeroAttrsComp, targetX: number) {

View File

@@ -32,6 +32,9 @@ import { GameEvent } from "../common/config/GameEvent";
import { oops } from "db://oops-framework/core/Oops";
import { UIID } from "../common/config/GameUIConfig";
import { mLogger } from "../common/Logger";
import { MissionHeroCompComp } from "./MissionHeroComp";
import { MoveComp } from "../hero/MoveComp";
import { FacSet } from "../common/config/GameSet";
const {property, ccclass } = _decorator;
@@ -98,6 +101,44 @@ export class HInfoComp extends CCComp {
this.refresh();
}
/**
* 根据 node_index 获取对应的硬编码位置,并查找该位置的英雄
*/
refreshByNodeIndex() {
if (this.node_index < 1 || this.node_index > 6) return;
const targetPos = MissionHeroCompComp.HERO_POSITIONS[this.node_index - 1];
let foundModel: HeroAttrsComp | null = null;
let foundEid: number = 0;
// 遍历所有英雄,查找 targetX 和 targetY 匹配该位置的英雄
ecs.query(ecs.allOf(HeroAttrsComp, MoveComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
const move = entity.get(MoveComp);
if (model && move && !model.is_dead && model.fac === FacSet.HERO) {
if (Math.abs(move.targetX - targetPos.x) < 2 && Math.abs(move.baseY - targetPos.y) < 2) {
foundModel = model;
foundEid = entity.eid;
}
}
});
if (foundModel) {
if (this.eid !== foundEid) {
this.bindData(foundEid, foundModel);
if (!this.node.active) this.node.active = true;
} else {
this.refresh();
}
} else {
if (this.eid !== 0) {
this.eid = 0;
this.model = null;
if (this.node.active) this.node.active = false;
}
}
}
/**
* 设置当前是否处于战斗阶段,控制出售按钮显示/隐藏
* @param isBattlePhase 是否处于战斗阶段

View File

@@ -141,15 +141,6 @@ export class MissionCardComp extends CCComp {
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
/** 卡牌原始定位点 */
private cardsPos = [-260,-75,108,260]
/**
* 英雄信息面板映射EID → { node, model, comp }
* 用于追踪每个出战英雄的面板实例和数据引用
*/
private heroInfoItems: Map<number, {
node: Node,
model: HeroAttrsComp,
comp: HInfoComp
}> = new Map();
/** 缓存预先放置的 6 个 HInfoComp */
private cachedHInfoComps: Map<number, HInfoComp> = new Map();
// ======================== 生命周期 ========================
@@ -226,11 +217,6 @@ export class MissionCardComp extends CCComp {
missionData.hero_extend_max_num = FightSet.HERO_MAX_NUM + 1;
}
// 确保 Map 被正确初始化
if (!this.heroInfoItems) {
this.heroInfoItems = new Map();
}
// 确保卡牌组件列表已被正确缓存
if (!this.cardComps || this.cardComps.length === 0) {
this.cacheCardComps();
@@ -276,7 +262,13 @@ export class MissionCardComp extends CCComp {
this.heroInfoSyncTimer += dt;
if (this.heroInfoSyncTimer < 0.15) return;
this.heroInfoSyncTimer = 0;
this.refreshHeroInfoPanels();
// 遍历所有预设的 HInfoComp让其根据 node_index 自己刷新
this.cachedHInfoComps.forEach(comp => {
if (comp && comp.isValid) {
comp.refreshByNodeIndex();
}
});
}
@@ -422,14 +414,12 @@ export class MissionCardComp extends CCComp {
if (!eid || !model) return;
const before = this.getAliveHeroCount();
this.ensureHeroInfoPanel(eid, model);
const after = this.getAliveHeroCount();
this.updateHeroNumUI(true, after > before);
}
/** 英雄死亡事件回调:刷新面板列表并更新英雄数量 UI */
private onHeroDead() {
this.refreshHeroInfoPanels();
this.updateHeroNumUI(true, false);
}
@@ -663,9 +653,9 @@ export class MissionCardComp extends CCComp {
Tween.stopAllByTarget(this.cards_node);
this.cards_node.setScale(this.cardsShowScale);
this.heroInfoItems.forEach(item => {
if (item.comp && item.comp.isValid) {
item.comp.setBattlePhase(false);
this.cachedHInfoComps.forEach(comp => {
if (comp && comp.isValid) {
comp.setBattlePhase(false);
}
});
}
@@ -683,9 +673,9 @@ export class MissionCardComp extends CCComp {
})
.start();
this.heroInfoItems.forEach(item => {
if (item.comp && item.comp.isValid) {
item.comp.setBattlePhase(true);
this.cachedHInfoComps.forEach(comp => {
if (comp && comp.isValid) {
comp.setBattlePhase(true);
}
});
}
@@ -895,132 +885,7 @@ export class MissionCardComp extends CCComp {
return Math.floor(cost);
}
private ensureHeroInfoPanel(eid: number, model: HeroAttrsComp) {
if (!this.hero_info_node) {
mLogger.error(this.debugMode, "MissionCardComp", "ensureHeroInfoPanel: missing hero_info_node");
return;
}
// MoveComp.ts 里的 assignment:
// lanePriority = [1, 0, 2]; // slotIndex 0->中路(1), 1->上路(0), 2->下路(2)
// laneIdx = lanePriority[slotIndex % 3]; // priority: 1(中), 0(上), 2(下)
// model.lane = laneIdx;
//
// 所以当:
// model.lane = 0 (上路), model.lane_index = 0 -> 对应 node_index = 1
// model.lane = 1 (中路), model.lane_index = 0 -> 对应 node_index = 2
// model.lane = 2 (下路), model.lane_index = 0 -> 对应 node_index = 3
const expectedNodeIndex = model.lane_index * 3 + model.lane + 1;
mLogger.log(this.debugMode, "MissionCardComp", `ensureHeroInfoPanel calculation: lane=${model.lane}, lane_index=${model.lane_index} -> expectedNodeIndex=${expectedNodeIndex}`);
const comp = this.cachedHInfoComps.get(expectedNodeIndex);
if (!comp) {
mLogger.error(this.debugMode, "MissionCardComp", `ensureHeroInfoPanel: missing pre-placed HInfoComp for index ${expectedNodeIndex}`);
return;
}
const current = this.heroInfoItems.get(eid);
if (current) {
current.model = model;
current.comp = comp;
current.node = comp.node;
comp.node.active = true;
comp.bindData(eid, model);
this.updateHeroInfoPanel(current);
return;
}
comp.node.active = true;
const item = {
node: comp.node,
model,
comp
};
comp.bindData(eid, model);
comp.setBattlePhase(this.isBattlePhase);
this.heroInfoItems.set(eid, item);
this.updateHeroInfoPanel(item);
mLogger.log(this.debugMode, "MissionCardComp", `ensureHeroInfoPanel: updated panel for eid ${eid} at node_index ${expectedNodeIndex}`);
}
private refreshHeroInfoPanels() {
const removeKeys: number[] = [];
// 1. 先将已死亡的英雄移除,释放占用的节点
this.heroInfoItems.forEach((item, eid) => {
if (!item.node || !item.node.isValid) {
removeKeys.push(eid);
return;
}
// 使用 model.is_dead 增加判断条件,更加准确
if (!item.comp.isModelAlive() || item.model.is_dead) {
if (item.node.isValid) item.node.active = false;
removeKeys.push(eid);
return;
}
});
for (let i = 0; i < removeKeys.length; i++) {
this.heroInfoItems.delete(removeKeys[i]);
}
// 2. 然后再处理所有存活英雄的位置转移和信息刷新
// 如果有多个英雄在同一帧发生位置变动,我们需要统一处理
const needTransfer: Array<{eid: number, expectedNodeIndex: number}> = [];
this.heroInfoItems.forEach((item, eid) => {
// 检查英雄是否改变了位置 (lane 或 lane_index 发生了变化)
const expectedNodeIndex = item.model.lane_index * 3 + item.model.lane + 1;
if (item.comp.node_index !== expectedNodeIndex) {
// 如果位置变了,需要转移到新的节点上
const newComp = this.cachedHInfoComps.get(expectedNodeIndex);
if (newComp) {
needTransfer.push({eid, expectedNodeIndex});
// 将原来的节点释放,以供其他可能换到这个位置的英雄使用
item.node.active = false;
}
} else {
this.updateHeroInfoPanel(item);
}
});
// 执行位置转移
for (const transfer of needTransfer) {
const item = this.heroInfoItems.get(transfer.eid);
if (!item) continue;
const newComp = this.cachedHInfoComps.get(transfer.expectedNodeIndex);
if (newComp) {
// 转移到新节点
item.comp = newComp;
item.node = newComp.node;
item.node.active = true;
item.comp.bindData(transfer.eid, item.model);
item.comp.setBattlePhase(this.isBattlePhase);
this.updateHeroInfoPanel(item);
}
}
this.updateHeroNumUI(false, false);
}
private updateHeroInfoPanel(item: {
node: Node,
model: HeroAttrsComp,
comp: HInfoComp
}) {
item.comp.refresh();
item.comp.setBattlePhase(this.isBattlePhase);
}
private clearHeroInfoPanels() {
if (this.heroInfoItems) {
this.heroInfoItems.clear();
}
if (this.cachedHInfoComps) {
this.cachedHInfoComps.forEach(comp => {
if (comp && comp.node && comp.node.isValid) {
@@ -1028,13 +893,6 @@ export class MissionCardComp extends CCComp {
}
});
}
// 不再销毁子节点,因为它们是预先放置的
// if (this.hero_info_node && this.hero_info_node.isValid) {
// for (let i = this.hero_info_node.children.length - 1; i >= 0; i--) {
// const child = this.hero_info_node.children[i];
// if (child && child.isValid) child.destroy();
// }
// }
this.heroInfoSyncTimer = 0;
this.syncMissionHeroData(0);
this.updateHeroNumUI(false, false);
@@ -1066,11 +924,11 @@ export class MissionCardComp extends CCComp {
private getAliveHeroCount(): number {
let count = 0;
this.heroInfoItems.forEach(item => {
if (!item?.node || !item.node.isValid) return;
if (!item.comp?.isModelAlive()) return;
if (item.model?.is_dead) return;
count += 1;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (model && model.fac === FacSet.HERO && !model.is_dead) {
count++;
}
});
return count;
}
@@ -1218,7 +1076,6 @@ export class MissionCardComp extends CCComp {
// this.resetButtonScale(this.cards_up);
// 关键:在 reset/销毁 时将 Map 置空,彻底切断引用
this.heroInfoItems = null as any;
this.cardComps = [] as any;
if (this.node && this.node.isValid) {

View File

@@ -39,6 +39,7 @@ import { FacSet, FightSet, BoxSet } from "../common/config/GameSet";
import { oneCom } from "../skill/oncend";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FieldSkillSet, FieldSkillType } from "../common/config/SkillSet";
import { MoveComp } from "../hero/MoveComp";
const { ccclass } = _decorator;
/**
@@ -52,18 +53,18 @@ const { ccclass } = _decorator;
export class MissionHeroCompComp extends CCComp {
// ======================== 常量 ========================
/** 硬编码的6个英雄占位点 */
public static readonly HERO_POSITIONS: Vec3[] = [
v3(-200, BoxSet.GAME_LINE + 100, 0), // index 0 (node_index 1): Top Front
v3(-200, BoxSet.GAME_LINE, 0), // index 1 (node_index 2): Mid Front
v3(-200, BoxSet.GAME_LINE - 100, 0), // index 2 (node_index 3): Bot Front
v3(-300, BoxSet.GAME_LINE + 100, 0), // index 3 (node_index 4): Top Back
v3(-300, BoxSet.GAME_LINE, 0), // index 4 (node_index 5): Mid Back
v3(-300, BoxSet.GAME_LINE - 100, 0), // index 5 (node_index 6): Bot Back
];
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
private static readonly HERO_DROP_HEIGHT = 260
/** 近战英雄起始出生 X 坐标 */
private static readonly HERO_SPAWN_START_MELEE_X = -320
/** 远程(含中程)英雄起始出生 X 坐标 */
private static readonly HERO_SPAWN_START_RANGED_X = -320
/** 三路高度偏移(上路, 中路, 下路) */
private static readonly HERO_LANE_Y_OFFSETS = [ BoxSet.GAME_LINE+90, BoxSet.GAME_LINE, BoxSet.GAME_LINE-90]
/** 每路前排容量 */
private static readonly HERO_LANE_CAP = 2
/** 同路内 X 间距 */
private static readonly HERO_GAP_X = 100
// ======================== 运行时属性 ========================
@@ -131,10 +132,7 @@ export class MissionHeroCompComp extends CCComp {
if (model && view) {
if (model.is_dead) {
view.alive();
const { lane, indexInLane } = this.pickLaneForHero(model.hero_uuid, [hero.eid]);
model.lane = lane;
model.lane_index = indexInLane;
const landingPos = this.resolveHeroLandingPos(model.hero_uuid, lane, indexInLane);
const landingPos = this.pickPositionForHero([hero.eid]);
// 不再直接设置位置,而是播放下落入场动画
// 计算出出生点(空中)
const spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
@@ -170,43 +168,39 @@ export class MissionHeroCompComp extends CCComp {
// ======================== 英雄生成 ========================
/**
* 动态分配英雄上场的路和排位(优先中路 -> 上路 -> 下路)
* 标记英雄的6个登录点
* @param uuid 英雄 UUID
* 动态分配英雄上场的位置
* @param excludeEids 排除计算的实体ID数组避免复活或合成时把自己算成占据的位置
*/
private pickLaneForHero(uuid: number, excludeEids: number[] = []): { lane: number; indexInLane: number } {
private pickPositionForHero(excludeEids: number[] = []): Vec3 {
const heroes = this.getAllHeroes().filter(h => {
const m = h.get(HeroAttrsComp);
return m && !m.is_dead && !excludeEids.includes(h.eid);
});
// 记录6个位置点的占用情况 [lane][indexInLane]
const occupied = [
[false, false], // 上路 0
[false, false], // 中路 1
[false, false] // 下路 2
];
const occupied = new Set<number>();
for (const h of heroes) {
const m = h.get(HeroAttrsComp);
if (m && m.lane >= 0 && m.lane <= 2 && m.lane_index >= 0 && m.lane_index <= 1) {
occupied[m.lane][m.lane_index] = true;
}
}
// 优先中路(1) -> 上路(0) -> 下路(2)
const priority = [1, 0, 2];
for (let indexInLane = 0; indexInLane < MissionHeroCompComp.HERO_LANE_CAP; indexInLane++) {
for (const lane of priority) {
if (!occupied[lane][indexInLane]) {
return { lane, indexInLane };
const move = h.get(MoveComp); // MoveComp 记录了英雄当前的目标位置
if (move) {
for (let i = 0; i < MissionHeroCompComp.HERO_POSITIONS.length; i++) {
const pos = MissionHeroCompComp.HERO_POSITIONS[i];
if (Math.abs(move.targetX - pos.x) < 2 && Math.abs(move.baseY - pos.y) < 2) {
occupied.add(i);
break;
}
}
}
}
// 溢出:仍放中路,沿 X 继续排
return { lane: 1, indexInLane: 2 };
// 优先中前(1) -> 上前(0) -> 下前(2) -> 中后(4) -> 上后(3) -> 下后(5)
const slotPriority = [1, 0, 2, 4, 3, 5];
for (const idx of slotPriority) {
if (!occupied.has(idx)) {
return MissionHeroCompComp.HERO_POSITIONS[idx];
}
}
// 溢出:默认中前
return MissionHeroCompComp.HERO_POSITIONS[1];
}
/**
@@ -223,16 +217,13 @@ export class MissionHeroCompComp extends CCComp {
console.log("addHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
const { lane, indexInLane } = this.pickLaneForHero(uuid);
const landingPos = this.resolveHeroLandingPos(uuid, lane, indexInLane);
const landingPos = this.pickPositionForHero();
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv);
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
if (model) {
model.lane = lane;
model.lane_index = indexInLane;
oops.message.dispatchEvent(GameEvent.MasterCalled, {
eid: hero.eid,
model: model
@@ -242,33 +233,7 @@ export class MissionHeroCompComp extends CCComp {
return hero;
}
/**
* 计算英雄落点位置。
* Y 坐标来自 HeroPos 配置X 坐标根据英雄类型(近战/远程)决定。
*
* @param uuid 英雄 UUID
* @param lane 分配到的路 (0: 上, 1: 中, 2: 下)
* @param indexInLane 该路排位
* @returns 落点 Vec3
*/
private resolveHeroLandingPos(uuid: number, lane: number, indexInLane: number): Vec3 {
const hero_pos = 0;
const baseY = HeroPos[hero_pos].pos.y + MissionHeroCompComp.HERO_LANE_Y_OFFSETS[lane];
const startX = this.resolveSpawnStartX(uuid);
return v3(startX + indexInLane * MissionHeroCompComp.HERO_GAP_X, baseY, 0);
}
/**
* 根据英雄类型决定出生 X 坐标。
* @param uuid 英雄 UUID
* @returns 近战 or 远程的起始 X
*/
private resolveSpawnStartX(uuid: number): number {
const heroType = HeroInfo[uuid]?.type;
return heroType === HType.Melee
? MissionHeroCompComp.HERO_SPAWN_START_MELEE_X
: MissionHeroCompComp.HERO_SPAWN_START_RANGED_X;
}
/**
* 生成合成后的高级英雄,并覆盖为聚合后的属性。
@@ -278,33 +243,21 @@ export class MissionHeroCompComp extends CCComp {
* @param pool_lv 卡池等级
* @param ap 聚合后攻击力
* @param hp_max 聚合后最大生命值
* @param targetLane 指定生成
* @param targetIndex 指定该路排位
* @param targetPos 指定生成位置
* @returns 实际生成的英雄等级
*/
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetLane?: number, targetIndex?: number): number {
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetPos?: Vec3): number {
console.log("addMergedHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
// 如果未指定路,则按普通添加英雄处理
let lane = targetLane;
let indexInLane = targetIndex;
if (lane === undefined || indexInLane === undefined) {
const res = this.pickLaneForHero(uuid);
lane = res.lane;
indexInLane = res.indexInLane;
}
const landingPos = this.resolveHeroLandingPos(uuid, lane, indexInLane);
const landingPos = targetPos || this.pickPositionForHero();
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv);
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
if (model) {
model.lane = lane;
model.lane_index = indexInLane;
model.ap = Math.max(0, ap);
model.hp_max = Math.max(1, hp_max);
model.hp = model.hp_max;
@@ -540,14 +493,13 @@ export class MissionHeroCompComp extends CCComp {
}
// 计算目标出生点(提前排除素材英雄所占的位置)
const { lane, indexInLane } = this.pickLaneForHero(uuid, mergeEids);
const landingPos = this.resolveHeroLandingPos(uuid, lane, indexInLane);
const landingPos = this.pickPositionForHero(mergeEids);
const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
// 汇聚 → 特效 → 生成
await this.mergeDestroyAtBirth(mergeHeroes, spawnPos);
await this.playMergeBoomFx(spawnPos);
return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax, lane, indexInLane);
return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax, landingPos);
}
/**