Files
pixelheros/assets/script/game/map/MissionHeroComp.ts
panw 44dbded217 fix(地图): 调整英雄出生点起始X坐标以解决重叠问题
将远程和近战英雄的起始出生X坐标统一调整为-280,避免因坐标差异导致英雄单位在地图上生成时出现重叠或间距异常。
2026-03-31 09:51:28 +08:00

335 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { _decorator, instantiate, Prefab, v3, Vec3 } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { Hero } from "../hero/Hero";
import { smc } from "../common/SingletonModuleComp";
import { Timer } from "db://oops-framework/core/common/timer/Timer";
import { GameEvent } from "../common/config/GameEvent";
import { HeroInfo, HeroPos, HType } from "../common/config/heroSet";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { FacSet } from "../common/config/GameSet";
import { oneCom } from "../skill/oncend";
import { HeroViewComp } from "../hero/HeroViewComp";
const { ccclass } = _decorator;
/** 视图层对象 */
@ccclass('MissionHeroCompComp')
@ecs.register('MissionHeroComp', false)
export class MissionHeroCompComp extends CCComp {
/** 英雄出生时的掉落高度,用于表现从空中落地 */
private static readonly HERO_DROP_HEIGHT = 260
/** 出生点横向间隔 */
private static readonly HERO_SPAWN_STEP_X = -60
/** 近战起始出生 X */
private static readonly HERO_SPAWN_START_MELEE_X = -280
/** 远程(含中程)起始出生 X */
private static readonly HERO_SPAWN_START_RANGED_X = -280
/** 占位判断容差 */
private static readonly HERO_SPAWN_OCCUPY_EPSILON_X = 10
/** 预留计时器 */
timer:Timer=new Timer(2)
/** 预留状态:友方是否全部死亡 */
Friend_is_dead:boolean=false
/** 当前处理的英雄 uuid */
current_hero_uuid:number=0
/** 当前英雄数量缓存 */
current_hero_num:number=-1
/** 合成规则2 合 1 或 3 合 1 */
merge_need_count:number=2
/** 允许合成的最高等级 */
merge_max_lv:number=3
/** 是否正在执行一次合成流程 */
is_merging:boolean=false
/** 是否正在消费召唤队列,防止并发处理 */
is_processing_queue:boolean=false
/** 召唤请求队列,保证召唤与合成串行 */
summon_queue:{ uuid: number; hero_lv: number }[]=[]
/** 预留英雄列表 */
heros:any=[]
onLoad(){
/** 节点事件监听 */
this.on(GameEvent.FightReady,this.fight_ready,this)
this.on(GameEvent.Zhaohuan,this.zhao_huan,this)
this.on(GameEvent.MissionEnd,this.clear_heros,this)
/** 全局消息监听 */
oops.message.on(GameEvent.CallHero,this.call_hero,this)
}
onDestroy(){
/** 清理监听,避免节点销毁后仍响应消息 */
oops.message.off(GameEvent.CallHero,this.call_hero,this)
oops.message.off(GameEvent.FightReady,this.fight_ready,this)
oops.message.off(GameEvent.Zhaohuan,this.zhao_huan,this)
oops.message.off(GameEvent.MissionEnd,this.clear_heros,this)
}
start() {
// this.test_call()
}
/** 关卡结束时,清理全部存活英雄 */
clear_heros(){
const heroes = this.getAliveHeroes();
for (let i = 0; i < heroes.length; i++) {
heroes[i].destroy();
}
}
/** 战斗准备阶段重置出战英雄计数 */
fight_ready(){
smc.vmdata.mission_data.hero_num=0
}
// protected update(dt: number): void {
// }
/** 预留:召唤事件扩展入口 */
private zhao_huan(event: string, args: any){
}
/** 召唤请求入口:归一化参数并进入串行队列 */
private async call_hero(event: string, args: any){
const uuid = Number(args?.uuid ?? 1001);
const hero_lv = Math.max(1, Number(args?.hero_lv ?? 1));
this.summon_queue.push({ uuid, hero_lv });
this.processSummonQueue();
}
/** 添加英雄:固定出生点上方生成,再落至落点 */
private addHero(uuid:number=1001,hero_lv:number=1) {
console.log("addHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
const landingPos = this.resolveHeroLandingPos(uuid);
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv);
return hero;
}
private resolveHeroLandingPos(uuid: number): Vec3 {
const hero_pos = 0;
const baseY = HeroPos[hero_pos].pos.y;
const startX = this.resolveSpawnStartX(uuid);
let candidateX = startX;
let guard = 0;
while (guard < 50) {
if (!this.isSpawnXOccupied(candidateX, baseY)) {
break;
}
candidateX += MissionHeroCompComp.HERO_SPAWN_STEP_X;
guard += 1;
}
return v3(candidateX, baseY, 0);
}
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;
}
private isSpawnXOccupied(targetX: number, baseY: number): boolean {
const aliveHeroes = this.getAliveHeroes();
for (let i = 0; i < aliveHeroes.length; i++) {
const view = aliveHeroes[i].get(HeroViewComp);
const node = view?.node;
if (!node || !node.isValid) continue;
const pos = node.getPosition();
if (Math.abs(pos.y - baseY) > 80) continue;
if (Math.abs(pos.x - targetX) <= MissionHeroCompComp.HERO_SPAWN_OCCUPY_EPSILON_X) {
return true;
}
}
return false;
}
/** 添加合成后的新英雄,并覆盖为聚合后的属性 */
private addMergedHero(uuid:number, hero_lv:number, ap:number, hp_max:number): number {
const hero = this.addHero(uuid, hero_lv);
const model = hero.get(HeroAttrsComp);
if (!model) return hero_lv;
model.ap = Math.max(0, ap);
model.hp_max = Math.max(1, hp_max);
model.hp = model.hp_max;
model.dirty_hp = true;
return model.lv;
}
/** 获取当前全部存活友方英雄 */
private getAliveHeroes(): Hero[] {
const heroes: Hero[] = [];
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (!model) return;
if (model.fac !== FacSet.HERO) return;
if (model.is_dead) return;
heroes.push(entity as Hero);
});
return heroes;
}
/** 挑选可参与本次合成的英雄组 */
private pickMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number, needCount: number = 3): Hero[] {
const mergeHeroes: Hero[] = [];
for (let i = 0; i < aliveHeroes.length; i++) {
const model = aliveHeroes[i].get(HeroAttrsComp);
if (!model) continue;
if (model.hero_uuid !== uuid) continue;
if (model.lv !== hero_lv) continue;
mergeHeroes.push(aliveHeroes[i]);
if (mergeHeroes.length === needCount) break;
}
return mergeHeroes;
}
/** 统计满足同 uuid、同等级的可合成英雄数量 */
private countMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number): number {
let count = 0;
for (let i = 0; i < aliveHeroes.length; i++) {
const model = aliveHeroes[i].get(HeroAttrsComp);
if (!model) continue;
if (model.hero_uuid !== uuid) continue;
if (model.lv !== hero_lv) continue;
count += 1;
}
return count;
}
/** 读取当前合成需要数量,仅支持 2 或 3 */
private getMergeNeedCount(): number {
return this.merge_need_count === 2 ? 2 : 3;
}
/** 判断该等级是否还能继续向上合成 */
private canMergeLevel(hero_lv: number): boolean {
return hero_lv < Math.max(1, this.merge_max_lv);
}
/** 串行消费召唤队列,避免同帧并发触发多次合成导致状态错乱 */
private async processSummonQueue() {
if (this.is_processing_queue) return;
this.is_processing_queue = true;
try {
while (this.summon_queue.length > 0) {
const payload = this.summon_queue.shift();
if (!payload) continue;
await this.handleSingleSummon(payload.uuid, payload.hero_lv);
}
} finally {
this.is_processing_queue = false;
}
}
/** 处理单次召唤:先生成,再检测是否触发合成,再尝试链式合成 */
private async handleSingleSummon(uuid: number, hero_lv: number) {
this.addHero(uuid, hero_lv);
if (!this.canMergeLevel(hero_lv)) return;
const needCount = this.getMergeNeedCount();
const aliveHeroes = this.getAliveHeroes();
const mergeHeroes = this.pickMergeHeroes(aliveHeroes, uuid, hero_lv, needCount);
if (mergeHeroes.length !== needCount) return;
this.is_merging = true;
try {
const mergedLv = await this.mergeGroupHeroes(mergeHeroes, uuid, hero_lv);
await this.tryChainMerge(uuid, mergedLv);
} finally {
this.is_merging = false;
}
}
/** 将一组合成素材向出生点汇聚并销毁,全部完成后返回 */
private mergeDestroyAtBirth(mergeHeroes: Hero[], spawnPos: Vec3): Promise<void> {
return new Promise((resolve) => {
let doneCount = 0;
const total = mergeHeroes.length;
if (total <= 0) {
resolve();
return;
}
const onDone = () => {
doneCount += 1;
if (doneCount >= total) {
resolve();
}
};
for (let i = 0; i < mergeHeroes.length; i++) {
mergeHeroes[i].mergeToBirthAndDestroy(spawnPos, onDone);
}
});
}
/** 播放合成爆点特效,使用 oneCom 控制特效生命周期 */
private playMergeBoomFx(worldPos: Vec3): Promise<void> {
return new Promise((resolve) => {
const scene = smc.map?.MapView?.scene;
const layer = scene?.entityLayer?.node;
if (!layer || !layer.isValid) {
resolve();
return;
}
const prefab: Prefab = oops.res.get("game/skill/end/dead", Prefab)!;
if (!prefab) {
resolve();
return;
}
const fx = instantiate(prefab);
if (!fx || !fx.isValid) {
resolve();
return;
}
fx.parent = layer;
fx.setPosition(worldPos);
fx.getComponent(oneCom) || fx.addComponent(oneCom);
this.scheduleOnce(() => resolve(), 0.4);
});
}
/** 执行一次完整合成:聚合属性、销毁素材、播放特效、生成高一级英雄 */
private async mergeGroupHeroes(mergeHeroes: Hero[], uuid: number, hero_lv: number): Promise<number> {
let sumAp = 0;
let sumHpMax = 0;
for (let i = 0; i < mergeHeroes.length; i++) {
const model = mergeHeroes[i].get(HeroAttrsComp);
if (!model) continue;
sumAp += model.ap;
sumHpMax += model.hp_max;
}
const landingPos = this.resolveHeroLandingPos(uuid);
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), sumAp, sumHpMax);
}
/** 链式合成:当前等级合成完成后,继续尝试更高等级,直到条件不满足 */
private async tryChainMerge(uuid: number, startLv: number) {
let checkLv = Math.max(1, startLv);
const needCount = this.getMergeNeedCount();
let guard = 0;
while (guard < 20) {
guard += 1;
if (!this.canMergeLevel(checkLv)) {
break;
}
const aliveHeroes = this.getAliveHeroes();
const sameCount = this.countMergeHeroes(aliveHeroes, uuid, checkLv);
if (sameCount < needCount) {
break;
}
const mergeHeroes = this.pickMergeHeroes(aliveHeroes, uuid, checkLv, needCount);
if (mergeHeroes.length < needCount) {
break;
}
checkLv = await this.mergeGroupHeroes(mergeHeroes, uuid, checkLv);
}
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件时触发,用于自定义释放逻辑 */
reset() {
// this.node.destroy();
}
}