4 Commits

Author SHA1 Message Date
pan
9a24f0a9a5 refactor(missionMon): 调整怪物登场排布逻辑
将怪物从按X轴均匀分布改为按类型分三排登场,匹配英雄编队镜像布局,高优先级怪物自动占据前排
2026-08-20 17:30:36 +08:00
pan
965b04094d refactor(rogue): 调整Rogue模式怪物配置与战斗逻辑
1. 新增怪物出生站位配置
2. 调整单场战斗怪物数量为3只,支持0~2个Boss组合
3. 更新配置校验与怪物生成逻辑
4. 修复英雄预设体的尺寸与位置偏移
2026-08-20 17:19:24 +08:00
pan
e18bcf2133 fix(config): 统一怪物普攻伤害计算方式
补充怪物普攻使用SkillSet基础攻击的注释说明,为所有怪物技能添加overrides.ap=100以显式声明伤害为攻击力100%,修正部分怪物技能ID不匹配问题
2026-08-20 17:19:12 +08:00
pan
75ef3af42b fix(hero): 统一调整英雄预制体尺寸与属性配置
1. 修正多个英雄prefab的碰撞盒、显示尺寸参数,对齐统一规格
2. 调整mb1/3/4/5/6的本地位置参数适配新尺寸
3. 翻倍英雄基础血量属性适配平衡调整
4. 优化HeroAnmComp代码格式与动画播放逻辑
2026-08-20 15:38:24 +08:00
11 changed files with 203 additions and 131 deletions

View File

@@ -250,7 +250,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -85.933,
"y": -40.433,
"z": 0
},
"_lrot": {
@@ -290,8 +290,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 384,
"height": 384
"width": 191.9,
"height": 192
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1094,7 +1094,7 @@
},
"_size": {
"__type__": "cc.Size",
"width": 30,
"width": 80,
"height": 100
},
"_id": ""

View File

@@ -981,8 +981,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 100
"width": 150,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1090,12 +1090,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 50
"y": 75
},
"_size": {
"__type__": "cc.Size",
"width": 30,
"height": 100
"width": 150,
"height": 150
},
"_id": ""
},

View File

@@ -250,7 +250,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -84.866,
"y": -84.432,
"z": 0
},
"_lrot": {
@@ -978,8 +978,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 100
"width": 150,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1087,12 +1087,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 50
"y": 75
},
"_size": {
"__type__": "cc.Size",
"width": 30,
"height": 100
"width": 150,
"height": 150
},
"_id": ""
},

View File

@@ -250,7 +250,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -84.192,
"y": -57.035,
"z": 0
},
"_lrot": {
@@ -290,8 +290,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 384,
"height": 384
"width": 256,
"height": 256
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -981,8 +981,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 100
"width": 150,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1090,12 +1090,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 50
"y": 75
},
"_size": {
"__type__": "cc.Size",
"width": 30,
"height": 100
"width": 150,
"height": 150
},
"_id": ""
},

View File

@@ -60,7 +60,7 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -0.041,
"x": 0,
"y": 0,
"z": 0
},
@@ -250,7 +250,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -87.202,
"y": -57.266,
"z": 0
},
"_lrot": {
@@ -290,8 +290,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 384,
"height": 384
"width": 256,
"height": 256
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -978,8 +978,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 100
"width": 150,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1087,12 +1087,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 50
"y": 75
},
"_size": {
"__type__": "cc.Size",
"width": 30,
"height": 100
"width": 150,
"height": 150
},
"_id": ""
},

View File

@@ -250,7 +250,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -81.733,
"y": -53.96,
"z": 0
},
"_lrot": {
@@ -290,8 +290,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 384,
"height": 384
"width": 256,
"height": 256
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -978,8 +978,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 200,
"height": 220
"width": 150,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1087,12 +1087,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 110
"y": 75
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 220
"height": 150
},
"_id": ""
},

View File

@@ -27,6 +27,11 @@ export enum FightSet {
HERO_ROW_FRONT_X = -200, // 前排(最靠右迎敌):坦克 / 战士
HERO_ROW_MID_X = -260, // 中排:刺客 / 射手
HERO_ROW_BACK_X = -320, // 后排(最靠左):法师 / 辅助
/** 怪物三排出生/编队基准 x(y 均为 BoxSet.GAME_LINE):前排迎敌、中排、后排靠右 */
MON_ROW_FRONT_X = 200, // 前排(最靠左迎敌):近战 / 重型 / 近战Boss
MON_ROW_MID_X = 260, // 中排:远程 / 刺客 / 远程Boss
MON_ROW_BACK_X = 320, // 后排(最靠右):辅助 / 召唤师
WAVE_COIN_BASE = 7, // 回合金币基础奖励
WAVE_COIN_GROW = 2, // 回合金币递增值
WAVE_COIN_MAX = 17, // 回合金币最大基础奖励

View File

@@ -1,4 +1,4 @@
import { v3 } from "cc"
import { v3 } from "cc"
import { BoxSet, FacSet, FightSet } from "./GameSet"
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams } from "./SkillSet"
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
@@ -145,12 +145,12 @@ export const HRoleName: Record<HRole, string> = {
* Why: 局外升级机制将在加载时(Hero.ts / Mon.ts)统一重算属性,配置表只承担"基准+增量"的静态表达。
*/
export const HERO_ROLE_BASE: Record<HRole, { hp: number; ap: number; def: number }> = {
[HRole.Tank]: { hp: 300, ap: 20, def: 50 }, // 坦克:高防高血,前排承伤
[HRole.Warrior]: { hp: 280, ap: 22, def: 30 }, // 战士:均衡
[HRole.Assassin]: { hp: 280, ap: 22, def: 10 }, // 刺客:低防,近战爆发
[HRole.Archer]: { hp: 140, ap: 32, def: 10 }, // 射手:低防,远程物理主C
[HRole.Mage]: { hp: 140, ap: 32, def: 10 }, // 法师:低防,远程增益
[HRole.Support]: { hp: 140, ap: 32, def: 15 }, // 辅助:低防,光环/治疗
[HRole.Tank]: { hp: 600, ap: 20, def: 50 }, // 坦克:高防高血,前排承伤
[HRole.Warrior]: { hp: 560, ap: 22, def: 30 }, // 战士:均衡
[HRole.Assassin]: { hp: 560, ap: 22, def: 10 }, // 刺客:低防,近战爆发
[HRole.Archer]: { hp: 280, ap: 32, def: 10 }, // 射手:低防,远程物理主C
[HRole.Mage]: { hp: 280, ap: 32, def: 10 }, // 法师:低防,远程增益
[HRole.Support]: { hp: 280, ap: 32, def: 15 }, // 辅助:低防,光环/治疗
};
/**
@@ -782,6 +782,8 @@ export const HeroInfo: Record<number, heroInfo> = {
* 性能基准:1 级怪物与 1 级英雄单体性能持平 ——
* 近战怪基准 hp300/ap22/cd1.20,远程怪基准 hp160/ap28/cd1.20,
* 重型/Boss 以更高 hp 换取更低 DPS,承担"墙"与"压场"定位。
* 怪物普攻统一使用 SkillSet 基础单体攻击(6001~6014 段),
* 并通过 overrides.ap=100 显式声明伤害为攻击力的 100%。
* 运行时最终属性 = 本表基础值 × RogueConfig.MON_HP_MUL / MON_AP_MUL
* (Boss 额外 × BOSS_HP_BONUS_MUL),校准目标:整场战斗双方对砍 ≈ 30s。
*/
@@ -791,7 +793,7 @@ export const HeroInfo: Record<number, heroInfo> = {
// 前排怪物 — 近战镜像 1 级英雄基准(hp300/ap22/cd1.20),变体微分定位
6001: {
uuid: 6001, name: "兽人战士", path: "m1", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Melee, hp: 300, ap: 22, def: 20, speed: 70,
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0 } }, info: "基础前排怪, 与1级近战英雄持平"
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "基础前排怪, 与1级近战英雄持平"
},
6002: {
uuid: 6002, name: "兽人精锐战士", path: "m2", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Melee, hp: 340, ap: 26, def: 25, speed: 110,
@@ -799,56 +801,56 @@ export const HeroInfo: Record<number, heroInfo> = {
},
6003: {
uuid: 6003, name: "兽人重装兵", path: "m3", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Heavy, hp: 800, ap: 14, def: 60, speed: 50,
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0 } }, info: "重型坦克怪, 高HP低攻承伤位"
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "重型坦克怪, 高HP低攻承伤位"
},
// 后排怪物 — 远程镜像 1 级法师基准(hp160/ap28/cd1.20),变体微分定位
6004: {
uuid: 6004, name: "兽人射手", path: "m4", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Long, hp: 160, ap: 28, def: 8, speed: 70,
skills: { 6006: { uuid: 6006, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0 } }, info: "后排输出怪, 与1级远程英雄持平"
skills: { 6006: { uuid: 6006, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "后排输出怪, 与1级远程英雄持平"
},
6005: {
uuid: 6005, name: "兽人刺客", path: "m5", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Assassin, hp: 130, ap: 26, def: 5, speed: 130,
skills: { 6102: { uuid: 6102, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal].cd, ccd: 0 } }, info: "高攻速低HP刺客"
skills: { 6002: { uuid: 6002, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal].cd, ccd: 0, overrides: { ap: 100, hit_count: 2 } } }, info: "高攻速低HP刺客"
},
// 特殊位怪物
6006: {
uuid: 6006, name: "骷髅领主", path: "m6", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.MeleeBoss, hp: 900, ap: 20, def: 40, speed: 60,
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0 } }, info: "前排MiniBoss级坦克"
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排MiniBoss级坦克"
},
6007: {
uuid: 6007, name: "兽人术士", path: "m7", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Support, hp: 240, ap: 18, def: 10, speed: 70,
skills: { 6102: { uuid: 6102, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0 } }, info: "后排辅助法师怪"
skills: { 6009: { uuid: 6009, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100, hit_count: 2 } } }, info: "后排辅助法师怪"
},
6008: {
uuid: 6008, name: "兽人火法", path: "m8", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Summoner, hp: 220, ap: 24, def: 10, speed: 70,
skills: { 6014: { uuid: 6014, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0 } }, info: "后排召唤系法师怪"
skills: { 6010: { uuid: 6010, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100, hit_count: 2 } } }, info: "后排召唤系法师怪"
},
// BOSS怪物 — 本表 hp/ap 仅作外观/技能档位参考,运行时面板由 RogueConfig 的
// BOSS_BASE_HP / BOSS_BASE_AP(初始英雄 2 倍强度)统一决定,不读取本表数值
6101: {
uuid: 6101, name: "兽人首领-双刀战士", path: "mb1", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 1200, ap: 24, def: 45, speed: 120,
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0 } }, info: "前排Boss, 攻速型"
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排Boss, 攻速型"
},
6102: {
uuid: 6102, name: "兽人首领-斧头战士", path: "mb2", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 1500, ap: 18, def: 55, speed: 60,
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0 } }, info: "前排Boss, 超高HP"
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排Boss, 超高HP"
},
6103: {
uuid: 6103, name: "兽人首领-魔法师", path: "mb3", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 1000, ap: 28, def: 25, speed: 110,
skills: { 6102: { uuid: 6102, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0 } }, info: "后排法系Boss"
skills: { 6009: { uuid: 6009, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100, hit_count: 2 } } }, info: "后排法系Boss"
},
6104: {
uuid: 6104, name: "兽人首领-射手", path: "mb4", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 1100, ap: 24, def: 25, speed: 70,
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0 } }, info: "后排位Boss, 均衡型"
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "后排位Boss, 均衡型"
},
6105: {
uuid: 6105, name: "亡灵首领-法师", path: "mb5", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 1000, ap: 30, def: 20, speed: 110,
skills: { 6014: { uuid: 6014, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 80, timed_buff_id: 7002 } } }, info: "后排高AP Boss"
skills: { 6011: { uuid: 6011, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100, hit_count: 2, timed_buff_id: 7002 } } }, info: "后排高AP Boss"
},
6106: {
uuid: 6106, name: "亡灵首领-骑马战士", path: "mb6", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 1800, ap: 20, def: 60, speed: 130,
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0 } }, info: "前排终极Boss, 最高HP+高速"
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排终极Boss, 最高HP+高速"
},

View File

@@ -5,15 +5,15 @@ import { FlashSprite } from "./hit-flash-white/scripts/FlashSprite";
const { ccclass, property } = _decorator;
@ccclass('HeroAnmComp')
export default class HeroAnmComp extends Component{
fsSprite:FlashSprite = null!;
private anmcon:any=null
export default class HeroAnmComp extends Component {
fsSprite: FlashSprite = null!;
private anmcon: any = null
private _hasStop = true;
private _atkIndex = 0;
private default_anim:string='idle'
anms:any[]=["idle","move","atk0","max0","max1"]
onLoad () {
this.anmcon=this.node.getComponent(Animation)
private default_anim: string = 'idle'
anms: any[] = ["idle", "move", "atk0", "max0", "max1"]
onLoad() {
this.anmcon = this.node.getComponent(Animation)
this.fsSprite = this.node.getComponent(FlashSprite);
this.anmcon.on(Animation.EventType.FINISHED, this.onAnimationFinished, this);
}
@@ -24,46 +24,61 @@ export default class HeroAnmComp extends Component{
}
}
stop () {
/** 大招是否播放中(最高优先级,期间禁止一切动画切换) */
private get maxPlaying(): boolean {
return !!(this.anmcon.getState("max0")?.isPlaying || this.anmcon.getState("max1")?.isPlaying);
}
/** 普攻是否播放中(次优先级,仅可被大招打断) */
private get atkPlaying(): boolean {
return !!this.anmcon.getState("atk0")?.isPlaying;
}
/** buff 是否播放中(可被大招/普攻打断,但拦截 idle/move) */
private get buffPlaying(): boolean {
return !!this.anmcon.getState("buff")?.isPlaying;
}
stop() {
this._hasStop = true;
}
onAnimationFinished(type:Animation.EventType, state:AnimationState){
if(state.name!="idle"&&state.name!="move"){
onAnimationFinished(type: Animation.EventType, state: AnimationState) {
if (state.name != "idle" && state.name != "move") {
this.anmcon.play(this.default_anim)
}
}
atked(){
atked() {
this.fsSprite.clickFlash();
}
move () {
if(this.anmcon.getState("move").isPlaying) return
this.anmcon.play("move")
this.default_anim='move'
move() {
if (this.maxPlaying || this.atkPlaying || this.buffPlaying) return
if (this.anmcon.getState("move").isPlaying) return
this.anmcon.play("move")
this.default_anim = 'move'
}
atk () {
if(this.anmcon.getState("max0").isPlaying||this.anmcon.getState("max1").isPlaying) return
atk() {
if (this.maxPlaying) return
this.anmcon.play(`atk0`)
}
max () {
if(this.anmcon.getState("max0").isPlaying||this.anmcon.getState("max1").isPlaying) return
this.anmcon.play("max0")
max() {
if (this.maxPlaying) return
this.anmcon.play("max0")
}
play(anm:string=""){
if(anm==="") return
if(this.anmcon.getState("max0").isPlaying||this.anmcon.getState("max1").isPlaying) return
play(anm: string = "") {
if (anm === "") return
if (this.maxPlaying) return
this.anmcon.play(anm)
}
idle () {
if(this.anmcon.getState("idle").isPlaying) return
idle() {
if (this.maxPlaying || this.atkPlaying || this.buffPlaying) return
if (this.anmcon.getState("idle").isPlaying) return
this.anmcon.play("idle")
this.default_anim='idle'
this.default_anim = 'idle'
}
buff(){
if(this.anmcon.getState("max0").isPlaying||this.anmcon.getState("max1").isPlaying) return
buff() {
if (this.maxPlaying) return
this.anmcon.play("buff")
}
dead(){
dead() {
}
}

View File

@@ -7,7 +7,7 @@
* 2. 战斗开始:PhasePrepareEnd 时恢复怪物行动(in_fight 前怪物仅原地待命,不会推进/索敌)。
*
* 关键设计:
* - 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场,玩家看清阵容后三选一。
* - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。
* - 实际推进由统一 MoveComp 在战斗中向敌人移动自然形成,不再使用固定网格点。
* - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。
*/
@@ -19,8 +19,9 @@ import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/O
import { Monster } from "../hero/Mon";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { BoxSet } from "../common/config/GameSet";
import { spawningEngine, GeneratedMonster, TestModeConfig, BOSS_COUNT, MINION_COUNT } from "./RogueConfig";
import { BoxSet, FightSet } from "../common/config/GameSet";
import { MonType } from "../common/config/heroSet";
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MoveComp } from "../hero/MoveComp";
@@ -33,10 +34,23 @@ export class MissionMonCompComp extends CCComp {
/** 怪物出生掉落高度 */
private static readonly MON_DROP_HEIGHT = 0;
/** 怪物登场 X 范围下限(靠近英雄侧) */
private static readonly SPAWN_X_MIN = 100;
/** 怪物登场 X 范围上限(靠近右侧) */
private static readonly SPAWN_X_MAX = 320;
/**
* 怪物类型 → 编队优先级(与英雄 HRole 枚举值升序一致):
* 近战/重型/近战Boss → 0(最优先,占前排),
* 远程/刺客/远程Boss → 1(占中排),
* 辅助/召唤师 → 2(占后排)。
*/
private static readonly MON_TYPE_PRIORITY: Record<number, number> = {
[MonType.Melee]: 0,
[MonType.Heavy]: 0,
[MonType.MeleeBoss]: 0,
[MonType.Long]: 1,
[MonType.Assassin]: 1,
[MonType.LongBoss]: 1,
[MonType.Support]: 2,
[MonType.Summoner]: 2,
};
/** 槽位基准 Y(3行×4列结构仅供 monGrid 寻路与 SCastSystem 索敌定位) */
public static readonly MON_POSITIONS: Vec3[] = [
@@ -104,11 +118,16 @@ export class MissionMonCompComp extends CCComp {
smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount;
// 怪物先行登场:玩家侦查 Boss 组合后再做英雄三选一(in_fight 前怪物原地待命,不推进不索敌)
const squad = this.spawnQueue;
// 按编队优先级排序后依次登场:近战/Boss(0) → 远程/刺客(1) → 辅助/召唤(2)
const squad = [...this.spawnQueue].sort((a, b) => {
const pa = MissionMonCompComp.MON_TYPE_PRIORITY[a.type] ?? 1;
const pb = MissionMonCompComp.MON_TYPE_PRIORITY[b.type] ?? 1;
return pa - pb;
});
this.spawnQueue = [];
smc.vmdata.mission_data.pending_mon_num = 0;
for (const monData of squad) {
this.addMonsterAtGrid(this.waveSpawnedCount, monData, this.currentWave);
for (let i = 0; i < squad.length; i++) {
this.addMonsterAtGrid(i, squad[i], this.currentWave);
this.waveSpawnedCount++;
}
@@ -126,9 +145,24 @@ export class MissionMonCompComp extends CCComp {
// ======================== 怪物生成 ========================
/**
* 解析怪物出生 X:按编队优先级名次映射三排锚点(与英雄编队排队逻辑一致)。
* 名次映射:rank0→前排(MON_ROW_FRONT_X)、rank1→中排(MON_ROW_MID_X)、rank≥2→后排(MON_ROW_BACK_X)。
* 效果:高优先级类型永远占据更靠前的排;当更高优先级类型插入时,低优先级类型自动向后让位。
*
* @param index 当前怪物在排序后列表中的索引(名次)
* @returns 出生 X 坐标
*/
private resolveSpawnX(index: number): number {
if (index === 0) return FightSet.MON_ROW_FRONT_X;
if (index === 1) return FightSet.MON_ROW_MID_X;
return FightSet.MON_ROW_BACK_X;
}
/**
* 在指定位置索引处生成一个怪物
* 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场直接开打
* 怪物按编队优先级名次落排(rank0→前排、rank1→中排、rank≥2→后排),
* 与英雄编队排队逻辑一致:高优先级类型永远占更靠前的排,低优先级自动让位。
*/
private addMonsterAtGrid(
posIndex: number,
@@ -139,9 +173,7 @@ export class MissionMonCompComp extends CCComp {
let scale = -1;
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
const spawnX = MissionMonCompComp.SPAWN_X_MIN
+ (MissionMonCompComp.SPAWN_X_MAX - MissionMonCompComp.SPAWN_X_MIN)
* (posIndex / Math.max(1, BOSS_COUNT + MINION_COUNT - 1));
const spawnX = this.resolveSpawnX(posIndex);
const landingY = basePos.y;
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;

View File

@@ -4,7 +4,7 @@
*
* 游戏机制:
* - 整局只有一场战斗:开局英雄三次三选一后自动开战。
* - 怪物从 BossComboLibrary 按权重随机抽一组(1 Boss + 4 小怪),
* - 怪物从 BossComboLibrary 按权重随机抽一组(固定 3 只怪:0~2 Boss + 剩余小怪),
* 战斗开始瞬间一次全量登场(X∈[100,320] 均匀铺开),双方立即互相攻击。
* - 怪物全灭 → 胜利结算;英雄全灭 → 失败结算;无回合递进、无分批刷怪。
*
@@ -18,7 +18,7 @@
* Boss 基础面板固定为初始英雄 2 倍强度(BOSS_BASE_HP/BOSS_BASE_AP),与小怪共用同一套系数,
* 场上 Boss 恰好为小怪 2 倍强度(如需额外压场可调 BOSS_HP_BONUS_MUL)。
* 小怪基础面板与 1 级英雄持平(近战镜像 hp300/ap22/cd1.8),默认系数校准为
* 整场战斗(3 英雄 vs 4 小怪 + 1 Boss)双方对砍 ≈ WAVE_DURATION(30s)。
* 整场战斗(3 英雄 vs 3 只怪)双方对砍 ≈ WAVE_DURATION(30s)。
* 手感微调只改 MON_HP_MUL / MON_AP_MUL / BOSS_HP_BONUS_MUL 三个系数即可。
*/
@@ -31,9 +31,10 @@ import { FacSet } from "../common/config/GameSet";
/** 单场战斗时长基准(秒):供清屏速度评价(BattleBannerComp 清屏奖励)等使用 */
export const WAVE_DURATION = 30;
/** 怪物登场固定构成:1 个 Boss + 4 个小怪(一次全量登场,打完即整局结束) */
export const BOSS_COUNT = 1;
export const MINION_COUNT = 4;
/** 怪物登场固定构成:单场战斗固定 3 只怪,Boss 数量由组合配置决定(0~2 个),
* 剩余位置由小怪填充。例如:3 小怪、1 Boss + 2 小怪、2 Boss + 1 小怪(最大难度) */
export const BOSS_COUNT = 2;
export const MINION_COUNT = 3;
/** 怪物血量可调系数:最终 hp = HeroInfo 基础hp × MON_HP_MUL。
* 2.6 = 校准后怪物总血量 / 3 英雄 30s 输出(≈30 DPS),双方对砍 ≈ 30s 分胜负 */
@@ -104,11 +105,13 @@ export interface BossComboMinionSlot {
count: number;
}
/** Boss 组合配置:一场战斗随机抽一组登场(1 Boss + minions 小怪) */
/** Boss 组合配置:一场战斗随机抽一组登场(固定 3 只怪,Boss 数量由 bossCount 决定) */
export interface BossComboConfig {
id: string;
name: string;
/** 小怪构成(合计数量建议等于 MINION_COUNT,超出截断、不足自动补近战) */
/** 本场 Boss 数量(0~2),剩余位置由小怪填充 */
bossCount: number;
/** 小怪构成(合计数量建议等于 MINION_COUNT - bossCount,超出截断、不足自动补近战) */
minions: BossComboMinionSlot[];
/** Boss 技能池 id(引用 BossSkillPool,可选) */
boss_skill_pool?: string[];
@@ -118,37 +121,46 @@ export interface BossComboConfig {
weight: number;
}
/** Boss 组合库(硬编码):每场战斗按权重随机抽一组 */
/** Boss 组合库(硬编码):每场战斗按权重随机抽一组,固定 3 只怪。
* 组合类型:
* - 3 小怪(0 Boss):均衡难度
* - 1 Boss + 2 小怪:标准难度
* - 2 Boss + 1 小怪:最大难度 */
export const BossComboLibrary: Record<string, BossComboConfig> = {
/** 3 小怪:均衡组合 */
rage_charge: {
id: "rage_charge", name: "狂暴冲锋组", weight: 10,
minions: [{ type: MonType.Melee, count: 3 }, { type: MonType.Heavy, count: 1 }],
boss_skill_pool: ["boss_rage"],
id: "rage_charge", name: "狂暴冲锋组", weight: 10, bossCount: 0,
minions: [{ type: MonType.Melee, count: 2 }, { type: MonType.Heavy, count: 1 }],
},
iron_wall: {
id: "iron_wall", name: "铁壁推进组", weight: 8,
minions: [{ type: MonType.Heavy, count: 2 }, { type: MonType.Support, count: 1 }, { type: MonType.Long, count: 1 }],
boss_skill_pool: ["boss_iron"],
id: "iron_wall", name: "铁壁推进组", weight: 8, bossCount: 0,
minions: [{ type: MonType.Heavy, count: 2 }, { type: MonType.Support, count: 1 }],
skill_pool: ["tough"],
},
doom_ritual: {
id: "doom_ritual", name: "毁灭仪式组", weight: 6,
minions: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }],
boss_skill_pool: ["boss_doom"],
id: "doom_ritual", name: "毁灭仪式组", weight: 6, bossCount: 0,
minions: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 1 }, { type: MonType.Support, count: 1 }],
skill_pool: ["warcry"],
},
/** 1 Boss + 2 小怪:标准难度 */
assassin_raid: {
id: "assassin_raid", name: "刺客突袭组", weight: 7,
minions: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Melee, count: 2 }],
id: "assassin_raid", name: "刺客突袭组", weight: 7, bossCount: 1,
minions: [{ type: MonType.Assassin, count: 1 }, { type: MonType.Melee, count: 1 }],
boss_skill_pool: ["boss_rage", "boss_doom"],
skill_pool: ["berserk"],
},
blood_legion: {
id: "blood_legion", name: "吸血军团组", weight: 5,
minions: [{ type: MonType.Melee, count: 2 }, { type: MonType.Long, count: 1 }, { type: MonType.Summoner, count: 1 }],
id: "blood_legion", name: "吸血军团组", weight: 5, bossCount: 1,
minions: [{ type: MonType.Melee, count: 1 }, { type: MonType.Long, count: 1 }],
boss_skill_pool: ["boss_iron", "boss_doom"],
skill_pool: ["leech"],
},
/** 2 Boss + 1 小怪:最大难度 */
twin_boss: {
id: "twin_boss", name: "双王降临", weight: 3, bossCount: 2,
minions: [{ type: MonType.Support, count: 1 }],
boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"],
},
};
/**
@@ -277,7 +289,7 @@ export const BossSkillPool: Record<string, MonSkillSet> = {
export function validateRogueConfig(): string[] {
const errors: string[] = [];
// 1. 校验组合引用与数量
// 1. 校验组合引用与数量(固定 3 只怪:bossCount + minionTotal 必须等于 MINION_COUNT)
for (const id in BossComboLibrary) {
const combo = BossComboLibrary[id];
let minionTotal = 0;
@@ -287,8 +299,12 @@ export function validateRogueConfig(): string[] {
errors.push(`组合 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`);
}
}
if (minionTotal !== MINION_COUNT) {
errors.push(`组合 ${id} 小怪数量=${minionTotal} 与 MINION_COUNT=${MINION_COUNT} 不一致`);
const totalMon = combo.bossCount + minionTotal;
if (totalMon !== MINION_COUNT) {
errors.push(`组合 ${id} 总怪数=${totalMon}(bossCount=${combo.bossCount} + minions=${minionTotal})与 MINION_COUNT=${MINION_COUNT} 不一致`);
}
if (combo.bossCount < 0 || combo.bossCount > BOSS_COUNT) {
errors.push(`组合 ${id} bossCount=${combo.bossCount} 超出允许范围 [0, ${BOSS_COUNT}]`);
}
if (combo.skill_pool) {
for (const skillId of combo.skill_pool) {
@@ -371,7 +387,7 @@ export const TestModeConfig = {
export class RogueSpawningEngine {
/**
* 生成一场战斗的怪物列表:随机一组 Boss 组合(1 Boss + 4 小怪,属性走基础×系数)。
* 生成一场战斗的怪物列表:随机一组 Boss 组合(固定 3 只怪,Boss 数量由 combo.bossCount 决定)。
* Boss 压队尾(登场时由 MissionMonComp 统一布位)。
* @returns 怪物列表(小怪在前、Boss 在尾)
*/
@@ -383,6 +399,7 @@ export class RogueSpawningEngine {
const combo = pickBossCombo();
const monsters: GeneratedMonster[] = [];
const bossCount = combo ? Math.max(0, Math.min(combo.bossCount, BOSS_COUNT)) : 0;
if (combo) {
for (const slot of combo.minions) {
@@ -391,13 +408,14 @@ export class RogueSpawningEngine {
}
}
}
// 组合库为空或数量不足兜底:补近战小怪至 MINION_COUNT
while (monsters.length < MINION_COUNT) {
// 组合库为空或数量不足兜底:补近战小怪至 MINION_COUNT - bossCount
const minionTarget = MINION_COUNT - bossCount;
while (monsters.length < minionTarget) {
monsters.push(this.makeMonster(MonType.Melee, monsters.length));
}
// Boss 压队尾
for (let i = 0; i < BOSS_COUNT; i++) {
for (let i = 0; i < bossCount; i++) {
monsters.push(this.makeBoss());
}