diff --git a/assets/script/game/common/SingletonModuleComp.ts b/assets/script/game/common/SingletonModuleComp.ts index 9599a8c3..fcaa81cc 100644 --- a/assets/script/game/common/SingletonModuleComp.ts +++ b/assets/script/game/common/SingletonModuleComp.ts @@ -45,6 +45,8 @@ export class SingletonModuleComp extends ecs.Comp { in_select: false, in_fight: false, stop_mon_action: false, + heroGrid: [-1, -1, -1, -1, -1, -1], + monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], }; finish_guides: number[] = [0] data: any = { diff --git a/assets/script/game/hero/Hero.ts b/assets/script/game/hero/Hero.ts index 58c51823..7e98429f 100644 --- a/assets/script/game/hero/Hero.ts +++ b/assets/script/game/hero/Hero.ts @@ -42,6 +42,13 @@ export class Hero extends ecs.Entity { view.node.destroy(); } + const model = this.get(HeroAttrsComp); + if (model && model.posIndex >= 0) { + if (smc.mission.heroGrid[model.posIndex] === this.eid) { + smc.mission.heroGrid[model.posIndex] = -1; + } + } + // 手动移除组件,确保 ecs 侧引用及时释放 this.remove(HeroViewComp); this.remove(HeroAttrsComp); @@ -66,7 +73,7 @@ export class Hero extends ecs.Entity { * 2) 初始化表现与属性数据 * 3) 播放下落入场并在落地后启用碰撞与移动 */ - load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, dropToY:number = pos.y,hero_lv:number=1, pool_lv:number=1) { + load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, dropToY:number = pos.y,hero_lv:number=1, pool_lv:number=1, posIndex: number = -1) { // 英雄始终朝右,表现缩放固定为正向 scale = 1 // 英雄等级在当前规则下上限为 3,避免超配表范围 @@ -114,6 +121,10 @@ export class Hero extends ecs.Entity { model.type = hero.type; model.fac = FacSet.HERO; model.dis = hero.dis ?? 720; + model.posIndex = posIndex; + if (posIndex >= 0) { + smc.mission.heroGrid[posIndex] = this.eid; + } // 复制触发技能配置 model.call = hero.call; diff --git a/assets/script/game/hero/HeroAttrsComp.ts b/assets/script/game/hero/HeroAttrsComp.ts index ad1f6a7b..4c24f29a 100644 --- a/assets/script/game/hero/HeroAttrsComp.ts +++ b/assets/script/game/hero/HeroAttrsComp.ts @@ -76,6 +76,7 @@ export class HeroAttrsComp extends ecs.Comp { minSkillDistance: number = 0; // 最近技能攻击距离(缓存,不受MP影响,用于停止位置判断) // ==================== 阵型位置 ==================== + posIndex: number = -1; // ==================== 标记状态 ==================== is_dead: boolean = false; @@ -408,6 +409,7 @@ export class HeroAttrsComp extends ecs.Comp { this.maxSkillDistance = 0; this.minSkillDistance = 0; + this.posIndex = -1; this.is_dead = false; this.is_count_dead = false; diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index c01e9b81..b55ca471 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -113,6 +113,12 @@ export class Monster extends ecs.Entity { Monster.putToPool(path, view.node); } + if (model && model.posIndex >= 0) { + if (smc.mission.monGrid[model.posIndex] === this.eid) { + smc.mission.monGrid[model.posIndex] = -1; + } + } + // 手动移除组件,避免 ecs 引用滞留 this.remove(HeroViewComp); this.remove(HeroAttrsComp); @@ -170,6 +176,10 @@ export class Monster extends ecs.Entity { model.type = hero.type; model.fac = FacSet.MON; model.dis = hero.dis ?? 720; + model.posIndex = laneIndex; + if (laneIndex >= 0) { + smc.mission.monGrid[laneIndex] = this.eid; + } // 复制触发技能配置 model.call = hero.call; diff --git a/assets/script/game/hero/SCastSystem.ts b/assets/script/game/hero/SCastSystem.ts index 146092be..7d425e3b 100644 --- a/assets/script/game/hero/SCastSystem.ts +++ b/assets/script/game/hero/SCastSystem.ts @@ -100,12 +100,17 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate let highestAp = baseAp; // 2. 获取场上最高攻击力的英雄,保证后期奶量/增益绝对够用 - ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => { - const attr = e.get(HeroAttrsComp); - if (attr && attr.fac === FacSet.HERO && !attr.is_dead && attr.ap > highestAp) { - highestAp = attr.ap; + for (const eid of smc.mission.heroGrid) { + if (eid >= 0) { + const entity = ecs.getEntityByEid(eid); + if (entity) { + const attr = entity.get(HeroAttrsComp); + if (attr && !attr.is_dead && attr.ap > highestAp) { + highestAp = attr.ap; + } + } } - }); + } mockAttrs.ap = highestAp; mockAttrs.critical = 0; @@ -562,16 +567,21 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate */ private collectFriendlyTargetEids(fac: number, selfEid: number | undefined, includeSelf: boolean): number[] { const eids: number[] = []; - ecs.query(this.getHeroMatcher()).forEach(entity => { - const model = entity.get(HeroAttrsComp); - const view = entity.get(HeroViewComp); - if (!model || !view?.node || !view.ent) return; - if (model.fac !== fac) return; - if (model.is_dead || model.is_reviving) return; - const eid = view.ent.eid; - if (!includeSelf && typeof selfEid === "number" && eid === selfEid) return; - eids.push(eid); - }); + const grid = fac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid; + + for (const eid of grid) { + if (eid >= 0) { + if (!includeSelf && typeof selfEid === "number" && eid === selfEid) continue; + + const entity = ecs.getEntityByEid(eid); + if (entity) { + const model = entity.get(HeroAttrsComp); + if (model && !model.is_dead && !model.is_reviving) { + eids.push(eid); + } + } + } + } return eids; } @@ -592,119 +602,85 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate return group === TGroup.Self; } + /** + * 根据网格快速查找最近的敌人 + * 英雄查找怪物:按列从前往后(列0 -> 列3),列内优先同排 + * 怪物查找英雄:按列从前往后(列0 -> 列1),列内优先中路或同排 + */ + private findNearestEnemyByGrid(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null { + if (!heroView.node) return null; + const isHero = heroAttrs.fac === FacSet.HERO; + const myPosIndex = heroAttrs.posIndex; + let myRow = myPosIndex >= 0 ? myPosIndex % 3 : 1; // 默认中路 + + const currentX = heroView.node.position.x; + let targetView: HeroViewComp | null = null; + let minCol = -1; + + if (isHero) { + // 英雄找怪物 + for (let col = 0; col < 4; col++) { + // 列内顺序:优先同排,其次中路,再次其他 + const rowOrder = myRow === 1 ? [1, 0, 2] : [myRow, 1, myRow === 0 ? 2 : 0]; + for (const row of rowOrder) { + const idx = col * 3 + row; + const eid = smc.mission.monGrid[idx]; + if (eid >= 0) { + const target = ecs.getEntityByEid(eid); + if (target) { + const tModel = target.get(HeroAttrsComp); + const tView = target.get(HeroViewComp); + if (tModel && !tModel.is_dead && !tModel.is_reviving && tView && tView.node) { + const dist = Math.abs(currentX - tView.node.position.x); + if (dist <= maxRange) { + return tView; // 找到列内最优且在射程内的目标,直接返回 + } + } + } + } + } + } + } else { + // 怪物找英雄 + for (let col = 0; col < 2; col++) { + // 列内顺序:怪物配置优先中路 + const rowOrder = [1, myRow, myRow === 0 ? 2 : 0]; + for (const row of rowOrder) { + const idx = col * 3 + row; + const eid = smc.mission.heroGrid[idx]; + if (eid >= 0) { + const target = ecs.getEntityByEid(eid); + if (target) { + const tModel = target.get(HeroAttrsComp); + const tView = target.get(HeroViewComp); + if (tModel && !tModel.is_dead && !tModel.is_reviving && tView && tView.node) { + const dist = Math.abs(currentX - tView.node.position.x); + if (dist <= maxRange) { + return tView; + } + } + } + } + } + } + } + return null; + } + /** * 在施法距离内查找最近敌人。 - * 用于单体技能与基础目标参考。 - * 考虑三路设计:同路(Y差较小)优先,如果同路没有目标再考虑跨路 + * 替换为网格化查找,大幅提升性能并解决同排优先问题。 */ private findNearestEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null { - if (!heroView.node) return null; - const currentX = heroView.node.position.x; - const currentY = heroView.node.position.y; - let nearest: HeroViewComp | null = null; - let minDist = Infinity; - let foundPreferredLane = false; - const isHero = heroAttrs.fac === FacSet.HERO; - - ecs.query(this.getHeroMatcher()).forEach(entity => { - const attrs = entity.get(HeroAttrsComp); - const view = entity.get(HeroViewComp); - if (!attrs || !view?.node) return; - if (attrs.fac === heroAttrs.fac) return; - if (attrs.is_dead || attrs.is_reviving) return; - if (this.isOutOfBattleBounds(view.node.position.x)) return; - - const distX = Math.abs(currentX - view.node.position.x); - if (distX > maxRange) return; - - if (isHero) { - // 英雄:单纯找X轴最近,无视路线 - if (distX < minDist) { - minDist = distX; - nearest = view; - } - } else { - // 怪物:优先找中路目标 - const isMidLane = Math.abs(view.node.position.y - BoxSet.GAME_LINE) < 30; // BoxSet.GAME_LINE(100) 是中路 - - if (foundPreferredLane && !isMidLane) return; - - if (isMidLane && !foundPreferredLane) { - foundPreferredLane = true; - minDist = distX; - nearest = view; - return; - } - - if (distX >= minDist) return; - minDist = distX; - nearest = view; - } - }); - return nearest; + return this.findNearestEnemyByGrid(heroAttrs, heroView, maxRange); } /** * 在施法距离内查找“最前排”敌人。 - * 依据施法者面向方向选择 x 轴上更前的目标。 - * 考虑三路设计:英雄找最前排,怪物优先找中路最前排 + * 依据网格排布,列0天然是最前排,因此直接复用网格查找即可。 */ private findFrontEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number, nearestEnemy: HeroViewComp): HeroViewComp | null { - if (!heroView.node || !nearestEnemy.node) return null; - const currentX = heroView.node.position.x; - const currentY = heroView.node.position.y; - const direction = nearestEnemy.node.position.x >= currentX ? 1 : -1; - let frontEnemy: HeroViewComp | null = null; - let edgeX = direction > 0 ? Infinity : -Infinity; - let foundPreferredLane = false; - const isHero = heroAttrs.fac === FacSet.HERO; - - ecs.query(this.getHeroMatcher()).forEach(entity => { - const attrs = entity.get(HeroAttrsComp); - const view = entity.get(HeroViewComp); - if (!attrs || !view?.node) return; - if (attrs.fac === heroAttrs.fac) return; - if (attrs.is_dead || attrs.is_reviving) return; - const enemyX = view.node.position.x; - if (this.isOutOfBattleBounds(enemyX)) return; - - const dist = Math.abs(currentX - enemyX); - if (dist > maxRange) return; - - if (isHero) { - // 英雄:无视路线,找X轴最前的 - if (direction > 0) { - if (enemyX >= edgeX) return; - edgeX = enemyX; - } else { - if (enemyX <= edgeX) return; - edgeX = enemyX; - } - frontEnemy = view; - } else { - // 怪物:优先找中路最前排的 - const isMidLane = Math.abs(view.node.position.y - BoxSet.GAME_LINE) < 30; // BoxSet.GAME_LINE(100) 是中路 - - if (foundPreferredLane && !isMidLane) return; - - if (isMidLane && !foundPreferredLane) { - foundPreferredLane = true; - edgeX = enemyX; - frontEnemy = view; - return; - } - - if (direction > 0) { - if (enemyX >= edgeX) return; - edgeX = enemyX; - } else { - if (enemyX <= edgeX) return; - edgeX = enemyX; - } - frontEnemy = view; - } - }); - return frontEnemy; + return this.findNearestEnemyByGrid(heroAttrs, heroView, maxRange); } /** diff --git a/assets/script/game/map/MissionHeroComp.ts b/assets/script/game/map/MissionHeroComp.ts index a352c58e..12553c50 100644 --- a/assets/script/game/map/MissionHeroComp.ts +++ b/assets/script/game/map/MissionHeroComp.ts @@ -132,11 +132,14 @@ export class MissionHeroComp extends CCComp { if (model && view) { if (model.is_dead) { view.alive(); - const landingPos = this.pickPositionForHero([hero.eid]); + const posIndex = this.pickPositionIndexForHero([hero.eid]); + const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex]; // 不再直接设置位置,而是播放下落入场动画 // 计算出出生点(空中) const spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0); view.node.setPosition(spawnPos); + model.posIndex = posIndex; + if (posIndex >= 0) smc.mission.heroGrid[posIndex] = hero.eid; hero.playDropAnim(spawnPos, landingPos.y); } model.dirty_hp = true; @@ -171,7 +174,7 @@ export class MissionHeroComp extends CCComp { * 动态分配英雄上场的位置 * @param excludeEids 排除计算的实体ID数组(避免复活或合成时把自己算成占据的位置) */ - private pickPositionForHero(excludeEids: number[] = []): Vec3 { + private pickPositionIndexForHero(excludeEids: number[] = []): number { const heroes = this.getAllHeroes().filter(h => { const m = h.get(HeroAttrsComp); return m && !m.is_dead && !excludeEids.includes(h.eid); @@ -179,28 +182,20 @@ export class MissionHeroComp extends CCComp { const occupied = new Set(); for (const h of heroes) { - const move = h.get(MoveComp); // MoveComp 记录了英雄当前的目标位置 - if (move) { - for (let i = 0; i < MissionHeroComp.HERO_POSITIONS.length; i++) { - const pos = MissionHeroComp.HERO_POSITIONS[i]; - if (Math.abs(move.targetX - pos.x) < 2 && Math.abs(move.baseY - pos.y) < 2) { - occupied.add(i); - break; - } - } - } + const m = h.get(HeroAttrsComp); + if (m && m.posIndex >= 0) occupied.add(m.posIndex); } // 优先中前(1) -> 上前(0) -> 下前(2) -> 中后(4) -> 上后(3) -> 下后(5) const slotPriority = [1, 0, 2, 4, 3, 5]; for (const idx of slotPriority) { if (!occupied.has(idx)) { - return MissionHeroComp.HERO_POSITIONS[idx]; + return idx; } } // 溢出:默认中前 - return MissionHeroComp.HERO_POSITIONS[1]; + return 1; } /** @@ -217,9 +212,10 @@ export class MissionHeroComp extends CCComp { console.log("addHero uuid:",uuid) let hero = ecs.getEntity(Hero); let scale = 1 - const landingPos = this.pickPositionForHero(); + const posIndex = this.pickPositionIndexForHero(); + const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex]; let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0); - hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv); + hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv,posIndex); // 召唤完成后,派发事件以更新英雄面板 const model = hero.get(HeroAttrsComp); @@ -246,14 +242,19 @@ export class MissionHeroComp extends CCComp { * @param targetPos 指定生成位置 * @returns 实际生成的英雄等级 */ - private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetPos?: Vec3): number { + private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetPosIndex?: number, targetPos?: Vec3): number { console.log("addMergedHero uuid:",uuid) let hero = ecs.getEntity(Hero); let scale = 1 - const landingPos = targetPos || this.pickPositionForHero(); + let posIndex = targetPosIndex; + let landingPos = targetPos; + if (posIndex === undefined || posIndex < 0 || !landingPos) { + posIndex = this.pickPositionIndexForHero(); + landingPos = MissionHeroComp.HERO_POSITIONS[posIndex]; + } let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0); - hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv); + hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv,posIndex); // 召唤完成后,派发事件以更新英雄面板 const model = hero.get(HeroAttrsComp); @@ -481,14 +482,14 @@ export class MissionHeroComp extends CCComp { sumHpMax += model.hp_max; } - // 计算目标出生点(提前排除素材英雄所占的位置) - const landingPos = this.pickPositionForHero(mergeEids); + const posIndex = this.pickPositionIndexForHero(mergeEids); + const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex]; const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.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, landingPos); + return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax, posIndex, landingPos); } /** diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index ff98f241..0ef195a1 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -544,7 +544,7 @@ export const InfiniteModeConfig = { */ export const TestModeConfig = { /** 是否开启单挑测试模式 */ - enable: true, + enable: false, /** 测试模式中生成怪物的基础生命值 (对应 1级 英雄) */ baseHp: 150, /** 测试模式中生成怪物的基础攻击力 (对应 1级 英雄) */