feat(monster&spawn): 新增飞行怪物支持,重构怪物移动与刷怪系统
抽离MonMoveComp拆分怪物移动逻辑,让MoveComp仅负责英雄移动 新增Fly和FlyBoss怪物类型,配置三层飞行轨道支持空中怪物 重写波次刷怪逻辑,移除固定5槽限制,按轨道自由排布怪物 将怪物生成上限与恢复阈值从5/3调整为50/30 优化渲染排序逻辑,为飞行怪添加持续浮动动画 移除跨波怪物属性继承,波次切换时自动清理残留怪物
This commit is contained in:
@@ -7,6 +7,7 @@ import { HeroInfo } from "../common/config/heroSet";
|
|||||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||||
import { HeroViewComp } from "./HeroViewComp";
|
import { HeroViewComp } from "./HeroViewComp";
|
||||||
import { MoveComp } from "./MoveComp";
|
import { MoveComp } from "./MoveComp";
|
||||||
|
import { MonMoveComp } from "./MonMoveComp";
|
||||||
import { GameEvent } from "../common/config/GameEvent";
|
import { GameEvent } from "../common/config/GameEvent";
|
||||||
/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */
|
/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */
|
||||||
@ecs.register(`Monster`)
|
@ecs.register(`Monster`)
|
||||||
@@ -16,7 +17,7 @@ export class Monster extends ecs.Entity {
|
|||||||
/** 怪物表现组件引用 */
|
/** 怪物表现组件引用 */
|
||||||
HeroView!: HeroViewComp;
|
HeroView!: HeroViewComp;
|
||||||
/** 怪物移动组件引用 */
|
/** 怪物移动组件引用 */
|
||||||
MonMove!: MoveComp;
|
MonMove!: MonMoveComp;
|
||||||
/** 调试开关,控制生命周期日志输出 */
|
/** 调试开关,控制生命周期日志输出 */
|
||||||
private debugMode: boolean = false;
|
private debugMode: boolean = false;
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ export class Monster extends ecs.Entity {
|
|||||||
/** 注册实体必需组件:移动 + 属性 */
|
/** 注册实体必需组件:移动 + 属性 */
|
||||||
protected init() {
|
protected init() {
|
||||||
this.addComponents<ecs.Comp>(
|
this.addComponents<ecs.Comp>(
|
||||||
MoveComp,
|
MonMoveComp,
|
||||||
HeroAttrsComp,
|
HeroAttrsComp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,7 @@ export class Monster extends ecs.Entity {
|
|||||||
* 2) 初始化表现、属性、技能与阵营
|
* 2) 初始化表现、属性、技能与阵营
|
||||||
* 3) 播放下落入场并在落地后启用碰撞与移动
|
* 3) 播放下落入场并在落地后启用碰撞与移动
|
||||||
*/
|
*/
|
||||||
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y,mon_lv:number=1) {
|
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y,mon_lv:number=1, flyLaneIndex:number = 0) {
|
||||||
// 怪物默认朝左,表现缩放固定为负向
|
// 怪物默认朝左,表现缩放固定为负向
|
||||||
scale=-1
|
scale=-1
|
||||||
// 当前怪物尺寸固定,保留变量便于后续扩展
|
// 当前怪物尺寸固定,保留变量便于后续扩展
|
||||||
@@ -200,7 +201,7 @@ export class Monster extends ecs.Entity {
|
|||||||
// 广播怪物加载事件,供刷怪与战斗系统联动
|
// 广播怪物加载事件,供刷怪与战斗系统联动
|
||||||
oops.message.dispatchEvent("monster_load",this)
|
oops.message.dispatchEvent("monster_load",this)
|
||||||
// 初始化移动参数:方向、目标 X、站位基准 Y
|
// 初始化移动参数:方向、目标 X、站位基准 Y
|
||||||
const move = this.get(MoveComp);
|
const move = this.get(MonMoveComp);
|
||||||
move.reset();
|
move.reset();
|
||||||
move.direction = -1;
|
move.direction = -1;
|
||||||
move.targetX = pos.x;
|
move.targetX = pos.x;
|
||||||
@@ -219,7 +220,19 @@ export class Monster extends ecs.Entity {
|
|||||||
if (!node || !node.isValid) return;
|
if (!node || !node.isValid) return;
|
||||||
// 落地后锁定最终位置,切换到落地完成状态
|
// 落地后锁定最终位置,切换到落地完成状态
|
||||||
node.setPosition(pos.x, dropToY, 0);
|
node.setPosition(pos.x, dropToY, 0);
|
||||||
|
// 如果是飞行怪,可以保持空中状态,这里依然调用 down
|
||||||
view.playEnd("down");
|
view.playEnd("down");
|
||||||
|
|
||||||
|
// 飞行怪加一个轻微的上下浮动动效
|
||||||
|
if (flyLaneIndex > 0) {
|
||||||
|
tween(node)
|
||||||
|
.by(1.5, { position: v3(0, 15, 0) }, { easing: "sineOut" })
|
||||||
|
.by(1.5, { position: v3(0, -15, 0) }, { easing: "sineIn" })
|
||||||
|
.union()
|
||||||
|
.repeatForever()
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
move.moving = true;
|
move.moving = true;
|
||||||
// 落地后启用怪物碰撞分组
|
// 落地后启用怪物碰撞分组
|
||||||
if (collider) {
|
if (collider) {
|
||||||
|
|||||||
229
assets/script/game/hero/MonMoveComp.ts
Normal file
229
assets/script/game/hero/MonMoveComp.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||||
|
import { HeroViewComp } from "./HeroViewComp";
|
||||||
|
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||||
|
import { smc } from "../common/SingletonModuleComp";
|
||||||
|
import { FacSet } from "../common/config/GameSet";
|
||||||
|
import { HeroDisVal, HType } from "../common/config/heroSet";
|
||||||
|
import { Node } from "cc";
|
||||||
|
|
||||||
|
@ecs.register('MonMoveComp')
|
||||||
|
export class MonMoveComp extends ecs.Comp {
|
||||||
|
/** 朝向:1=向右,-1=向左 */
|
||||||
|
direction: number = -1;
|
||||||
|
/** 当前移动目标 X */
|
||||||
|
targetX: number = 0;
|
||||||
|
/** 是否允许移动(出生落地前会短暂关闭) */
|
||||||
|
moving: boolean = true;
|
||||||
|
/** 站位基准 Y */
|
||||||
|
baseY: number = 0;
|
||||||
|
/** 出生序,用于同条件渲染排序稳定 */
|
||||||
|
spawnOrder: number = 0;
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.direction = -1;
|
||||||
|
this.targetX = 0;
|
||||||
|
this.moving = true;
|
||||||
|
this.baseY = 0;
|
||||||
|
this.spawnOrder = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ecs.register('MonMoveSystem')
|
||||||
|
export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
||||||
|
/** 近战判定射程 */
|
||||||
|
private readonly meleeAttackRange = 250;
|
||||||
|
/** 远程判定射程 */
|
||||||
|
private readonly longAttackRange = 600;
|
||||||
|
/** 渲染层级重排节流,避免每帧排序 */
|
||||||
|
private readonly renderSortInterval = 0.05;
|
||||||
|
private lastRenderSortAt = 0;
|
||||||
|
private monMoveMatcher: ecs.IMatcher | null = null;
|
||||||
|
private heroViewMatcher: ecs.IMatcher | null = null;
|
||||||
|
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number }[] = [];
|
||||||
|
private renderEntryCount = 0;
|
||||||
|
|
||||||
|
private getMonMoveMatcher(): ecs.IMatcher {
|
||||||
|
if (!this.monMoveMatcher) {
|
||||||
|
this.monMoveMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp, MonMoveComp);
|
||||||
|
}
|
||||||
|
return this.monMoveMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHeroViewMatcher(): ecs.IMatcher {
|
||||||
|
if (!this.heroViewMatcher) {
|
||||||
|
this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
|
||||||
|
}
|
||||||
|
return this.heroViewMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(): ecs.IMatcher {
|
||||||
|
return ecs.allOf(MonMoveComp, HeroViewComp, HeroAttrsComp);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(e: ecs.Entity) {
|
||||||
|
/** 战斗未开始/暂停时不驱动移动 */
|
||||||
|
if (!smc.mission.play || smc.mission.pause) return;
|
||||||
|
|
||||||
|
const model = e.get(HeroAttrsComp);
|
||||||
|
const move = e.get(MonMoveComp);
|
||||||
|
const view = e.get(HeroViewComp);
|
||||||
|
if (!model || !move || !view || !view.node) return;
|
||||||
|
if (model.fac !== FacSet.MON) return;
|
||||||
|
if (!move.moving) return;
|
||||||
|
|
||||||
|
/** 关卡阶段性冻结怪物行为 */
|
||||||
|
if (smc.mission.stop_mon_action) {
|
||||||
|
this.clearCombatTarget(model);
|
||||||
|
view.status_change("idle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost()) {
|
||||||
|
this.clearCombatTarget(model);
|
||||||
|
if (!model.is_reviving) view.status_change("idle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有移动都锁定在 baseY,避免出现“漂移” */
|
||||||
|
if (view.node.position.y !== move.baseY) {
|
||||||
|
view.node.setPosition(view.node.position.x, move.baseY, 0);
|
||||||
|
}
|
||||||
|
// 渲染层级统交由 MoveSystem 统一处理,避免两个 System 争抢 setSiblingIndex
|
||||||
|
|
||||||
|
// 仅在战斗中才处理索敌和移动
|
||||||
|
if (!smc.mission.in_fight) return;
|
||||||
|
|
||||||
|
const nearestEnemy = this.findNearestEnemy(e);
|
||||||
|
if (nearestEnemy) {
|
||||||
|
/** 有敌人:进入战斗位移逻辑 */
|
||||||
|
this.processCombatLogic(e, move, view, model, nearestEnemy);
|
||||||
|
this.syncCombatTarget(model, view, nearestEnemy);
|
||||||
|
} else {
|
||||||
|
/** 无敌人:继续向左推进 */
|
||||||
|
this.clearCombatTarget(model);
|
||||||
|
model.is_atking = false;
|
||||||
|
this.moveEntity(view, -1, model.speed / 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearCombatTarget(model: HeroAttrsComp): void {
|
||||||
|
model.combat_target_eid = -1;
|
||||||
|
model.enemy_in_cast_range = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncCombatTarget(model: HeroAttrsComp, selfView: HeroViewComp, enemyView: HeroViewComp): void {
|
||||||
|
if (!enemyView || !enemyView.node || !enemyView.ent) {
|
||||||
|
this.clearCombatTarget(model);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const enemyAttrs = enemyView.ent.get(HeroAttrsComp);
|
||||||
|
if (!enemyAttrs || enemyAttrs.is_dead || enemyAttrs.is_reviving || enemyAttrs.fac === model.fac) {
|
||||||
|
this.clearCombatTarget(model);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.combat_target_eid = enemyView.ent.eid;
|
||||||
|
model.enemy_in_cast_range = this.isEnemyInAttackRange(model, selfView.node.position.x, enemyView.node.position.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isEnemyInAttackRange(model: HeroAttrsComp, selfX: number, enemyX: number): boolean {
|
||||||
|
const dist = Math.abs(selfX - enemyX);
|
||||||
|
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
|
||||||
|
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
|
||||||
|
return dist <= this.longAttackRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private processCombatLogic(e: ecs.Entity, move: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
||||||
|
const selfX = view.node.position.x;
|
||||||
|
const enemyX = enemy.node.position.x;
|
||||||
|
const dist = Math.abs(selfX - enemyX);
|
||||||
|
const inRange = this.isEnemyInAttackRange(model, selfX, enemyX);
|
||||||
|
|
||||||
|
// 接触判定距离,只有接触英雄才停止移动
|
||||||
|
// 飞行怪忽略接触碰撞,不被阻挡
|
||||||
|
const isFly = move.baseY > 50;
|
||||||
|
const touchDistance = 50;
|
||||||
|
const isTouching = !isFly && (dist <= touchDistance);
|
||||||
|
|
||||||
|
// 攻击判定
|
||||||
|
if (inRange) {
|
||||||
|
model.is_atking = true;
|
||||||
|
} else {
|
||||||
|
model.is_atking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动判定:只有接触了英雄才停止移动,否则继续向英雄方向移动
|
||||||
|
if (isTouching) {
|
||||||
|
view.status_change("idle");
|
||||||
|
} else {
|
||||||
|
const dir = enemyX > selfX ? 1 : -1;
|
||||||
|
this.moveEntity(view, dir, model.speed / 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveEntity(view: HeroViewComp, direction: number, speed: number) {
|
||||||
|
const model = view.ent.get(HeroAttrsComp);
|
||||||
|
const move = view.ent.get(MonMoveComp);
|
||||||
|
if (!model || !move) return;
|
||||||
|
|
||||||
|
// 简化的边界限制(怪物主要往左走,英雄防线在左侧,-999999 代表左侧尽头)
|
||||||
|
const moveMinX = -999999;
|
||||||
|
const moveMaxX = 999999;
|
||||||
|
|
||||||
|
const currentX = view.node.position.x;
|
||||||
|
const delta = speed * this.dt * direction;
|
||||||
|
let newX = currentX + delta;
|
||||||
|
|
||||||
|
newX = Math.max(moveMinX, Math.min(moveMaxX, newX));
|
||||||
|
if (Math.abs(newX - currentX) < 0.01) {
|
||||||
|
view.status_change("idle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.node.setPosition(newX, move.baseY, 0);
|
||||||
|
move.direction = direction;
|
||||||
|
|
||||||
|
// 确保怪物的朝向表现,向左走 scale=-1,向右走 scale=1
|
||||||
|
if (direction < 0) {
|
||||||
|
view.scale = -1;
|
||||||
|
} else if (direction > 0) {
|
||||||
|
view.scale = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.status_change("move");
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveCombatRange(model: HeroAttrsComp, defaultMin: number, defaultMax: number): [number, number] {
|
||||||
|
const minRange = model.getCachedMinSkillDistance();
|
||||||
|
const maxRange = model.getCachedMaxSkillDistance();
|
||||||
|
if (maxRange <= 0) return [defaultMin, defaultMax];
|
||||||
|
const safeMin = Math.max(0, Math.min(minRange, maxRange - 20));
|
||||||
|
return [safeMin, maxRange];
|
||||||
|
}
|
||||||
|
|
||||||
|
private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null {
|
||||||
|
const currentView = entity.get(HeroViewComp);
|
||||||
|
if (!currentView?.node) return null;
|
||||||
|
|
||||||
|
const currentPos = currentView.node.position;
|
||||||
|
const myFac = entity.get(HeroAttrsComp).fac;
|
||||||
|
|
||||||
|
let nearest: HeroViewComp | null = null;
|
||||||
|
let minDis = Infinity;
|
||||||
|
|
||||||
|
/** 一次遍历筛出最近敌人(仅比较 x 轴距离,忽略 Y,飞行和地面都能互相攻击) */
|
||||||
|
ecs.query(this.getHeroViewMatcher()).forEach(e => {
|
||||||
|
const m = e.get(HeroAttrsComp);
|
||||||
|
if (m.fac !== myFac && !m.is_dead) {
|
||||||
|
const v = e.get(HeroViewComp);
|
||||||
|
if (v?.node) {
|
||||||
|
const d = Math.abs(currentPos.x - v.node.position.x);
|
||||||
|
if (d < minDis) {
|
||||||
|
minDis = d;
|
||||||
|
nearest = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
assets/script/game/hero/MonMoveComp.ts.meta
Normal file
9
assets/script/game/hero/MonMoveComp.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "a3113d69-645e-4a4b-bf68-a434fcbd6d8d",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { smc } from "../common/SingletonModuleComp";
|
|||||||
import { FacSet } from "../common/config/GameSet";
|
import { FacSet } from "../common/config/GameSet";
|
||||||
import { HeroDisVal, HType } from "../common/config/heroSet";
|
import { HeroDisVal, HType } from "../common/config/heroSet";
|
||||||
import { BoxCollider2D, Node } from "cc";
|
import { BoxCollider2D, Node } from "cc";
|
||||||
|
import { MonMoveComp } from "./MonMoveComp";
|
||||||
|
|
||||||
@ecs.register('MoveComp')
|
@ecs.register('MoveComp')
|
||||||
export class MoveComp extends ecs.Comp {
|
export class MoveComp extends ecs.Comp {
|
||||||
@@ -47,8 +48,10 @@ interface MoveFacConfig {
|
|||||||
|
|
||||||
@ecs.register('MoveSystem')
|
@ecs.register('MoveSystem')
|
||||||
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
||||||
/** 近战判定射程(来自 heroSet) */
|
/** 近战判定射程 */
|
||||||
private readonly meleeAttackRange = HeroDisVal[HType.Melee];
|
private readonly meleeAttackRange = 250;
|
||||||
|
/** 远程判定射程 */
|
||||||
|
private readonly longAttackRange = 600;
|
||||||
private readonly heroFrontAnchorX = -100;
|
private readonly heroFrontAnchorX = -100;
|
||||||
private readonly monFrontAnchorX = 0;
|
private readonly monFrontAnchorX = 0;
|
||||||
/** 常规同阵营横向最小间距(英雄) */
|
/** 常规同阵营横向最小间距(英雄) */
|
||||||
@@ -106,21 +109,13 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
/** 战斗未开始/暂停时不驱动移动 */
|
/** 战斗未开始/暂停时不驱动移动 */
|
||||||
if (!smc.mission.play || smc.mission.pause) return;
|
if (!smc.mission.play || smc.mission.pause) return;
|
||||||
|
|
||||||
/** 在战斗阶段,英雄和怪物不移动 */
|
|
||||||
if (smc.mission.in_fight) return;
|
|
||||||
|
|
||||||
const model = e.get(HeroAttrsComp);
|
const model = e.get(HeroAttrsComp);
|
||||||
const move = e.get(MoveComp);
|
const move = e.get(MoveComp);
|
||||||
const view = e.get(HeroViewComp);
|
const view = e.get(HeroViewComp);
|
||||||
if (!model || !move || !view || !view.node) return;
|
if (!model || !move || !view || !view.node) return;
|
||||||
if (model.fac !== FacSet.HERO && model.fac !== FacSet.MON) return;
|
if (model.fac !== FacSet.HERO) return; // 只处理英雄移动
|
||||||
if (!move.moving) return;
|
if (!move.moving) return;
|
||||||
/** 关卡阶段性冻结怪物行为 */
|
|
||||||
if (model.fac === FacSet.MON && smc.mission.stop_mon_action) {
|
|
||||||
this.clearCombatTarget(model);
|
|
||||||
view.status_change("idle");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost()) {
|
if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost()) {
|
||||||
this.clearCombatTarget(model);
|
this.clearCombatTarget(model);
|
||||||
if (!model.is_reviving) view.status_change("idle");
|
if (!model.is_reviving) view.status_change("idle");
|
||||||
@@ -131,7 +126,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
if (view.node.position.y !== move.baseY) {
|
if (view.node.position.y !== move.baseY) {
|
||||||
view.node.setPosition(view.node.position.x, move.baseY, 0);
|
view.node.setPosition(view.node.position.x, move.baseY, 0);
|
||||||
}
|
}
|
||||||
this.updateRenderOrder(view);
|
|
||||||
|
// 渲染层级重排放在独立的系统或这里统筹,这里我们让它处理所有英雄和怪物的排序
|
||||||
|
this.updateRenderOrder();
|
||||||
|
|
||||||
const nearestEnemy = this.findNearestEnemy(e);
|
const nearestEnemy = this.findNearestEnemy(e);
|
||||||
if (nearestEnemy) {
|
if (nearestEnemy) {
|
||||||
/** 有敌人:进入战斗位移逻辑 */
|
/** 有敌人:进入战斗位移逻辑 */
|
||||||
@@ -169,12 +167,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
const dist = Math.abs(selfX - enemyX);
|
const dist = Math.abs(selfX - enemyX);
|
||||||
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
|
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
|
||||||
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
|
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
|
||||||
if (rangeType === HType.Long) {
|
return dist <= this.longAttackRange;
|
||||||
const [, maxRange] = this.resolveCombatRange(model, 360, 720);
|
|
||||||
return dist <= maxRange;
|
|
||||||
}
|
|
||||||
const [, maxRange] = this.resolveCombatRange(model, 120, 720);
|
|
||||||
return dist <= maxRange;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private processCombatLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
private processCombatLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
||||||
@@ -366,30 +359,42 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
return nearest;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateRenderOrder(view: HeroViewComp) {
|
private updateRenderOrder() {
|
||||||
const scene = smc.map?.MapView?.scene;
|
const scene = smc.map?.MapView?.scene;
|
||||||
const actorRoot = scene?.entityLayer?.node?.getChildByName("HERO");
|
const actorRoot = scene?.entityLayer?.node?.getChildByName("HERO");
|
||||||
if (!actorRoot) return;
|
if (!actorRoot) return;
|
||||||
|
|
||||||
if (view.node.parent !== actorRoot) {
|
|
||||||
view.node.parent = actorRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now() / 1000;
|
const now = Date.now() / 1000;
|
||||||
if (now - this.lastRenderSortAt < this.renderSortInterval) return;
|
if (now - this.lastRenderSortAt < this.renderSortInterval) return;
|
||||||
this.lastRenderSortAt = now;
|
this.lastRenderSortAt = now;
|
||||||
|
|
||||||
this.renderEntryCount = 0;
|
this.renderEntryCount = 0;
|
||||||
ecs.query(this.getHeroMoveMatcher()).forEach(e => {
|
ecs.query(this.getHeroViewMatcher()).forEach(e => {
|
||||||
const attrs = e.get(HeroAttrsComp);
|
const attrs = e.get(HeroAttrsComp);
|
||||||
const actorView = e.get(HeroViewComp);
|
const actorView = e.get(HeroViewComp);
|
||||||
const actorMove = e.get(MoveComp);
|
if (!attrs || !actorView?.node || attrs.is_dead) return;
|
||||||
if (!attrs || !actorView?.node || !actorMove || attrs.is_dead) return;
|
|
||||||
if (attrs.fac !== FacSet.HERO && attrs.fac !== FacSet.MON) return;
|
if (attrs.fac !== FacSet.HERO && attrs.fac !== FacSet.MON) return;
|
||||||
|
|
||||||
if (actorView.node.parent !== actorRoot) {
|
if (actorView.node.parent !== actorRoot) {
|
||||||
actorView.node.parent = actorRoot;
|
actorView.node.parent = actorRoot;
|
||||||
}
|
}
|
||||||
const frontScore = attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x;
|
|
||||||
|
// 获取 spawnOrder,由于可能挂载 MoveComp 或 MonMoveComp,我们需要动态获取
|
||||||
|
let spawnOrder = 0;
|
||||||
|
const heroMove = e.get(MoveComp);
|
||||||
|
if (heroMove) {
|
||||||
|
spawnOrder = heroMove.spawnOrder;
|
||||||
|
} else {
|
||||||
|
const monMove = e.get(MonMoveComp);
|
||||||
|
if (monMove) {
|
||||||
|
spawnOrder = monMove.spawnOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFly = actorView.node.position.y > 50;
|
||||||
|
// 飞行怪在最前,其次是地面的 X 坐标
|
||||||
|
const frontScore = isFly ? 999999 - actorView.node.position.x : (attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x);
|
||||||
|
|
||||||
const entryIndex = this.renderEntryCount;
|
const entryIndex = this.renderEntryCount;
|
||||||
let entry = this.renderEntries[entryIndex];
|
let entry = this.renderEntries[entryIndex];
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
@@ -399,7 +404,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
entry.node = actorView.node;
|
entry.node = actorView.node;
|
||||||
entry.bossPriority = attrs.is_boss ? 1 : 0;
|
entry.bossPriority = attrs.is_boss ? 1 : 0;
|
||||||
entry.frontScore = frontScore;
|
entry.frontScore = frontScore;
|
||||||
entry.spawnOrder = actorMove.spawnOrder;
|
entry.spawnOrder = spawnOrder;
|
||||||
entry.eid = e.eid;
|
entry.eid = e.eid;
|
||||||
this.renderEntryCount += 1;
|
this.renderEntryCount += 1;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ export class MissionComp extends CCComp {
|
|||||||
// ======================== 配置参数 ========================
|
// ======================== 配置参数 ========================
|
||||||
|
|
||||||
/** 怪物数量上限(超过后暂停刷怪) */
|
/** 怪物数量上限(超过后暂停刷怪) */
|
||||||
private maxMonsterCount: number = 5;
|
private maxMonsterCount: number = 50;
|
||||||
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
|
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
|
||||||
private resumeMonsterCount: number = 3;
|
private resumeMonsterCount: number = 30;
|
||||||
/** 新一波金币奖励基础值(现已固定,不再随波次增长) */
|
/** 新一波金币奖励基础值(现已固定,不再随波次增长) */
|
||||||
private prepareBaseCoinReward: number = 25;
|
private prepareBaseCoinReward: number = 25;
|
||||||
/** 每一波金币增长值(固定收益设为0) */
|
/** 每一波金币增长值(固定收益设为0) */
|
||||||
|
|||||||
@@ -3,18 +3,15 @@
|
|||||||
* @description 怪物(Monster)波次刷新管理组件(逻辑层)
|
* @description 怪物(Monster)波次刷新管理组件(逻辑层)
|
||||||
*
|
*
|
||||||
* 职责:
|
* 职责:
|
||||||
* 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 分配怪物到固定槽位。
|
* 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 生成怪物。
|
||||||
* 2. 管理 5 个固定刷怪槽位的占用状态,支持 Boss 占 2 格。
|
* 2. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。
|
||||||
* 3. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。
|
* 3. 自动推进波次:当前波所有怪物被清除后自动进入下一波。
|
||||||
* 4. 自动推进波次:当前波所有怪物被清除后自动进入下一波。
|
|
||||||
*
|
*
|
||||||
* 关键设计:
|
* 关键设计:
|
||||||
* - 全场固定 5 个槽位(索引 0-4),每个槽位占固定 X 坐标。
|
* - 突破 5 槽限制,怪物按刷怪顺序依次从 X=280 开始向右(每隔 50)排布。
|
||||||
* - Boss 默认占 3 个连续槽位,只要有连续三格空闲即可。
|
* - 3 条刷怪线:地面、+120、+240。
|
||||||
* - slotOccupiedEids 记录每个槽位占用的怪物 ECS 实体 ID。
|
|
||||||
* - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。
|
* - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。
|
||||||
* - refreshSlotOccupancy() 定期检查槽位占用的实体是否仍存活,清除已死亡的占用。
|
* - 去除跨波 HP 继承,上一波残留怪在波次结束/开始时销毁。
|
||||||
* - tryAdvanceWave() 在所有怪物死亡后自动推进波次。
|
|
||||||
*
|
*
|
||||||
* 怪物属性计算公式:
|
* 怪物属性计算公式:
|
||||||
* ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias)
|
* ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias)
|
||||||
@@ -25,7 +22,7 @@
|
|||||||
* - RogueConfig —— 怪物类型、成长值、波次配置
|
* - RogueConfig —— 怪物类型、成长值、波次配置
|
||||||
* - Monster(hero/Mon.ts)—— 怪物 ECS 实体类
|
* - Monster(hero/Mon.ts)—— 怪物 ECS 实体类
|
||||||
* - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置)
|
* - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置)
|
||||||
* - HeroAttrsComp / MoveComp —— 怪物属性和移动组件
|
* - HeroAttrsComp / MonMoveComp —— 怪物属性和移动组件
|
||||||
* - BoxSet.GAME_LINE —— 地面基准 Y 坐标
|
* - BoxSet.GAME_LINE —— 地面基准 Y 坐标
|
||||||
*/
|
*/
|
||||||
import { _decorator, v3, Vec3 } from "cc";
|
import { _decorator, v3, Vec3 } from "cc";
|
||||||
@@ -37,33 +34,31 @@ import { Monster } from "../hero/Mon";
|
|||||||
import { HeroInfo, HType } from "../common/config/heroSet";
|
import { HeroInfo, HType } from "../common/config/heroSet";
|
||||||
import { smc } from "../common/SingletonModuleComp";
|
import { smc } from "../common/SingletonModuleComp";
|
||||||
import { GameEvent } from "../common/config/GameEvent";
|
import { GameEvent } from "../common/config/GameEvent";
|
||||||
import {BoxSet } from "../common/config/GameSet";
|
import {BoxSet, FacSet } from "../common/config/GameSet";
|
||||||
import { MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType, WaveSlotConfig, DefaultWaveSlot, IWaveSlot } from "./RogueConfig";
|
import { MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType, WaveSlotConfig, DefaultWaveSlot, IWaveSlot } from "./RogueConfig";
|
||||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||||
import { MoveComp } from "../hero/MoveComp";
|
import { MonMoveComp } from "../hero/MonMoveComp";
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MissionMonCompComp —— 怪物波次刷新管理器
|
* MissionMonCompComp —— 怪物波次刷新管理器
|
||||||
*
|
*
|
||||||
* 每波开始时根据 WaveSlotConfig 配置分配怪物到固定槽位,
|
* 每波开始时根据 WaveSlotConfig 配置生成怪物,
|
||||||
* 战斗中监控槽位状态,所有怪物消灭后自动推进到下一波。
|
* 战斗中监控数量,所有怪物消灭后自动推进到下一波。
|
||||||
*/
|
*/
|
||||||
@ccclass('MissionMonCompComp')
|
@ccclass('MissionMonCompComp')
|
||||||
@ecs.register('MissionMonComp', false)
|
@ecs.register('MissionMonComp', false)
|
||||||
export class MissionMonCompComp extends CCComp {
|
export class MissionMonCompComp extends CCComp {
|
||||||
// ======================== 常量 ========================
|
// ======================== 常量 ========================
|
||||||
|
|
||||||
/** Boss 的渲染优先级偏移(确保 Boss 始终渲染在最前) */
|
/** 怪物出生点起点 X */
|
||||||
private static readonly BOSS_RENDER_PRIORITY = 1000000;
|
private static readonly MON_SPAWN_START_X = 280;
|
||||||
/** 第一个槽位的 X 坐标起点 */
|
/** 怪物出生的 X 间距 */
|
||||||
private static readonly MON_SLOT_START_X = 50;
|
private static readonly MON_SPAWN_GAP_X = 50;
|
||||||
/** 槽位间的 X 间距 */
|
|
||||||
private static readonly MON_SLOT_X_INTERVAL = 65;
|
|
||||||
/** 怪物出生掉落高度 */
|
/** 怪物出生掉落高度 */
|
||||||
private static readonly MON_DROP_HEIGHT = 280;
|
private static readonly MON_DROP_HEIGHT = 280;
|
||||||
/** 最大槽位数 */
|
/** 飞行层高度偏移(地面, 空中1, 空中2) */
|
||||||
private static readonly MAX_SLOTS = 5;
|
private static readonly FLY_LANE_Y_OFFSETS = [0, 120, 240];
|
||||||
|
|
||||||
// ======================== 编辑器属性 ========================
|
// ======================== 编辑器属性 ========================
|
||||||
|
|
||||||
@@ -81,12 +76,14 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
uuid: number,
|
uuid: number,
|
||||||
/** 怪物等级 */
|
/** 怪物等级 */
|
||||||
level: number,
|
level: number,
|
||||||
|
/** 飞行层 */
|
||||||
|
flyLane: number,
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
// ======================== 运行时状态 ========================
|
// ======================== 运行时状态 ========================
|
||||||
|
|
||||||
/** 槽位占用状态:记录每个槽位当前占用的怪物 ECS 实体 ID,null 表示空闲 */
|
/** 记录每条线当前排到的索引 */
|
||||||
private slotOccupiedEids: Array<number | null> = Array(5).fill(null);
|
private laneIndices: number[] = [0, 0, 0];
|
||||||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||||||
private globalSpawnOrder: number = 0;
|
private globalSpawnOrder: number = 0;
|
||||||
/** 插队刷怪处理计时器 */
|
/** 插队刷怪处理计时器 */
|
||||||
@@ -110,17 +107,13 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
/**
|
/**
|
||||||
* 帧更新:
|
* 帧更新:
|
||||||
* 1. 检查游戏是否运行中。
|
* 1. 检查游戏是否运行中。
|
||||||
* 2. 刷新槽位占用状态(清除已死亡怪物的占用)。
|
* 2. 处理插队刷怪队列。
|
||||||
* 3. 尝试推进波次(所有怪物清除后自动进入下一波)。
|
|
||||||
* 4. 处理插队刷怪队列。
|
|
||||||
*/
|
*/
|
||||||
protected update(dt: number): void {
|
protected update(dt: number): void {
|
||||||
if(!smc.mission.play) return
|
if(!smc.mission.play) return
|
||||||
if(smc.mission.pause) return
|
if(smc.mission.pause) return
|
||||||
if(smc.mission.stop_mon_action) return;
|
if(smc.mission.stop_mon_action) return;
|
||||||
if(!smc.mission.in_fight) return;
|
if(!smc.mission.in_fight) return;
|
||||||
this.refreshSlotOccupancy();
|
|
||||||
if(!smc.mission.in_fight) return;
|
|
||||||
if(smc.mission.stop_spawn_mon) return;
|
if(smc.mission.stop_spawn_mon) return;
|
||||||
this.updateSpecialQueue(dt);
|
this.updateSpecialQueue(dt);
|
||||||
}
|
}
|
||||||
@@ -130,7 +123,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
/**
|
/**
|
||||||
* 接收特殊刷怪事件并入队。
|
* 接收特殊刷怪事件并入队。
|
||||||
* @param event 事件名
|
* @param event 事件名
|
||||||
* @param args { uuid: number, level: number }
|
* @param args { uuid: number, level: number, flyLane?: number }
|
||||||
*/
|
*/
|
||||||
private onSpawnSpecialMonster(event: string, args: any) {
|
private onSpawnSpecialMonster(event: string, args: any) {
|
||||||
if (!args) return;
|
if (!args) return;
|
||||||
@@ -138,6 +131,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
this.MonQueue.push({
|
this.MonQueue.push({
|
||||||
uuid: args.uuid,
|
uuid: args.uuid,
|
||||||
level: args.level,
|
level: args.level,
|
||||||
|
flyLane: args.flyLane || 0
|
||||||
});
|
});
|
||||||
// 加速队列消费
|
// 加速队列消费
|
||||||
this.queueTimer = 1.0;
|
this.queueTimer = 1.0;
|
||||||
@@ -158,11 +152,12 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
this.waveTargetCount = 0
|
this.waveTargetCount = 0
|
||||||
this.waveSpawnedCount = 0
|
this.waveSpawnedCount = 0
|
||||||
this.MonQueue = []
|
this.MonQueue = []
|
||||||
|
this.laneIndices = [0, 0, 0];
|
||||||
|
|
||||||
let hasBoss = false;
|
let hasBoss = false;
|
||||||
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
||||||
for (const slot of config) {
|
for (const slot of config) {
|
||||||
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss) {
|
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss) {
|
||||||
hasBoss = true;
|
hasBoss = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,7 +167,6 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
bossWave: hasBoss,
|
bossWave: hasBoss,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 不再直接调用 startNextWave(),等待进入 PrepareEnd 阶段再刷怪
|
|
||||||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
|
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,54 +174,26 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
|
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
|
||||||
* 1. 判断怪物是否为 Boss(决定占用 1 格还是 2 格)。
|
* 1. 根据飞行层排号。
|
||||||
* 2. 在空闲槽位中查找合适位置。
|
* 2. 找到后从队列中移除并生成怪物。
|
||||||
* 3. 找到后从队列中移除并生成怪物。
|
|
||||||
*/
|
*/
|
||||||
private updateSpecialQueue(dt: number) {
|
private updateSpecialQueue(dt: number) {
|
||||||
if (this.MonQueue.length <= 0) return;
|
if (this.MonQueue.length <= 0) return;
|
||||||
this.queueTimer += dt;
|
this.queueTimer += dt;
|
||||||
if (this.queueTimer < 0.15) return;
|
if (this.queueTimer < 0.15) return;
|
||||||
|
|
||||||
const item = this.MonQueue[0];
|
const item = this.MonQueue.shift()!;
|
||||||
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].includes(item.uuid);
|
this.queueTimer = 0;
|
||||||
const isLongBoss = MonList[MonType.LongBoss].includes(item.uuid);
|
|
||||||
const slotsPerMon = isBoss ? 3 : 1;
|
|
||||||
|
|
||||||
// 查找空闲槽位
|
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) ||
|
||||||
let slotIndex = -1;
|
MonList[MonType.LongBoss].includes(item.uuid) ||
|
||||||
if (slotsPerMon === 3) {
|
(MonList[MonType.FlyBoss] && MonList[MonType.FlyBoss].includes(item.uuid));
|
||||||
// 构造可用索引列表
|
|
||||||
let allowedIndices = [];
|
|
||||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS - 2; i++) {
|
|
||||||
allowedIndices.push(i);
|
|
||||||
}
|
|
||||||
// 远程 Boss 插队时优先尝试从后往前找
|
|
||||||
if (isLongBoss) {
|
|
||||||
allowedIndices.reverse();
|
|
||||||
}
|
|
||||||
for (const idx of allowedIndices) {
|
|
||||||
if (!this.slotOccupiedEids[idx] && !this.slotOccupiedEids[idx + 1] && !this.slotOccupiedEids[idx + 2]) {
|
|
||||||
slotIndex = idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 普通怪找第一个空闲格
|
|
||||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
|
||||||
if (!this.slotOccupiedEids[i]) {
|
|
||||||
slotIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slotIndex !== -1) {
|
const upType = this.getRandomUpType();
|
||||||
this.MonQueue.shift();
|
const lane = item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : 0;
|
||||||
this.queueTimer = 0;
|
|
||||||
const upType = this.getRandomUpType();
|
this.addMonsterAt(lane, this.laneIndices[lane], item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)));
|
||||||
this.addMonsterBySlot(slotIndex, item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)), slotsPerMon);
|
this.laneIndices[lane]++;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 波次管理 ========================
|
// ======================== 波次管理 ========================
|
||||||
@@ -246,7 +212,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
let hasBoss = false;
|
let hasBoss = false;
|
||||||
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
||||||
for (const slot of config) {
|
for (const slot of config) {
|
||||||
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss) {
|
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss) {
|
||||||
hasBoss = true;
|
hasBoss = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,30 +277,45 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
// ======================== 槽位管理 ========================
|
// ======================== 槽位管理 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置槽位并生成本波所有怪物:
|
* 重新分配并生成本波所有怪物:
|
||||||
* 1. 读取波次配置(WaveSlotConfig 或 DefaultWaveSlot)。
|
* 1. 清理上一波残留怪物。
|
||||||
* 2. 将 Boss 和普通怪分类。
|
* 2. 读取波次配置。
|
||||||
* 3. Boss 优先分配到 0, 2, 4 号位(占 2 格)。
|
* 3. 依据配置和 flyLane 属性,为每只怪物分配自增索引。
|
||||||
* 4. 普通怪填充剩余空闲格。
|
* 4. 立即实例化所有怪物。
|
||||||
* 5. 立即实例化所有怪物。
|
|
||||||
*
|
*
|
||||||
* @param wave 当前波数
|
* @param wave 当前波数
|
||||||
*/
|
*/
|
||||||
private resetSlotSpawnData(wave: number = 1) {
|
private resetSlotSpawnData(wave: number = 1) {
|
||||||
|
// 1. 清理上一波残留怪物
|
||||||
|
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
|
||||||
|
const attrs = e.get(HeroAttrsComp);
|
||||||
|
if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) {
|
||||||
|
e.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 读取波次配置
|
||||||
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
|
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
|
||||||
const oldOccupiedEids = [...this.slotOccupiedEids];
|
|
||||||
this.slotOccupiedEids = Array(MissionMonCompComp.MAX_SLOTS).fill(null);
|
// 3. 重置排号索引
|
||||||
|
this.laneIndices = [0, 0, 0];
|
||||||
|
|
||||||
let allMons: any[] = [];
|
let allMons: any[] = [];
|
||||||
|
|
||||||
// 解析配置
|
// 解析配置
|
||||||
for (const slot of config) {
|
for (const slot of config) {
|
||||||
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss;
|
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss;
|
||||||
const slotsPerMon = slot.slotsPerMon || (isBoss ? 3 : 1);
|
// 判断分配到的飞行层
|
||||||
|
let lane: number = slot.flyLane !== undefined ? slot.flyLane : 0;
|
||||||
|
if (slot.type === MonType.Fly || slot.type === MonType.FlyBoss) {
|
||||||
|
lane = slot.flyLane !== undefined ? slot.flyLane : 1; // 飞行怪默认在第一层
|
||||||
|
}
|
||||||
|
lane = Math.max(0, Math.min(2, lane)); // 约束在 0,1,2
|
||||||
|
|
||||||
for (let i = 0; i < slot.count; i++) {
|
for (let i = 0; i < slot.count; i++) {
|
||||||
const uuid = this.getRandomUuidByType(slot.type);
|
const uuid = this.getRandomUuidByType(slot.type);
|
||||||
const upType = this.getRandomUpType();
|
const upType = this.getRandomUpType();
|
||||||
const req = { uuid, isBoss, upType, monLv: wave, slotsPerMon };
|
const req = { uuid, isBoss, upType, monLv: wave, lane };
|
||||||
allMons.push(req);
|
allMons.push(req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,166 +323,48 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
this.waveTargetCount = allMons.length;
|
this.waveTargetCount = allMons.length;
|
||||||
this.waveSpawnedCount = 0;
|
this.waveSpawnedCount = 0;
|
||||||
|
|
||||||
let assignedSlots = new Array(MissionMonCompComp.MAX_SLOTS).fill(null);
|
// 4. 立即生成本波所有怪物
|
||||||
|
for (const req of allMons) {
|
||||||
// 统一按顺序分配(根据所需格数找连续空位)
|
this.addMonsterAt(req.lane, this.laneIndices[req.lane], req.uuid, req.isBoss, req.upType, req.monLv);
|
||||||
for (const mon of allMons) {
|
this.laneIndices[req.lane]++;
|
||||||
let placed = false;
|
|
||||||
|
|
||||||
if (mon.slotsPerMon === 3) {
|
|
||||||
// 需要 3 格,占满连续的 3 格
|
|
||||||
for (let idx = 0; idx < MissionMonCompComp.MAX_SLOTS - 2; idx++) {
|
|
||||||
if (!assignedSlots[idx] && !assignedSlots[idx + 1] && !assignedSlots[idx + 2]) {
|
|
||||||
assignedSlots[idx] = mon;
|
|
||||||
assignedSlots[idx + 1] = "occupied"; // 占位标记
|
|
||||||
assignedSlots[idx + 2] = "occupied"; // 占位标记
|
|
||||||
placed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 只需要 1 格
|
|
||||||
for (let idx = 0; idx < MissionMonCompComp.MAX_SLOTS; idx++) {
|
|
||||||
if (!assignedSlots[idx]) {
|
|
||||||
assignedSlots[idx] = mon;
|
|
||||||
placed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!placed) {
|
|
||||||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] No slot for monster! uuid:", mon.uuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let absorbedEids = new Set<number>();
|
|
||||||
|
|
||||||
// 立即生成本波所有怪物
|
|
||||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
|
||||||
const req = assignedSlots[i];
|
|
||||||
if (req && req !== "occupied") {
|
|
||||||
let inheritedHp = 0;
|
|
||||||
let inheritedAp = 0;
|
|
||||||
|
|
||||||
for (let j = 0; j < req.slotsPerMon; j++) {
|
|
||||||
let oldEid = oldOccupiedEids[i + j];
|
|
||||||
if (oldEid && !absorbedEids.has(oldEid)) {
|
|
||||||
absorbedEids.add(oldEid);
|
|
||||||
const entity = ecs.getEntityByEid(oldEid);
|
|
||||||
if (entity) {
|
|
||||||
const attrs = entity.get(HeroAttrsComp);
|
|
||||||
if (attrs && attrs.hp > 0 && !attrs.is_dead) {
|
|
||||||
inheritedHp += attrs.hp;
|
|
||||||
inheritedAp += Math.floor(attrs.ap / 2);
|
|
||||||
}
|
|
||||||
entity.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oldOccupiedEids[i + j] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addMonsterBySlot(i, req.uuid, req.isBoss, req.upType, req.monLv, req.slotsPerMon, inheritedHp, inheritedAp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理被覆盖但没有被新怪占用槽位的旧怪(或者保留它们)
|
|
||||||
// 按照需求,保留未被覆盖的旧怪物
|
|
||||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
|
||||||
if (oldOccupiedEids[i]) {
|
|
||||||
const entity = ecs.getEntityByEid(oldOccupiedEids[i]!);
|
|
||||||
if (entity) {
|
|
||||||
const attrs = entity.get(HeroAttrsComp);
|
|
||||||
if (attrs && attrs.hp > 0 && !attrs.is_dead) {
|
|
||||||
this.slotOccupiedEids[i] = oldOccupiedEids[i];
|
|
||||||
} else {
|
|
||||||
entity.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 检查是否有任何槽位仍被活着的怪物占用 */
|
|
||||||
private hasActiveSlotMonster() {
|
|
||||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
|
||||||
if (this.slotOccupiedEids[i]) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新所有槽位的占用状态:
|
|
||||||
* 检查每个占用的 ECS 实体是否仍存在且 HP > 0,
|
|
||||||
* 已失效的清除占用标记。
|
|
||||||
*/
|
|
||||||
private refreshSlotOccupancy() {
|
|
||||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
|
||||||
const eid = this.slotOccupiedEids[i];
|
|
||||||
if (!eid) continue;
|
|
||||||
const entity = ecs.getEntityByEid(eid);
|
|
||||||
if (!entity) {
|
|
||||||
this.slotOccupiedEids[i] = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const attrs = entity.get(HeroAttrsComp);
|
|
||||||
if (!attrs || attrs.hp <= 0) {
|
|
||||||
this.slotOccupiedEids[i] = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 怪物生成 ========================
|
// ======================== 怪物生成 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在指定槽位生成一个怪物:
|
* 在指定层级、指定索引处生成一个怪物:
|
||||||
* 1. 计算出生坐标(多格时居中)。
|
|
||||||
* 2. 创建 Monster ECS 实体。
|
|
||||||
* 3. 标记槽位占用。
|
|
||||||
* 4. 设置渲染排序(Boss 优先级更高)。
|
|
||||||
* 5. 根据阶段和成长类型计算最终 AP / HP。
|
|
||||||
*
|
*
|
||||||
* @param slotIndex 槽位索引(0-5)
|
* @param laneIndex 飞行层索引 (0, 1, 2)
|
||||||
|
* @param monIndex 该层级的第几个怪 (0, 1, 2...)
|
||||||
* @param uuid 怪物 UUID
|
* @param uuid 怪物 UUID
|
||||||
* @param isBoss 是否为 Boss
|
* @param isBoss 是否为 Boss
|
||||||
* @param upType 属性成长类型
|
* @param upType 属性成长类型
|
||||||
* @param monLv 怪物等级
|
* @param monLv 怪物等级
|
||||||
* @param slotsPerMon 占用格数
|
|
||||||
*/
|
*/
|
||||||
private addMonsterBySlot(
|
private addMonsterAt(
|
||||||
slotIndex: number,
|
laneIndex: number,
|
||||||
|
monIndex: number,
|
||||||
uuid: number = 1001,
|
uuid: number = 1001,
|
||||||
isBoss: boolean = false,
|
isBoss: boolean = false,
|
||||||
upType: UpType = UpType.AP1_HP1,
|
upType: UpType = UpType.AP1_HP1,
|
||||||
monLv: number = 1,
|
monLv: number = 1
|
||||||
slotsPerMon: number = 1,
|
|
||||||
inheritedHp: number = 0,
|
|
||||||
inheritedAp: number = 0,
|
|
||||||
) {
|
) {
|
||||||
let mon = ecs.getEntity<Monster>(Monster);
|
let mon = ecs.getEntity<Monster>(Monster);
|
||||||
let scale = -1;
|
let scale = -1;
|
||||||
// 多格占用时居中出生点
|
|
||||||
const centerXOffset = (slotsPerMon - 1) * MissionMonCompComp.MON_SLOT_X_INTERVAL / 2;
|
// 计算坐标
|
||||||
const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL + centerXOffset;
|
const spawnX = MissionMonCompComp.MON_SPAWN_START_X + monIndex * MissionMonCompComp.MON_SPAWN_GAP_X;
|
||||||
const landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0);
|
const landingY = BoxSet.GAME_LINE + MissionMonCompComp.FLY_LANE_Y_OFFSETS[laneIndex] + (isBoss ? 6 : 0);
|
||||||
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
|
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
|
||||||
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
||||||
|
|
||||||
mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv);
|
mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv, laneIndex);
|
||||||
|
|
||||||
// 标记槽位占用
|
|
||||||
for (let j = 0; j < slotsPerMon; j++) {
|
|
||||||
if (slotIndex + j < MissionMonCompComp.MAX_SLOTS) {
|
|
||||||
this.slotOccupiedEids[slotIndex + j] = mon.eid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置渲染排序
|
// 设置渲染排序
|
||||||
const move = mon.get(MoveComp);
|
const move = mon.get(MonMoveComp);
|
||||||
if (move) {
|
if (move) {
|
||||||
move.spawnOrder = isBoss
|
move.spawnOrder = this.globalSpawnOrder;
|
||||||
? MissionMonCompComp.BOSS_RENDER_PRIORITY + this.globalSpawnOrder
|
|
||||||
: this.globalSpawnOrder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算最终属性
|
// 计算最终属性
|
||||||
@@ -511,8 +374,8 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
const stage = this.getCurrentStage();
|
const stage = this.getCurrentStage();
|
||||||
const grow = this.resolveGrowPair(upType, isBoss);
|
const grow = this.resolveGrowPair(upType, isBoss);
|
||||||
const bias = Math.max(0.1, this.getSpawnPowerBias());
|
const bias = Math.max(0.1, this.getSpawnPowerBias());
|
||||||
model.ap = Math.max(1, Math.floor((base.ap + stage * grow[0]) * bias)) + inheritedAp;
|
model.ap = Math.max(1, Math.floor((base.ap + stage * grow[0]) * bias));
|
||||||
model.hp_max = Math.max(1, Math.floor((base.hp + stage * grow[1]) * bias)) + inheritedHp;
|
model.hp_max = Math.max(1, Math.floor((base.hp + stage * grow[1]) * bias));
|
||||||
model.hp = model.hp_max;
|
model.hp = model.hp_max;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ export const MonType = {
|
|||||||
MeleeBoss: 3,
|
MeleeBoss: 3,
|
||||||
/** 远程 Boss */
|
/** 远程 Boss */
|
||||||
LongBoss: 4,
|
LongBoss: 4,
|
||||||
|
/** 飞行普通怪 */
|
||||||
|
Fly: 5,
|
||||||
|
/** 飞行 Boss */
|
||||||
|
FlyBoss: 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 怪物 UUID 池 ========================
|
// ======================== 怪物 UUID 池 ========================
|
||||||
@@ -79,6 +83,8 @@ export const MonList = {
|
|||||||
[MonType.Support]: [6005], // 辅助怪池
|
[MonType.Support]: [6005], // 辅助怪池
|
||||||
[MonType.MeleeBoss]:[6006,6105], // 近战 Boss 池
|
[MonType.MeleeBoss]:[6006,6105], // 近战 Boss 池
|
||||||
[MonType.LongBoss]:[6104], // 远程 Boss 池
|
[MonType.LongBoss]:[6104], // 远程 Boss 池
|
||||||
|
[MonType.Fly]: [6004, 6005], // 飞行怪池 (占位)
|
||||||
|
[MonType.FlyBoss]: [6104], // 飞行 Boss 池 (占位)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 全局刷怪强度系数 ========================
|
// ======================== 全局刷怪强度系数 ========================
|
||||||
@@ -98,23 +104,24 @@ export interface IWaveSlot {
|
|||||||
type: number;
|
type: number;
|
||||||
/** 该类型的怪物数量 */
|
/** 该类型的怪物数量 */
|
||||||
count: number;
|
count: number;
|
||||||
/** (可选)每个怪物占用几个槽位,默认 1;大型 Boss 设为 2 */
|
/** (可选)每个怪物占用几个槽位,默认 1;大型 Boss 设为 2 (新方案中主要用作标识Boss) */
|
||||||
slotsPerMon?: number;
|
slotsPerMon?: number;
|
||||||
|
/** (可选)飞行层:0=地面, 1=第一层空中(+120), 2=第二层空中(+240)。默认地面为 0,Fly/FlyBoss 类型默认 1 */
|
||||||
|
flyLane?: 0 | 1 | 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================================
|
// =========================================================================================
|
||||||
// 【每波怪物占位与刷怪配置说明】
|
// 【每波怪物占位与刷怪配置说明】
|
||||||
//
|
//
|
||||||
// 字段说明:
|
// 字段说明:
|
||||||
// - type: 怪物类型 (参考 MonType,如近战 0,远程 1,Boss 3 等)。
|
// - type: 怪物类型 (参考 MonType,如近战 0,远程 1,Boss 3,飞行 5 等)。
|
||||||
// - count: 该类型的怪在场上同时存在几个。
|
// - count: 该类型的怪在场上同时存在几个。
|
||||||
// - slotsPerMon: (可选) 单个怪物体积占用几个占位坑,默认为 1。
|
// - slotsPerMon: (可选) Boss 占位,旧方案占用多格,现作为标识。
|
||||||
// 大型 Boss 默认设为 3,它会跨占位降落。
|
// - flyLane: (可选) 飞行层,0=地面,1=空中1层,2=空中2层。
|
||||||
//
|
//
|
||||||
// 【规则约束】:
|
// 【规则约束】:
|
||||||
// - 全场固定 5 个槽位(索引 0-4)。
|
// - 突破 5 槽限制,怪物按刷怪顺序依次从 X=280 开始向右(每隔 50)排布。
|
||||||
// - Boss 默认占用 3 个位置,只要有连续 3 格即可。
|
// - Boss 和普通怪占据一个相同的 X 锚点,但视觉体积更大。
|
||||||
// - 每波怪物总槽位占用不能超过 5。
|
|
||||||
// =========================================================================================
|
// =========================================================================================
|
||||||
|
|
||||||
/** 各波次的怪物占位配置(key = 波次编号) */
|
/** 各波次的怪物占位配置(key = 波次编号) */
|
||||||
|
|||||||
Reference in New Issue
Block a user