refactor: 重构关卡战斗流程与AI逻辑,改为单场Boss战模式
1. 近战AI新增警戒线机制,无威胁时退回编队 2. 替换多回合波次为单场Boss战,简化刷怪与回合逻辑 3. 重构招募系统为开局免费三选一流程 4. 调整英雄与怪物基础属性平衡 5. 精简HUD与配置文件冗余代码
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
// ========== 从 HeroAttrs.ts 导入属性相关定义 ==========
|
||||
// ========== 从 HeroAttrs.ts 导入属性相关定义 ==========
|
||||
import { Attrs } from "./HeroAttrs";
|
||||
|
||||
export enum HSSet {
|
||||
|
||||
@@ -402,7 +402,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
|
||||
// 5001 铁壁战士 — atked 护盾(标杆,原5011)
|
||||
5001: {
|
||||
uuid: 5001, name: "铁壁战士", path: "hk1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 300, ap: 28,
|
||||
uuid: 5001, name: "铁壁战士", path: "hk1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 300, ap: 22,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
atked: {
|
||||
6301: [
|
||||
@@ -415,7 +415,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5002 影袭刺客 — atking 自身攻击
|
||||
5002: {
|
||||
uuid: 5002, name: "影袭刺客", path: "hc1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, grow_type: HGrowType.Assassin, role: HRole.Assassin, hp: 300, ap: 28,
|
||||
uuid: 5002, name: "影袭刺客", path: "hc1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, grow_type: HGrowType.Assassin, role: HRole.Assassin, hp: 300, ap: 22,
|
||||
skills: { 6003: { uuid: 6003, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Fast2].cd, ccd: 0 } },
|
||||
atking: {
|
||||
6401: [
|
||||
@@ -428,7 +428,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5003 遗志将军 — dead 全队攻击
|
||||
5003: {
|
||||
uuid: 5003, name: "遗志将军", path: "hk2", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 300, ap: 28,
|
||||
uuid: 5003, name: "遗志将军", path: "hk2", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 300, ap: 22,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
dead: {
|
||||
6401: [
|
||||
@@ -441,7 +441,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5004 破晓先知 — fstart 全队攻击
|
||||
5004: {
|
||||
uuid: 5004, name: "破晓先知", path: "hm1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 143, ap: 40,
|
||||
uuid: 5004, name: "破晓先知", path: "hm1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 143, ap: 32,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid3].cd, ccd: 0 } },
|
||||
fstart: {
|
||||
6401: [
|
||||
@@ -454,7 +454,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5005 战歌法师 — field 攻击光环
|
||||
5005: {
|
||||
uuid: 5005, name: "战歌法师", path: "hm2", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 143, ap: 40,
|
||||
uuid: 5005, name: "战歌法师", path: "hm2", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 143, ap: 32,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid3].cd, ccd: 0 } },
|
||||
field: [
|
||||
{ lv: 1, uuids: [{ uuid: 7002, value: 0.1 }] },
|
||||
@@ -468,7 +468,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
|
||||
// 5101 不死斗士 — atked 回血(更高数值)
|
||||
5101: {
|
||||
uuid: 5101, name: "不死斗士", path: "hk3", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 600, ap: 57,
|
||||
uuid: 5101, name: "不死斗士", path: "hk3", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 600, ap: 46,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
atked: {
|
||||
6302: [
|
||||
@@ -481,7 +481,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5102 致命杀手 — atking 暴击(更高数值)
|
||||
5102: {
|
||||
uuid: 5102, name: "致命杀手", path: "hc2", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, grow_type: HGrowType.Assassin, role: HRole.Assassin, hp: 600, ap: 57,
|
||||
uuid: 5102, name: "致命杀手", path: "hc2", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, grow_type: HGrowType.Assassin, role: HRole.Assassin, hp: 600, ap: 46,
|
||||
skills: { 6003: { uuid: 6003, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Fast2].cd, ccd: 0 } },
|
||||
atking: {
|
||||
6403: [
|
||||
@@ -494,7 +494,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5103 殉道勇士 — dead 全队护盾(更高数值)
|
||||
5103: {
|
||||
uuid: 5103, name: "殉道勇士", path: "hk4", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 600, ap: 57,
|
||||
uuid: 5103, name: "殉道勇士", path: "hk4", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 600, ap: 46,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
dead: {
|
||||
6301: [
|
||||
@@ -507,7 +507,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5104 圣光祭司 — fstart 全队护盾(更高数值)
|
||||
5104: {
|
||||
uuid: 5104, name: "圣光祭司", path: "hm3", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 287, ap: 80,
|
||||
uuid: 5104, name: "圣光祭司", path: "hm3", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 287, ap: 64,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow1].cd, ccd: 0 } },
|
||||
fstart: {
|
||||
6301: [
|
||||
@@ -520,7 +520,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5105 风怒先知 — field 攻速光环
|
||||
5105: {
|
||||
uuid: 5105, name: "风怒先知", path: "hm1", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 287, ap: 80,
|
||||
uuid: 5105, name: "风怒先知", path: "hm1", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 287, ap: 64,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid2].cd, ccd: 0 } },
|
||||
field: [
|
||||
{ lv: 1, uuids: [{ uuid: 7006, value: 0.1 }] },
|
||||
@@ -531,7 +531,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5106 破甲射手 — atking 自身穿透伤害(计时buff增强)
|
||||
5106: {
|
||||
uuid: 5106, name: "破甲射手", path: "ha1", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 287, ap: 80,
|
||||
uuid: 5106, name: "破甲射手", path: "ha1", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 287, ap: 64,
|
||||
skills: { 6008: { uuid: 6008, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid2].cd, ccd: 0 } },
|
||||
atking: {
|
||||
6512: [
|
||||
@@ -547,7 +547,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
|
||||
// 5201 怒血勇士 — atked 全队攻击(范围扩大)
|
||||
5201: {
|
||||
uuid: 5201, name: "怒血勇士", path: "hk5", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 900, ap: 85,
|
||||
uuid: 5201, name: "怒血勇士", path: "hk5", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 900, ap: 68,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
atked: {
|
||||
6401: [
|
||||
@@ -560,7 +560,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5202 援护射手 — atking 队友攻击(范围扩大)
|
||||
5202: {
|
||||
uuid: 5202, name: "援护射手", path: "ha1", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 430, ap: 120,
|
||||
uuid: 5202, name: "援护射手", path: "ha1", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 430, ap: 96,
|
||||
skills: { 6008: { uuid: 6008, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal2].cd, ccd: 0 } },
|
||||
atking: {
|
||||
6401: [
|
||||
@@ -573,7 +573,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5203 传承卫士 — dead 全队生命(范围扩大)
|
||||
5203: {
|
||||
uuid: 5203, name: "传承卫士", path: "hk1", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 900, ap: 85,
|
||||
uuid: 5203, name: "传承卫士", path: "hk1", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 900, ap: 68,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
dead: {
|
||||
6402: [
|
||||
@@ -586,7 +586,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5204 晨光贤者 — fstart 全队暴击(范围扩大)
|
||||
5204: {
|
||||
uuid: 5204, name: "晨光贤者", path: "hm2", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 430, ap: 120,
|
||||
uuid: 5204, name: "晨光贤者", path: "hm2", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 430, ap: 96,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid3].cd, ccd: 0 } },
|
||||
fstart: {
|
||||
6403: [
|
||||
@@ -599,7 +599,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5205 精准术士 — field 暴击光环(范围扩大)
|
||||
5205: {
|
||||
uuid: 5205, name: "精准术士", path: "hm3", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 430, ap: 120,
|
||||
uuid: 5205, name: "精准术士", path: "hm3", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 430, ap: 96,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid2].cd, ccd: 0 } },
|
||||
field: [
|
||||
{ lv: 1, uuids: [{ uuid: 7004, value: 0.1 }] },
|
||||
@@ -613,7 +613,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
|
||||
// 5301 复仇守护者 — atked 全队暴击(范围+数值)
|
||||
5301: {
|
||||
uuid: 5301, name: "复仇守护者", path: "hk2", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 1200, ap: 113,
|
||||
uuid: 5301, name: "复仇守护者", path: "hk2", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 1200, ap: 90,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow1].cd, ccd: 0 } },
|
||||
atked: {
|
||||
6403: [
|
||||
@@ -626,7 +626,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5302 破灭游侠 — atking 队友暴伤(范围+数值)
|
||||
5302: {
|
||||
uuid: 5302, name: "破灭游侠", path: "ha2", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 573, ap: 160,
|
||||
uuid: 5302, name: "破灭游侠", path: "ha2", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 573, ap: 128,
|
||||
skills: { 6008: { uuid: 6008, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal1].cd, ccd: 0 } },
|
||||
atking: {
|
||||
6404: [
|
||||
@@ -639,7 +639,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5303 亡语刺客 — dead 全队暴击+暴伤(双效果)
|
||||
5303: {
|
||||
uuid: 5303, name: "亡语刺客", path: "hc3", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Melee, grow_type: HGrowType.Assassin, role: HRole.Assassin, hp: 1200, ap: 113,
|
||||
uuid: 5303, name: "亡语刺客", path: "hc3", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Melee, grow_type: HGrowType.Assassin, role: HRole.Assassin, hp: 1200, ap: 90,
|
||||
skills: { 6003: { uuid: 6003, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Fast3].cd, ccd: 0 } },
|
||||
dead: {
|
||||
6403: [
|
||||
@@ -657,7 +657,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5304 战吼术士 — fstart 全队攻击+生命(双效果)
|
||||
5304: {
|
||||
uuid: 5304, name: "战吼术士", path: "hm1", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 573, ap: 160,
|
||||
uuid: 5304, name: "战吼术士", path: "hm1", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 573, ap: 128,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid1].cd, ccd: 0 } },
|
||||
fstart: {
|
||||
6401: [
|
||||
@@ -675,7 +675,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5305 领域贤者 — field 暴伤+攻速光环(双效果)
|
||||
5305: {
|
||||
uuid: 5305, name: "领域贤者", path: "hm2", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 573, ap: 160,
|
||||
uuid: 5305, name: "领域贤者", path: "hm2", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Support, hp: 573, ap: 128,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid2].cd, ccd: 0 } },
|
||||
field: [
|
||||
{ lv: 1, uuids: [{ uuid: 7005, value: 0.2 }, { uuid: 7006, value: 0.1 }] },
|
||||
@@ -689,7 +689,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
|
||||
// 5401 不灭战魂 — atked 攻击 + revive(双技能)
|
||||
5401: {
|
||||
uuid: 5401, name: "不灭战魂", path: "hk3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 1500, ap: 142,
|
||||
uuid: 5401, name: "不灭战魂", path: "hk3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, hp: 1500, ap: 114,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
atked: {
|
||||
6401: [
|
||||
@@ -707,7 +707,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5402 战歌游侠 — atking 攻击 + field 攻速光环(双技能)
|
||||
5402: {
|
||||
uuid: 5402, name: "战歌游侠", path: "ha3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 717, ap: 200,
|
||||
uuid: 5402, name: "战歌游侠", path: "ha3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, grow_type: HGrowType.Assassin, role: HRole.Archer, hp: 717, ap: 160,
|
||||
skills: { 6008: { uuid: 6008, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Fast3].cd, ccd: 0 } },
|
||||
atking: {
|
||||
6401: [
|
||||
@@ -725,7 +725,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5403 凤凰祭司 — dead 攻击+护盾 + revive(双技能)
|
||||
5403: {
|
||||
uuid: 5403, name: "凤凰祭司", path: "hk4", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 1500, ap: 142,
|
||||
uuid: 5403, name: "凤凰祭司", path: "hk4", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, hp: 1500, ap: 114,
|
||||
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
|
||||
dead: {
|
||||
6401: [
|
||||
@@ -748,7 +748,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5404 涅槃贤者 — fstart 护盾+攻击 + revive(双技能)
|
||||
5404: {
|
||||
uuid: 5404, name: "涅槃贤者", path: "hm3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 717, ap: 200,
|
||||
uuid: 5404, name: "涅槃贤者", path: "hm3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, hp: 717, ap: 160,
|
||||
skills: { 6013: { uuid: 6013, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal3].cd, ccd: 0 } },
|
||||
fstart: {
|
||||
6301: [
|
||||
@@ -771,7 +771,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
},
|
||||
// 5405 圣言诗人 — atking 治疗 + field 生命光环(双技能)
|
||||
5405: {
|
||||
uuid: 5405, name: "圣言诗人", path: "hh1", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, grow_type: HGrowType.Support, role: HRole.Support, hp: 717, ap: 200,
|
||||
uuid: 5405, name: "圣言诗人", path: "hh1", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, grow_type: HGrowType.Support, role: HRole.Support, hp: 717, ap: 160,
|
||||
skills: { 6015: { uuid: 6015, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal2].cd, ccd: 0 } },
|
||||
atking: {
|
||||
6302: [
|
||||
@@ -801,63 +801,63 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
// 基础怪物 (全部固定点位站桩攻击,HType仅决定是前排还是后排)
|
||||
// 前排怪物 (站在前排,承受更多伤害) — v5: TD节奏CD,多而弱爽感设计
|
||||
6001: {
|
||||
uuid: 6001, name: "兽人战士", path: "m1", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Melee, hp: 220, ap: 10, speed: 70,
|
||||
uuid: 6001, name: "兽人战士", path: "m1", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Melee, hp: 286, ap: 10, speed: 70,
|
||||
skills: { 6005: { uuid: 6005, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow1].cd, ccd: 0 } }, info: "基础前排怪"
|
||||
},
|
||||
6002: {
|
||||
uuid: 6002, name: "兽人精锐战士", path: "m2", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Melee, hp: 300, ap: 14, speed: 110,
|
||||
uuid: 6002, name: "兽人精锐战士", path: "m2", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Melee, hp: 390, ap: 14, speed: 110,
|
||||
skills: { 6005: { uuid: 6005, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow2].cd, ccd: 0 } }, info: "进阶前排怪, 更快更痛"
|
||||
},
|
||||
6003: {
|
||||
uuid: 6003, name: "兽人重装兵", path: "m3", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Heavy, hp: 850, ap: 20, speed: 50,
|
||||
uuid: 6003, name: "兽人重装兵", path: "m3", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.Heavy, hp: 1105, ap: 20, speed: 50,
|
||||
skills: { 6005: { uuid: 6005, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow3].cd, ccd: 0 } }, info: "重型坦克怪, 高HP慢攻"
|
||||
},
|
||||
// 后排怪物 (站在后排,输出更高)
|
||||
6004: {
|
||||
uuid: 6004, name: "兽人射手", path: "m4", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Long, hp: 190, ap: 35, speed: 70,
|
||||
uuid: 6004, name: "兽人射手", path: "m4", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Long, hp: 247, ap: 35, speed: 70,
|
||||
skills: { 6008: { uuid: 6008, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow1].cd, ccd: 0 } }, info: "后排高DPS怪"
|
||||
},
|
||||
6005: {
|
||||
uuid: 6005, name: "兽人刺客", path: "m5", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Assassin, hp: 210, ap: 38, speed: 130,
|
||||
uuid: 6005, name: "兽人刺客", path: "m5", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Assassin, hp: 273, ap: 38, speed: 130,
|
||||
skills: { 6103: { uuid: 6103, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow2].cd, ccd: 0 } }, info: "高AP快速攻击刺客"
|
||||
},
|
||||
// 特殊位怪物
|
||||
6006: {
|
||||
uuid: 6006, name: "骷髅领主", path: "m6", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.MeleeBoss, hp: 5000, ap: 20, speed: 60,
|
||||
uuid: 6006, name: "骷髅领主", path: "m6", fac: FacSet.MON, lv: 1, type: HType.Melee, monType: MonType.MeleeBoss, hp: 6500, ap: 20, speed: 60,
|
||||
skills: { 6005: { uuid: 6005, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow3].cd, ccd: 0 } }, info: "前排MiniBoss级坦克"
|
||||
},
|
||||
6007: {
|
||||
uuid: 6007, name: "兽人术士", path: "m7", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Support, hp: 300, ap: 24, speed: 70,
|
||||
uuid: 6007, name: "兽人术士", path: "m7", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Support, hp: 390, ap: 24, speed: 70,
|
||||
skills: { 6103: { uuid: 6103, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow1].cd, ccd: 0 } }, info: "后排法师怪, 魔法攻击"
|
||||
},
|
||||
6008: {
|
||||
uuid: 6008, name: "兽人火法", path: "m8", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Summoner, hp: 270, ap: 32, speed: 70,
|
||||
uuid: 6008, name: "兽人火法", path: "m8", fac: FacSet.MON, lv: 1, type: HType.Long, monType: MonType.Summoner, hp: 351, ap: 32, speed: 70,
|
||||
skills: { 6103: { uuid: 6103, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow2].cd, ccd: 0 } }, info: "后排高输出法师怪"
|
||||
},
|
||||
|
||||
// BOSS怪物 — Boss节奏1.2-1.5s,删除不存在的6206技能
|
||||
6101: {
|
||||
uuid: 6101, name: "兽人首领-双刀战士", path: "mb1", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 1900, ap: 30, speed: 120,
|
||||
uuid: 6101, name: "兽人首领-双刀战士", path: "mb1", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 2470, ap: 30, speed: 120,
|
||||
skills: { 6103: { uuid: 6103, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow3].cd, ccd: 0 } }, info: "前排Boss, 高攻速"
|
||||
},
|
||||
6102: {
|
||||
uuid: 6102, name: "兽人首领-斧头战士", path: "mb2", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 7500, ap: 26, speed: 60,
|
||||
uuid: 6102, name: "兽人首领-斧头战士", path: "mb2", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 9750, ap: 26, speed: 60,
|
||||
skills: { 6005: { uuid: 6005, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow1].cd, ccd: 0 } }, info: "前排Boss, 超高HP"
|
||||
},
|
||||
6103: {
|
||||
uuid: 6103, name: "兽人首领-魔法师", path: "mb3", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 2250, ap: 38, speed: 110,
|
||||
uuid: 6103, name: "兽人首领-魔法师", path: "mb3", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 2925, ap: 38, speed: 110,
|
||||
skills: { 6103: { uuid: 6103, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow2].cd, ccd: 0 } }, info: "后排法系Boss, 高AP"
|
||||
},
|
||||
6104: {
|
||||
uuid: 6104, name: "兽人首领-射手", path: "mb4", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 6800, ap: 30, speed: 70,
|
||||
uuid: 6104, name: "兽人首领-射手", path: "mb4", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 8840, ap: 30, speed: 70,
|
||||
skills: { 6005: { uuid: 6005, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow3].cd, ccd: 0 } }, info: "后排位Boss, 均衡型"
|
||||
},
|
||||
6105: {
|
||||
uuid: 6105, name: "亡灵首领-法师", path: "mb5", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 2600, ap: 42, speed: 110,
|
||||
uuid: 6105, name: "亡灵首领-法师", path: "mb5", fac: FacSet.MON, lv: 6, type: HType.Long, monType: MonType.LongBoss, hp: 3380, ap: 42, speed: 110,
|
||||
skills: { 6103: { uuid: 6103, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow1].cd, ccd: 0 } }, info: "后排高AP Boss"
|
||||
},
|
||||
6106: {
|
||||
uuid: 6106, name: "亡灵首领-骑马战士", path: "mb6", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 9000, ap: 26, speed: 130,
|
||||
uuid: 6106, name: "亡灵首领-骑马战士", path: "mb6", fac: FacSet.MON, lv: 6, type: HType.Melee, monType: MonType.MeleeBoss, hp: 11700, ap: 26, speed: 130,
|
||||
skills: { 6005: { uuid: 6005, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow3].cd, ccd: 0 } }, info: "前排终极Boss, 最高HP+高速"
|
||||
},
|
||||
|
||||
|
||||
@@ -201,8 +201,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* 近战交战:有怪物时向右推进,贴近到固定接触距离后停下攻击。
|
||||
* Why: 近战手短需主动逼近;停止判定用固定接触距离(非攻击距离 dis),
|
||||
* 近战交战:怪物进入警戒线(ALERT_RANGE_X)内才向右推进迎敌,
|
||||
* 贴近到固定接触距离后停下攻击;警戒线外则退回后方编队位待命。
|
||||
* Why: 近战手短需主动逼近,但无威胁时不应孤军前压暴露阵型;
|
||||
* 停止判定用固定接触距离(非攻击距离 dis),
|
||||
* 使英雄进入攻击范围后仍继续贴近,直到贴上目标才站稳输出。
|
||||
* 怪物 y 与英雄同在 GAME_LINE 附近(微调也在攻击范围内),无需管 Y 轴。
|
||||
*/
|
||||
@@ -220,7 +222,14 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
if (view.status !== "atk") view.status_change("idle");
|
||||
return;
|
||||
}
|
||||
// 未贴近:向怪物方向推进(攻击范围内也继续移动),于接触距离处停下
|
||||
// 怪物在警戒线之外:不主动迎敌,退回后方编队位待命
|
||||
if (dist > MoveSystem.MELEE_ALERT_X) {
|
||||
this.clearCombatTarget(model);
|
||||
this.moveToSlot(view, move, model, move.targetX);
|
||||
model.is_atking = false;
|
||||
return;
|
||||
}
|
||||
// 警戒线内未贴近:向怪物方向推进(攻击范围内也继续移动),于接触距离处停下
|
||||
const dir = enemyX > selfX ? 1 : -1;
|
||||
move.direction = dir;
|
||||
// 终止点取接触距离与推进上限的较小者,防止越过 150 继续追击
|
||||
@@ -293,6 +302,8 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
private static readonly FORMATION_STEP_X = 60;
|
||||
/** 近战贴近怪物的固定接触距离:小于攻击距离 dis,使英雄进攻击范围后仍继续贴近到此间距才停 */
|
||||
private static readonly MELEE_ENGAGE_X = 60;
|
||||
/** 近战警戒线:怪物进入该距离内才主动向右推进迎敌,否则退回后方编队位待命 */
|
||||
private static readonly MELEE_ALERT_X = 200;
|
||||
/** 英雄向敌人推进的最远终止点 X,防止近战追敌时无限右移脱离战场 */
|
||||
private static readonly HERO_ADVANCE_LIMIT_X = 100;
|
||||
/** 同阵营英雄间最小间距:小于该值时低优先级一方需让位 */
|
||||
|
||||
@@ -192,13 +192,10 @@ export class MissionCardComp extends CCComp {
|
||||
private itemCardComps: ItemListComp[] = [];
|
||||
/** 是否已召唤过英雄(用于控制其他面板按钮是否可点击) */
|
||||
private hasCalledHero: boolean = false;
|
||||
/**
|
||||
* 招募计数:每次点击招募 +1。
|
||||
* 节奏为「每 4 次一循环」:前 3 次直接随机召唤,第 4 次走三选一。
|
||||
*/
|
||||
private recruitCount: number = 0;
|
||||
/** 全远程阵容时近战卡的权重放大倍数(近战保底) */
|
||||
private static readonly MELEE_PITY_WEIGHT_MUL = 3;
|
||||
/** 开局免费连续三选一的总次数 */
|
||||
private static readonly OPENING_FREE_PICK_COUNT = 3;
|
||||
/** 开局免费三选一剩余次数(>0 时招募不扣费,选完 1 张自动弹出下一组) */
|
||||
private openingFreePicks: number = 0;
|
||||
|
||||
// ======================== 面板划出状态 ========================
|
||||
|
||||
@@ -362,6 +359,10 @@ export class MissionCardComp extends CCComp {
|
||||
// 暂不启用 herobox 同步显示英雄信息(后续可能恢复),保留代码备查
|
||||
// this.refreshHeroBoxSlots();
|
||||
|
||||
// 重置开局免费三选一次数,随后立即弹出第 1 组三选一
|
||||
this.openingFreePicks = MissionCardComp.OPENING_FREE_PICK_COUNT;
|
||||
this.showCardsPanel();
|
||||
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "mission start");
|
||||
}
|
||||
|
||||
@@ -876,8 +877,18 @@ export class MissionCardComp extends CCComp {
|
||||
if (heroIdx >= 0) {
|
||||
const cards = this.buildDrawCards();
|
||||
this.dispatchCardsToSlots(cards);
|
||||
// 开局免费三选一次数未用完时保持面板展开,连续弹出下一组三选一
|
||||
if (this.openingFreePicks > 0) {
|
||||
this.openingFreePicks--;
|
||||
if (this.openingFreePicks <= 0) {
|
||||
// 最后一组选完,免费次数耗尽,收起面板并通知自动进入战斗
|
||||
this.hideCardsPanel();
|
||||
oops.message.dispatchEvent("OpeningPicksDone");
|
||||
}
|
||||
} else {
|
||||
// 抽 1 张后关闭英雄卡池面板
|
||||
this.hideCardsPanel();
|
||||
}
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "refresh hero pool after buy", {
|
||||
triggerSlot: heroIdx
|
||||
});
|
||||
@@ -1226,40 +1237,22 @@ export class MissionCardComp extends CCComp {
|
||||
}
|
||||
|
||||
/**
|
||||
* 英雄招募按钮(每 4 次一循环):
|
||||
* - 前 3 次:扣面板开启费后直接随机召唤 1 个英雄(不开面板,无需玩家选)。
|
||||
* - 第 4 次:扣费后展开三选一面板(缩放 0→1),玩家选 1。
|
||||
* 英雄招募按钮:每次都展开三选一面板,玩家选 1;
|
||||
* 开局免费三选一次数未用完时不扣面板开启费。
|
||||
* 满员(HERO_MAX_NUM)时气泡提示并拦截,不扣费、不计数。
|
||||
*/
|
||||
private onShowHerosClick() {
|
||||
oops.audio.playEffect("music/button");
|
||||
// 满员拦截:随机招募与三选一都禁止
|
||||
// 满员拦截
|
||||
if (this.getAliveHeroCount() >= this.getMissionHeroMaxNum()) {
|
||||
this.showSmallTip("panel_hero_full", "英雄槽已满");
|
||||
return;
|
||||
}
|
||||
// 随机招募与三选一都需支付面板开启费,金币不足则提示并中止
|
||||
if (!this.tryPayPanelOpenCost(0)) return;
|
||||
|
||||
// 先计数,再按节奏分支:count % 4 == 0 展开三选一,其余直接随机召唤
|
||||
this.recruitCount++;
|
||||
const isPickOneOfThree = (this.recruitCount % 4) === 0;
|
||||
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "hero recruit", {
|
||||
recruitCount: this.recruitCount,
|
||||
mode: isPickOneOfThree ? "pick_one_of_three" : "random_summon"
|
||||
});
|
||||
|
||||
// 收起装备/技能/药品覆盖面板,回到英雄面板
|
||||
// 开局免费次数内不扣面板开启费
|
||||
if (this.openingFreePicks <= 0 && !this.tryPayPanelOpenCost(0)) return;
|
||||
this.slideToPanel(0);
|
||||
|
||||
if (isPickOneOfThree) {
|
||||
// 第 4 次:展开英雄抽卡面板(缩放 0→1,点亮按钮 active 高亮)
|
||||
this.showCardsPanel();
|
||||
} else {
|
||||
// 前 3 次:直接随机召唤,不开面板
|
||||
this.randomSummonOne();
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 英雄信息盒(herosBoxNode) ========================
|
||||
@@ -1812,60 +1805,11 @@ export class MissionCardComp extends CCComp {
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接随机召唤 1 个英雄:
|
||||
* - 全英雄卡池按权重随机(不过滤已召唤英雄,配合二合一合成)。
|
||||
* - 近战保底:场上已有英雄且全部为远程/中程(无近战)时,近战卡权重放大。
|
||||
*/
|
||||
private randomSummonOne() {
|
||||
const pool = this.buildRandomSummonPool();
|
||||
if (pool.length === 0) return;
|
||||
const pick = this.weightedPick(pool);
|
||||
if (!pick) return;
|
||||
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "random summon", { uuid: pick.uuid });
|
||||
oops.message.dispatchEvent(GameEvent.CallHero, {
|
||||
uuid: pick.uuid,
|
||||
hero_lv: 1,
|
||||
card_lv: pick.card_lv ?? 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建随机召唤卡池:全英雄卡,必要时按近战保底放大近战权重。
|
||||
* @returns 带运行时权重的卡配置数组(weight 已按保底调整)
|
||||
* 构建随机召唤卡池:全英雄卡。
|
||||
* @returns 卡配置数组
|
||||
*/
|
||||
private buildRandomSummonPool(): CardConfig[] {
|
||||
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero);
|
||||
if (heroCards.length === 0) return [];
|
||||
|
||||
// 全远程判定:有存活英雄但没有任何近战 → 触发放大近战权重
|
||||
const meleeMul = this.isAllRangedFormation()
|
||||
? MissionCardComp.MELEE_PITY_WEIGHT_MUL
|
||||
: 1;
|
||||
if (meleeMul === 1) return heroCards;
|
||||
|
||||
return heroCards.map(c => {
|
||||
const hero = HeroInfo[c.uuid];
|
||||
const isMelee = hero && hero.type === HType.Melee;
|
||||
// 复制一份避免污染卡池原始 weight
|
||||
return isMelee ? { ...c, weight: (c.weight ?? 0) * meleeMul } : c;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前场上是否为「全远程阵容」:
|
||||
* 有至少 1 个存活英雄,且没有任何近战(Melee)英雄。
|
||||
*/
|
||||
private isAllRangedFormation(): boolean {
|
||||
let heroCount = 0;
|
||||
let meleeCount = 0;
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||||
const model = entity.get(HeroAttrsComp);
|
||||
if (!model || model.fac !== FacSet.HERO || model.is_dead) return;
|
||||
heroCount++;
|
||||
if (model.type === HType.Melee) meleeCount++;
|
||||
});
|
||||
return heroCount > 0 && meleeCount === 0;
|
||||
return CardPoolList.filter(c => c.type === CardType.Hero);
|
||||
}
|
||||
|
||||
// ======================== 阶段切换 ========================
|
||||
|
||||
@@ -48,7 +48,7 @@ import { Tooltip } from "../skill/Tooltip";
|
||||
import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
||||
import { FieldSkillType } from "../common/config/SkillSet";
|
||||
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
||||
import { spawningEngine, MAX_WAVE, DynamicTuner, WAVE_TIMEOUT, WAVE_TIMEOUT_BOSS, BATCH_INTERVAL, BATCH_COUNT, WAVE_DURATION } from "./RogueConfig";
|
||||
import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 任务(关卡)生命周期阶段 */
|
||||
@@ -82,11 +82,6 @@ export class MissionComp extends CCComp {
|
||||
|
||||
// ======================== 配置参数 ========================
|
||||
|
||||
/** 怪物数量上限(超过后暂停刷怪):略高于单回合上限 MAX_MONSTERS(54),只有跨回合堆积才触发泄压 */
|
||||
private maxMonsterCount: number = 60;
|
||||
/** 怪物数量恢复阈值(降至此值以下恢复刷怪,需留空间接纳后续批次,单批最大约 18 只) */
|
||||
private resumeMonsterCount: number = 40;
|
||||
|
||||
// ======================== 编辑器绑定节点 ========================
|
||||
|
||||
/** 开始战斗按钮 */
|
||||
@@ -196,8 +191,8 @@ export class MissionComp extends CCComp {
|
||||
this.on(GameEvent.NewWave, this.onNewWave, this)
|
||||
this.on(GameEvent.DO_AD_BACK, this.do_ad, this)
|
||||
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||||
// 第一回合准备阶段,首个英雄召唤后自动开始战斗(无需玩家点击开始按钮)
|
||||
oops.message.on(GameEvent.MasterCalled, this.onHeroSummoned, this);
|
||||
// 开局免费三选一选完后自动进入战斗
|
||||
oops.message.on("OpeningPicksDone", this.onOpeningPicksDone, this);
|
||||
this.removeMemoryPanel()
|
||||
}
|
||||
|
||||
@@ -219,7 +214,7 @@ export class MissionComp extends CCComp {
|
||||
if (this.start_btn && this.start_btn.isValid) {
|
||||
this.start_btn.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this);
|
||||
}
|
||||
oops.message.off(GameEvent.MasterCalled, this.onHeroSummoned, this);
|
||||
oops.message.off("OpeningPicksDone", this.onOpeningPicksDone, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,9 +254,8 @@ export class MissionComp extends CCComp {
|
||||
this.clearTime += dt
|
||||
this.update_time();
|
||||
|
||||
// 回合超时兜底:僵局(坦克 vs 高血 Boss 互磨不死)时强制收束回合,Boss 回合阈值放宽
|
||||
const timeout = this.isBossWave ? WAVE_TIMEOUT_BOSS : WAVE_TIMEOUT;
|
||||
if (this.clearTime >= timeout) {
|
||||
// 战斗超时兜底:僵局(坦克 vs 高血 Boss 互磨不死)时强制结算整局
|
||||
if (this.clearTime >= BATTLE_TIMEOUT) {
|
||||
this.onBattleTimeout();
|
||||
}
|
||||
}
|
||||
@@ -522,11 +516,11 @@ export class MissionComp extends CCComp {
|
||||
case MissionPhase.BattleStart:
|
||||
// 触发战斗开始技能(fstart)
|
||||
this.triggerHeroBattleSkills(true);
|
||||
// Boss 回合预警:战斗正式开始、Boss 压轴进场(最后一批),提前给 UI 表现窗口
|
||||
// Boss 预警:Boss 同步场立即登场(eta=0)
|
||||
if (this.isBossWave) {
|
||||
oops.message.dispatchEvent(GameEvent.BossWarning, {
|
||||
wave: this.currentWave,
|
||||
eta: BATCH_INTERVAL * (BATCH_COUNT - 1),
|
||||
eta: 0,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -570,26 +564,17 @@ export class MissionComp extends CCComp {
|
||||
const heroDeathCount = smc.mission.dead_heroes.length;
|
||||
if (heroDeathCount > 0) allAlive = false;
|
||||
|
||||
// 【动态难度调节】根据本回合战况自动放水 / 加压
|
||||
// 清场加速节省的时间还原进口径,避免"打得好"被节奏加速+强度加压双重惩罚
|
||||
const effectiveClearTime = this.clearTime + smc.vmdata.mission_data.wave_early_skip;
|
||||
const tuned = DynamicTuner.adjust(effectiveClearTime, heroDeathCount);
|
||||
if (tuned) {
|
||||
mLogger.log(this.debugMode, 'MissionComp', `[DynamicTuner] wave=${this.currentWave} effClear=${effectiveClearTime.toFixed(1)}s deaths=${heroDeathCount} factor=${DynamicTuner.factor.toFixed(3)}`);
|
||||
}
|
||||
// 快照供下一回合倒计时档位判定(onNewWave 会清零 clearTime,必须提前快照)
|
||||
// 快照供倒计时档位判定(onNewWave 会清零 clearTime,必须提前快照)
|
||||
this.lastWaveDeathCount = heroDeathCount;
|
||||
this.lastWaveClearTime = effectiveClearTime;
|
||||
this.lastWaveClearTime = this.clearTime;
|
||||
// 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分)
|
||||
if (hasHero && allAlive) {
|
||||
smc.vmdata.scores.wave_all_alive_count++;
|
||||
}
|
||||
|
||||
// 【评分系统 - 战绩分】判断是否通过最后一关(第20回合)
|
||||
if (this.currentWave === MAX_WAVE) {
|
||||
// 【评分系统 - 战绩分】单场 Boss 战定胜负,通过即视为通关
|
||||
smc.vmdata.scores.passed_wave_20 = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 触发战斗结束技能(fend)
|
||||
this.triggerHeroBattleSkills(false);
|
||||
@@ -751,13 +736,8 @@ export class MissionComp extends CCComp {
|
||||
this.to_fight();
|
||||
}
|
||||
|
||||
/**
|
||||
* 英雄召唤事件回调:
|
||||
* 第一回合准备阶段(Prepare)只有第 1 回合才会进入,
|
||||
* 此时首个英雄一旦召唤即自动开战,无需玩家点击开始按钮。
|
||||
* 后续回合走 PrepareStart 倒计时自动流转,不会进入 Prepare。
|
||||
*/
|
||||
private onHeroSummoned() {
|
||||
/** 开局免费三选一选完,自动进入战斗(等效于点击开始按钮) */
|
||||
private onOpeningPicksDone() {
|
||||
if (this.currentPhase !== MissionPhase.Prepare) return;
|
||||
if (!smc.mission.play || smc.mission.pause) return;
|
||||
this.to_fight();
|
||||
@@ -877,7 +857,7 @@ export class MissionComp extends CCComp {
|
||||
const wave = Number(data?.wave ?? 0);
|
||||
if (wave <= 0) return;
|
||||
|
||||
this.isBossWave = !!data?.bossWave;
|
||||
this.isBossWave = true; // 单场 Boss 战,恒为 Boss 战(超时阈值/预警走 Boss 档)
|
||||
|
||||
if (wave > 1) {
|
||||
// 在新一回合到来时,先进入 BattleEnd,触发上一回合的战斗结束技能 (fend),2秒后自动进入下一回合的准备阶段
|
||||
@@ -901,21 +881,10 @@ export class MissionComp extends CCComp {
|
||||
|
||||
// ======================== 怪物数量管理 ========================
|
||||
|
||||
/**
|
||||
* 获取怪物数量阈值配置。
|
||||
* @returns { max: 刷怪上限, resume: 恢复刷怪阈值 }
|
||||
*/
|
||||
private getMonsterThresholds(): { max: number; resume: number } {
|
||||
const max = Math.max(1, Math.floor(this.maxMonsterCount));
|
||||
const resume = Math.min(max - 1, Math.max(0, Math.floor(this.resumeMonsterCount)));
|
||||
return { max, resume };
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步怪物刷新状态(降频执行,每 0.2 秒一次):
|
||||
* 1. 遍历所有 HeroAttrsComp 实体,统计怪物和英雄数量。
|
||||
* 2. 检测英雄全灭。
|
||||
* 3. 根据 max/resume 阈值切换 stop_spawn_mon 状态。
|
||||
* 2. 检测英雄全灭 / 怪物全灭。
|
||||
*
|
||||
* @param dt 帧间隔
|
||||
*/
|
||||
@@ -938,14 +907,10 @@ export class MissionComp extends CCComp {
|
||||
});
|
||||
this.handleHeroWipe(heroCount);
|
||||
|
||||
// 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,且待刷新的怪物队列也为空,直接结束战斗进入下一回合的准备阶段
|
||||
// 怪物全灭检测:一场定胜负,全灭即整局通关,直接进入结算
|
||||
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
|
||||
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
|
||||
if (this.currentWave >= MAX_WAVE) {
|
||||
// 20 回合通关
|
||||
this.open_Victory(null, false);
|
||||
} else {
|
||||
// oops.message 全局事件总线:清屏庆祝(横幅/奖励由 BattleBannerComp 消费),需在推进回合前派发(clearTime 归零前取值)
|
||||
// oops.message 全局事件总线:清屏庆祝(横幅/奖励由 BattleBannerComp 消费),需在结算前派发(clearTime 归零前取值)
|
||||
const allAlive = this.checkAllHeroAlive();
|
||||
const fastClear = this.clearTime < WAVE_DURATION * 0.5 ? 2
|
||||
: this.clearTime < WAVE_DURATION * 0.75 ? 1 : 0;
|
||||
@@ -955,22 +920,11 @@ export class MissionComp extends CCComp {
|
||||
allAlive,
|
||||
fastClear,
|
||||
});
|
||||
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
||||
}
|
||||
this.open_Victory(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
smc.vmdata.mission_data.mon_num = monsterCount;
|
||||
const { max, resume } = this.getMonsterThresholds();
|
||||
smc.vmdata.mission_data.mon_max = max;
|
||||
const stopSpawn = !!smc.mission.stop_spawn_mon;
|
||||
if (stopSpawn) {
|
||||
// 降至恢复阈值以下 → 恢复刷怪
|
||||
if (monsterCount <= resume) smc.mission.stop_spawn_mon = false;
|
||||
return;
|
||||
}
|
||||
// 超过上限 → 暂停刷怪
|
||||
if (monsterCount >= max) smc.mission.stop_spawn_mon = true;
|
||||
}
|
||||
|
||||
/** 检测场上英雄是否全员存活(清屏 Perfect 判定用,独立于评分统计逻辑) */
|
||||
@@ -998,13 +952,13 @@ export class MissionComp extends CCComp {
|
||||
}
|
||||
|
||||
/**
|
||||
* 回合超时强制结束(僵局兜底):
|
||||
* 战斗超时强制结束(僵局兜底):
|
||||
* 1. 预扣留存分(残留怪销毁后 mon_num 会被下一次同步清零,BattleEnd 的累加会漏扣,故在此显式预扣并置防重标志)。
|
||||
* 2. 销毁全部残留怪(不触发 MonDead、不掉金币——超时是对"清不掉"的惩罚)。
|
||||
* 3. 复用正常回合推进流;DynamicTuner 因 clearTime 远超阈值会自动放水,无需特殊处理。
|
||||
* 3. 一场定胜负,超时按怪物全灭同等处理——直接结算整局。
|
||||
*/
|
||||
private onBattleTimeout() {
|
||||
// 防重入:同一回合只触发一次
|
||||
// 防重入:只触发一次
|
||||
if (this.currentPhase !== MissionPhase.Battle) return;
|
||||
|
||||
smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num;
|
||||
@@ -1018,13 +972,9 @@ export class MissionComp extends CCComp {
|
||||
});
|
||||
smc.vmdata.mission_data.mon_num = 0;
|
||||
|
||||
mLogger.log(this.debugMode, 'MissionComp', `[WaveTimeout] wave=${this.currentWave} 超时强制结束`);
|
||||
mLogger.log(this.debugMode, 'MissionComp', `[BattleTimeout] 超时强制结算`);
|
||||
|
||||
if (this.currentWave >= MAX_WAVE) {
|
||||
this.open_Victory(null, false);
|
||||
} else {
|
||||
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 清理 ========================
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
/**
|
||||
* @file MissionMonComp.ts
|
||||
* @description 怪物(Monster)回合刷新管理组件(逻辑层)
|
||||
* @description 怪物(Monster)登场管理组件(逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理每一回合怪物的生成计划:根据 RogueConfig 生成怪物。
|
||||
* 2. 自动推进回合:在准备阶段结束时(PhasePrepareEnd)启动分批释放。
|
||||
* 3. 持续刷怪:回合开始整波进入队列,30 秒内按速度曲线匀速刷出(铺垫 → 加压 → 高潮)。
|
||||
* 1. 预生成战斗怪物:FightReady 时通过 RogueSpawningEngine.generateBossCombo 随机一组 Boss 组合,并立即登场展示(供玩家侦查后再选英雄)。
|
||||
* 2. 战斗开始:PhasePrepareEnd 时恢复怪物行动(in_fight 前怪物仅原地待命,不会推进/索敌)。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 所有怪物统一从右侧 X=400 出生点刷出,速度曲线由 m.batch 驱动(前慢后快,压轴批自动高潮)。
|
||||
* - 每帧欠账式补刷(期望数 − 已刷数),自动补偿帧余量,保证 30 秒预算不流失。
|
||||
* - 场上怪物超阈值(stop_spawn_mon)时暂停推进,恢复后瞬时补齐欠账。
|
||||
* - 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场,玩家看清阵容后三选一。
|
||||
* - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。
|
||||
* - 槽位索引(3行×4列)仅用于 monGrid 寻路与 SCastSystem 索敌定位。
|
||||
* - 上一回合残留怪在回合结束/开始时统一清理。
|
||||
* - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。
|
||||
*/
|
||||
import { _decorator, v3, Vec3 } from "cc";
|
||||
import { mLogger } from "../common/Logger";
|
||||
@@ -23,8 +19,8 @@ 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, FacSet } from "../common/config/GameSet";
|
||||
import { spawningEngine, GeneratedMonster, TestModeConfig, WAVE_DURATION } from "./RogueConfig";
|
||||
import { BoxSet } from "../common/config/GameSet";
|
||||
import { spawningEngine, GeneratedMonster, TestModeConfig, BOSS_COUNT, MINION_COUNT } from "./RogueConfig";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { MonMoveComp } from "../hero/MonMoveComp";
|
||||
|
||||
@@ -37,28 +33,25 @@ export class MissionMonCompComp extends CCComp {
|
||||
|
||||
/** 怪物出生掉落高度 */
|
||||
private static readonly MON_DROP_HEIGHT = 0;
|
||||
/** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */
|
||||
private static readonly MON_SPAWN_X = 400;
|
||||
/** 怪物登场 X 范围下限(靠近英雄侧) */
|
||||
private static readonly SPAWN_X_MIN = 100;
|
||||
/** 怪物登场 X 范围上限(靠近右侧) */
|
||||
private static readonly SPAWN_X_MAX = 320;
|
||||
|
||||
/**
|
||||
* 怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标,
|
||||
* 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。
|
||||
* 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。
|
||||
* 超出 12 个槽位的怪物(放松回合)按 spawnIndex % 12 复用槽位。
|
||||
*/
|
||||
/** 槽位基准 Y(3行×4列结构仅供 monGrid 寻路与 SCastSystem 索敌定位) */
|
||||
public static readonly MON_POSITIONS: Vec3[] = [
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 0: Col1-Top
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 1: Col1-Mid
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 2: Col1-Bot
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 3: Col2-Top
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 4: Col2-Mid
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 5: Col2-Bot
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 6: Col3-Top
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 7: Col3-Mid
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 8: Col3-Bot
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 9: Col4-Top
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 10: Col4-Mid
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 11: Col4-Bot
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 0: Col1-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 1: Col1-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 2: Col1-Bot
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 3: Col2-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 4: Col2-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 5: Col2-Bot
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 6: Col3-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 7: Col3-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 8: Col3-Bot
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 9: Col4-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 10: Col4-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 11: Col4-Bot
|
||||
];
|
||||
|
||||
// ======================== 编辑器属性 ========================
|
||||
@@ -70,107 +63,30 @@ export class MissionMonCompComp extends CCComp {
|
||||
|
||||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||||
private globalSpawnOrder: number = 0;
|
||||
/** 当前回合数 */
|
||||
private currentWave: number = 0;
|
||||
/** 当前回合的目标怪物总数 */
|
||||
/** 战斗场次(本局恒为 1,保留供 HUD/事件协议) */
|
||||
private currentWave: number = 1;
|
||||
/** 本场战斗的怪物总数 */
|
||||
private waveTargetCount: number = 0;
|
||||
/** 当前回合已生成的怪物数量 */
|
||||
/** 已登场的怪物数量 */
|
||||
private waveSpawnedCount: number = 0;
|
||||
/** 待刷队列(回合开始时整波进入,按速度曲线匀速刷出) */
|
||||
/** 待登场队列(FightReady 时整组进入,PhasePrepareEnd 时一次全量刷出) */
|
||||
private spawnQueue: GeneratedMonster[] = [];
|
||||
/** 本回合已推进的战斗时间(秒,暂停刷怪时冻结) */
|
||||
private waveTime: number = 0;
|
||||
/** 各批次在队列中的起始下标(用于速度曲线按批变速) */
|
||||
private batchStarts: number[] = [0, 0, 0];
|
||||
/** 是否正在刷怪释放中 */
|
||||
private isReleasing: boolean = false;
|
||||
/** 当前批已刷出的怪物数(清场加速存活比例分母) */
|
||||
private batchReleasedCount: number = 0;
|
||||
/** 当前批是否已通过清场加速提前推进过(每批只触发一次) */
|
||||
private batchFastForwarded: boolean = false;
|
||||
/** 清场加速检测节流计时器(秒) */
|
||||
private aliveCheckTimer: number = 0;
|
||||
/** 本回合因清场加速累计节省的秒数(供 MissionComp 还原 DDA 判定口径,避免双重惩罚) */
|
||||
public waveEarlySkipTotal: number = 0;
|
||||
/** 清场加速触发阈值:当前批存活比例低于此值时提前推进(0.25 = 清掉 75%) */
|
||||
private static readonly BATCH_EARLY_RATIO = 0.25;
|
||||
/** 清场加速提前量(秒):等效推进时间快进,保底不被瞬间叠爆 */
|
||||
private static readonly BATCH_EARLY_SKIP = 2.0;
|
||||
/** 批次时间边界(秒):batch0 → [0, T1),batch1 → [T1, T2),batch2 → [T2, WAVE_DURATION] */
|
||||
private static readonly BATCH_T1 = WAVE_DURATION * 0.25;
|
||||
private static readonly BATCH_T2 = WAVE_DURATION * 0.60;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad() {
|
||||
this.on(GameEvent.FightReady, this.fight_ready, this);
|
||||
this.on("PhasePrepareEnd", this.onPhasePrepareEnd, this);
|
||||
this.on("TimeUpAdvanceWave", this.onTimeUpAdvanceWave, this);
|
||||
}
|
||||
|
||||
protected update(dt: number): void {
|
||||
// 待刷出统计(回合结束检测与 HUD 进度消费,必须在 return 前执行保证语义不受暂停影响)
|
||||
// 待登场统计(战斗结束检测与 HUD 进度消费)
|
||||
smc.vmdata.mission_data.pending_mon_num = this.spawnQueue.length;
|
||||
|
||||
// 场上怪物超阈值时暂停推进:冻结 waveTime,恢复后欠账式瞬时补齐,30 秒预算不流失
|
||||
if (smc.mission.stop_spawn_mon) return;
|
||||
if (!this.isReleasing) return;
|
||||
|
||||
this.waveTime += dt;
|
||||
|
||||
// 清场加速:0.2s 节流检测当前段存活比例,清得快则快进推进时间(压缩垃圾时间)
|
||||
this.aliveCheckTimer += dt;
|
||||
if (this.aliveCheckTimer >= 0.2) {
|
||||
this.aliveCheckTimer = 0;
|
||||
this.checkEarlyAdvance();
|
||||
}
|
||||
|
||||
// 欠账式补刷:期望累计刷出数 − 已刷数,自动补偿帧余量与暂停期间欠账
|
||||
let expected = this.expectedSpawned(this.waveTime);
|
||||
while (this.waveSpawnedCount < expected && this.spawnQueue.length > 0) {
|
||||
const monData = this.spawnQueue.shift()!;
|
||||
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MON_POSITIONS.length;
|
||||
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
|
||||
this.waveSpawnedCount++;
|
||||
}
|
||||
if (this.spawnQueue.length === 0) {
|
||||
this.isReleasing = false;
|
||||
}
|
||||
}
|
||||
|
||||
start() { }
|
||||
|
||||
private setupWaveData(monsters: GeneratedMonster[]) {
|
||||
// 按批次排序后整波入队,记录各批起始下标供速度曲线按批变速(batch 越大越靠后压轴)
|
||||
const sorted = monsters.slice().sort((a, b) => a.batch - b.batch);
|
||||
this.spawnQueue = sorted;
|
||||
const starts = [sorted.length, sorted.length, sorted.length];
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const b = Math.min(sorted[i].batch, 2);
|
||||
if (i < starts[b]) starts[b] = i;
|
||||
}
|
||||
// 空批起始下标前推到下一批起点(防 expectedSpawned 区间错乱)
|
||||
for (let b = 1; b >= 0; b--) {
|
||||
if (starts[b] === sorted.length) starts[b] = starts[b + 1];
|
||||
}
|
||||
this.batchStarts = starts;
|
||||
|
||||
this.waveTargetCount = monsters.length;
|
||||
smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount;
|
||||
|
||||
let hasBoss = monsters.some(m => m.isBoss);
|
||||
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 回合 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`);
|
||||
|
||||
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||||
wave: this.currentWave,
|
||||
total: this.waveTargetCount,
|
||||
bossWave: hasBoss,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 战斗准备:重置所有运行时状态并开始第一回合。
|
||||
* 战斗准备:重置运行时状态并预生成本场 Boss 组合。
|
||||
*/
|
||||
fight_ready() {
|
||||
smc.vmdata.mission_data.mon_num = 0;
|
||||
@@ -180,133 +96,39 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.waveTargetCount = 0;
|
||||
this.waveSpawnedCount = 0;
|
||||
this.spawnQueue = [];
|
||||
this.batchStarts = [0, 0, 0];
|
||||
this.waveTime = 0;
|
||||
this.isReleasing = false;
|
||||
this.batchReleasedCount = 0;
|
||||
this.batchFastForwarded = false;
|
||||
this.aliveCheckTimer = 0;
|
||||
this.waveEarlySkipTotal = 0;
|
||||
|
||||
// 预生成第一回合数据以获取数量和 Boss 信息
|
||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||
this.setupWaveData(monsters);
|
||||
// 随机一组 Boss 组合(最终 Boss 战强度)
|
||||
const monsters = spawningEngine.generateBossCombo();
|
||||
this.spawnQueue = monsters;
|
||||
this.waveTargetCount = monsters.length;
|
||||
smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount;
|
||||
|
||||
// 怪物先行登场:玩家侦查 Boss 组合后再做英雄三选一(in_fight 前怪物原地待命,不推进不索敌)
|
||||
const squad = this.spawnQueue;
|
||||
this.spawnQueue = [];
|
||||
smc.vmdata.mission_data.pending_mon_num = 0;
|
||||
for (const monData of squad) {
|
||||
this.addMonsterAtGrid(this.waveSpawnedCount, monData, this.currentWave);
|
||||
this.waveSpawnedCount++;
|
||||
}
|
||||
|
||||
if (TestModeConfig.enable) {
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启");
|
||||
}
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] Boss 组合生成完成,怪物总数: ${this.waveTargetCount}`);
|
||||
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
|
||||
}
|
||||
|
||||
// ======================== 回合管理 ========================
|
||||
|
||||
/**
|
||||
* 开始下一回合:回合数 +1 并预生成数据
|
||||
*/
|
||||
private onTimeUpAdvanceWave() {
|
||||
this.currentWave += 1;
|
||||
smc.vmdata.mission_data.level = this.currentWave;
|
||||
|
||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||
this.setupWaveData(monsters);
|
||||
}
|
||||
|
||||
private onPhasePrepareEnd() {
|
||||
// 准备结束:清残留怪与计时,启动持续刷怪(队列已在 setupWaveData 整波入队,不能清)
|
||||
this.resetBattleTimer();
|
||||
this.isReleasing = true;
|
||||
this.waveTime = 0;
|
||||
}
|
||||
|
||||
// ======================== 持续刷怪 ========================
|
||||
|
||||
/**
|
||||
* 速度曲线:按 waveTime 所在时间段返回"到当前时刻应累计刷出的怪物数"。
|
||||
* 三段时间对应三批(铺垫 → 加压 → 高潮),批内匀速;m.batch 只影响排序,不再切断节奏。
|
||||
*/
|
||||
private expectedSpawned(t: number): number {
|
||||
const total = this.waveTargetCount;
|
||||
if (total <= 0) return 0;
|
||||
const [s0, s1] = [this.batchStarts[0], this.batchStarts[1]];
|
||||
const s2 = this.batchStarts[2];
|
||||
const T1 = MissionMonCompComp.BATCH_T1;
|
||||
const T2 = MissionMonCompComp.BATCH_T2;
|
||||
if (t < T1) {
|
||||
const segTotal = s1 - s0;
|
||||
const segTime = T1;
|
||||
return s0 + Math.min(segTotal, Math.floor(t / segTime * segTotal));
|
||||
} else if (t < T2) {
|
||||
const segTotal = s2 - s1;
|
||||
const segTime = T2 - T1;
|
||||
return s1 + Math.min(segTotal, Math.floor((t - T1) / segTime * segTotal));
|
||||
} else {
|
||||
const segTotal = total - s2;
|
||||
const segTime = WAVE_DURATION - T2;
|
||||
return s2 + Math.min(segTotal, Math.floor((t - T2) / segTime * segTotal));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清场加速检测:当前时间段已刷完且存活比例 ≤ BATCH_EARLY_RATIO 时,快进推进时间。
|
||||
* 只奖励"清得快"的 build(压缩垃圾时间),弱 build 清不完则不触发、不叠加压力。
|
||||
*/
|
||||
private checkEarlyAdvance() {
|
||||
if (this.batchFastForwarded) return;
|
||||
if (this.spawnQueue.length === 0) return;
|
||||
// 当前时间段的 quota 是否已刷完:用 expectedSpawned 反推
|
||||
const expected = this.expectedSpawned(this.waveTime);
|
||||
if (this.waveSpawnedCount < expected) return; // 本时间段还没放完,不判断
|
||||
if (this.batchReleasedCount <= 0) {
|
||||
this.batchReleasedCount = Math.max(1, expected);
|
||||
}
|
||||
|
||||
let alive = 0;
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
|
||||
const a = e.get(HeroAttrsComp);
|
||||
if (a && a.fac === FacSet.MON && !a.is_dead) alive++;
|
||||
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||||
wave: this.currentWave,
|
||||
total: this.waveTargetCount,
|
||||
bossWave: monsters.some(m => m.isBoss),
|
||||
});
|
||||
// alive 含前几批残留,比例 clamp 防低估
|
||||
const aliveRatio = Math.min(1, alive / this.batchReleasedCount);
|
||||
if (aliveRatio <= MissionMonCompComp.BATCH_EARLY_RATIO) {
|
||||
this.batchFastForwarded = true;
|
||||
this.waveTime += MissionMonCompComp.BATCH_EARLY_SKIP;
|
||||
this.waveEarlySkipTotal += MissionMonCompComp.BATCH_EARLY_SKIP;
|
||||
smc.vmdata.mission_data.wave_early_skip = this.waveEarlySkipTotal;
|
||||
mLogger.log(this.debugMode, 'MissionMonComp',
|
||||
`[EarlyAdvance] waveTime=${this.waveTime.toFixed(1)} alive=${alive}/${this.batchReleasedCount},快进 ${MissionMonCompComp.BATCH_EARLY_SKIP}s`);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 槽位管理 ========================
|
||||
|
||||
/**
|
||||
* 清理上一回合残留怪物,并重置战斗计时与清场加速状态。
|
||||
* 注意:spawnQueue / batchStarts / waveTargetCount 由 setupWaveData 在 Prepare 阶段写入,
|
||||
* 本函数不能触碰,否则 PhasePrepareEnd 会清空未刷队列导致回合直接判空。
|
||||
*/
|
||||
private resetBattleTimer() {
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
|
||||
const attrs = e.get(HeroAttrsComp);
|
||||
if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) {
|
||||
e.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
this.waveSpawnedCount = 0;
|
||||
this.waveTime = 0;
|
||||
this.isReleasing = false;
|
||||
this.batchReleasedCount = 0;
|
||||
this.batchFastForwarded = false;
|
||||
this.aliveCheckTimer = 0;
|
||||
this.waveEarlySkipTotal = 0;
|
||||
smc.vmdata.mission_data.wave_early_skip = 0;
|
||||
}
|
||||
|
||||
// ======================== 怪物生成 ========================
|
||||
|
||||
/**
|
||||
* 在指定位置索引处生成一个怪物
|
||||
* 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场直接开打
|
||||
*/
|
||||
private addMonsterAtGrid(
|
||||
posIndex: number,
|
||||
@@ -317,7 +139,9 @@ export class MissionMonCompComp extends CCComp {
|
||||
let scale = -1;
|
||||
|
||||
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
|
||||
const spawnX = basePos.x;
|
||||
const spawnX = MissionMonCompComp.SPAWN_X_MIN
|
||||
+ (MissionMonCompComp.SPAWN_X_MAX - MissionMonCompComp.SPAWN_X_MIN)
|
||||
* (posIndex / Math.max(1, BOSS_COUNT + MINION_COUNT - 1));
|
||||
const landingY = basePos.y + (monData.isBoss ? 6 : 0);
|
||||
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
|
||||
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
||||
@@ -339,7 +163,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
move.spawnOrder = this.globalSpawnOrder;
|
||||
}
|
||||
|
||||
// 应用新引擎计算好的最终属性
|
||||
// 应用引擎计算好的最终属性
|
||||
const model = mon.get(HeroAttrsComp);
|
||||
if (model) {
|
||||
model.ap = monData.ap;
|
||||
|
||||
@@ -1,203 +1,59 @@
|
||||
/**
|
||||
* @file RogueConfig.ts
|
||||
* @description 肉鸽刷怪系统(基于英雄强度的动态难度 + 心流循环)
|
||||
* @description Boss 组合刷怪系统(单场 Boss 战定胜负)
|
||||
*
|
||||
* 游戏机制:
|
||||
* - 整局只有一场战斗:开局英雄三次三选一后自动开战。
|
||||
* - 怪物从 BossComboLibrary 按权重随机抽一组(1 Boss + 4 小怪),
|
||||
* 战斗开始瞬间一次全量登场(X∈[100,320] 均匀铺开),双方立即互相攻击。
|
||||
* - 怪物全灭 → 胜利结算;英雄全灭 → 失败结算;无回合递进、无分批刷怪。
|
||||
*
|
||||
* 设计层次:
|
||||
* 1. WaveType - 回合类型(普通 / 压力 / 放松),5 回合一个心流循环
|
||||
* 2. WaveConfig - 每回合硬编码(数量 + 小队池 + HP/AP 倍率 + 强度微调)
|
||||
* 3. DynamicTuner - 动态难度调节器(系统根据战况放水 / 加压)
|
||||
* 4. MonSkillSet - 怪物技能池(atking / atked / dead 等全触发类型)
|
||||
* 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
|
||||
* 1. BossComboLibrary - Boss 组合库(小怪构成 + Boss/小怪技能池 + 抽取权重)
|
||||
* 2. MonSkillPool/BossSkillPool - 怪物技能池(atking / atked / dead 等全触发类型)
|
||||
* 3. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
|
||||
*
|
||||
* 核心公式:
|
||||
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
|
||||
* targetPower = heroPower × 回合类型系数 × wave.power_adjust × DynamicTuner.factor
|
||||
* hpScale = targetPower × wave.hp_mul ÷ Σ 怪物基础强度
|
||||
* targetPower = heroPower × BOSS_TYPE_RATIO × FINAL_HP_MUL × MON_POWER_MUL
|
||||
* hpScale = targetPower ÷ Σ 怪物基础强度
|
||||
* 怪物 hp = base_hp × hpScale (走总强度框架,自动匹配玩家)
|
||||
*
|
||||
* 怪物 ap 独立挂钩玩家输出(不走总强度,避免被 hp 挤占导致"涨不动"):
|
||||
* 怪物 ap = 玩家平均攻击 × getApWaveGrowth(wave) × AP_RELATIVE × 类型系数
|
||||
* 回合递进取线性 1.0 → 3.0(wave 1 → 20)
|
||||
* 说明:ap_mul 字段已废弃(保留字段仅为兼容配置表,不再参与计算)
|
||||
*
|
||||
* 回合节奏:
|
||||
* - 最大 20 回合,第 20 回合通关
|
||||
* - 每回合 30 秒,固定分 3 批,每 10 秒释放一批
|
||||
* - 普通回合 36~72 只,放松回合 × 1.5 = 54~108 只(全局 MON_COUNT_MUL = 2)
|
||||
* - wave % 5 === 0 → 压力回合(必带 Boss,强度高、数量少)
|
||||
* - wave % 5 === 4 → 放松回合(数量 × 1.5,强度低,爽快清屏,大战前夜收割补给)
|
||||
* 怪物 ap 独立挂钩玩家输出(不走总强度,避免被 hp 挤占):
|
||||
* 怪物 ap = 玩家平均攻击 × AP_GROWTH_FINAL × AP_RELATIVE × 类型系数
|
||||
*/
|
||||
|
||||
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry, heroInfo } from "../common/config/heroSet";
|
||||
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry } from "../common/config/heroSet";
|
||||
import { SkillOverrides } from "../common/config/SkillSet";
|
||||
import { FacSet } from "../common/config/GameSet";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
|
||||
// ======================== 1. 回合类型与节奏常量 ========================
|
||||
// ======================== 1. 战斗常量 ========================
|
||||
|
||||
/** 回合类型枚举 */
|
||||
export enum WaveType {
|
||||
Normal = 0, // 普通回合
|
||||
Pressure = 1, // 压力回合(wave % 5 === 0,必带 Boss)
|
||||
Relax = 2, // 放松回合(wave % 5 === 4,量大强度低,大战前夜的收割补给)
|
||||
}
|
||||
|
||||
/** 回合类型名称 */
|
||||
export const WaveTypeName: Record<WaveType, string> = {
|
||||
[WaveType.Normal]: "普通",
|
||||
[WaveType.Pressure]: "压力",
|
||||
[WaveType.Relax]: "放松",
|
||||
};
|
||||
|
||||
/** 回合类型强度系数(硬编码) */
|
||||
export const WAVE_TYPE_POWER_RATIO: Record<WaveType, number> = {
|
||||
[WaveType.Normal]: 0.9, // 普通回合:标准强度
|
||||
[WaveType.Pressure]: 1.2, // 压力回合:强度高、数量少
|
||||
[WaveType.Relax]: 0.6, // 放松回合:量大、强度低
|
||||
};
|
||||
|
||||
/** 放松回合数量倍率(相对普通回合) */
|
||||
export const RELAX_COUNT_MUL = 1.5;
|
||||
|
||||
/** 全局怪物数量倍率(在 base_count 基础上整体翻倍,量大管饱) */
|
||||
export const MON_COUNT_MUL = 2;
|
||||
|
||||
/** 全局怪物强度系数(2/3 = 强度削弱三分之一,同时作用于 hp 缩放与 ap 挂钩公式) */
|
||||
export const MON_POWER_MUL = 2 / 3;
|
||||
|
||||
/** 最大回合数(第 20 回合通关) */
|
||||
export const MAX_WAVE = 20;
|
||||
|
||||
/** 每回合时长(秒) */
|
||||
/** 单场战斗时长基准(秒):供清屏速度评价(BattleBannerComp 清屏奖励)等使用 */
|
||||
export const WAVE_DURATION = 30;
|
||||
|
||||
/** 每回合分批次数 */
|
||||
export const BATCH_COUNT = 3;
|
||||
|
||||
/** 每批间隔(秒):30 秒 / 3 批 = 10 秒 */
|
||||
export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT;
|
||||
|
||||
/** 每回合怪物硬上限(放松回合 72 × 1.5 = 108) */
|
||||
export const MAX_MONSTERS = 108;
|
||||
|
||||
/** Boss 护卫队数量(与 Boss 同批压轴进场,占用回合总名额) */
|
||||
export const BOSS_GUARD_COUNT = 3;
|
||||
|
||||
/** 批次怪物数量占比(铺垫 → 加压 → 高潮,第三批另有收尾小队加压) */
|
||||
export const BATCH_RATIO: number[] = [0.25, 0.35, 0.40];
|
||||
|
||||
/** 收尾高潮批额外补入的最强小队数量(非放松回合生效) */
|
||||
export const FINALE_SQUAD_COUNT = 2;
|
||||
|
||||
/** 按回合类型的逐个刷怪间隔(秒):放松回合快速倾泻造潮水感,压力回合稍慢便于聚焦 */
|
||||
export const SPAWN_INTERVAL_BY_TYPE: Record<WaveType, number> = {
|
||||
[WaveType.Normal]: 0.18,
|
||||
[WaveType.Pressure]: 0.25,
|
||||
[WaveType.Relax]: 0.12,
|
||||
};
|
||||
|
||||
/**
|
||||
* 怪物 ap 回合递进曲线(挂钩玩家输出,独立于总强度框架)。
|
||||
* 怪物 ap = 玩家平均攻击 × AP_WAVE_GROWTH(wave) × AP_RELATIVE × 怪物类型系数。
|
||||
* 线性 1.0 → 3.0(wave 1 → 20),保证后期伤害有可感知的爬升。
|
||||
*/
|
||||
export function getApWaveGrowth(wave: number): number {
|
||||
const t = Math.min(Math.max(wave, 1), MAX_WAVE);
|
||||
return 1.0 + (t - 1) / (MAX_WAVE - 1) * 2.0; // wave1=1.0 → wave20=3.0
|
||||
}
|
||||
/** 怪物登场固定构成:1 个 Boss + 4 个小怪(一次全量登场,打完即整局结束) */
|
||||
export const BOSS_COUNT = 1;
|
||||
export const MINION_COUNT = 4;
|
||||
|
||||
/** Boss 战强度系数(原压力回合系数) */
|
||||
export const BOSS_TYPE_RATIO = 1.2;
|
||||
/** 最终 Boss 战 HP 乘区(原 wave 20 配置) */
|
||||
export const FINAL_HP_MUL = 1.8;
|
||||
/** 全局怪物强度系数(2/3 = 强度削弱三分之一,同时作用于 hp 缩放与 ap 挂钩公式) */
|
||||
export const MON_POWER_MUL = 2 / 3;
|
||||
/** 怪物 ap 最终递进系数(原 wave 20 满档 3.0) */
|
||||
export const AP_GROWTH_FINAL = 3.0;
|
||||
/** 怪物 ap 相对玩家平均攻击的基础系数(<1 保证单怪不至于一击致命,由 Boss/精英类型再上浮) */
|
||||
export const AP_RELATIVE = 0.6;
|
||||
/** Boss 血量加成(原 wave 20 bossBonusHpMul = 1.6) */
|
||||
export const BOSS_HP_BONUS_MUL = 1.6;
|
||||
/** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */
|
||||
export const BATTLE_TIMEOUT = 90;
|
||||
|
||||
/** 回合战斗超时(秒):超过后强制结束回合(残留怪销毁并扣留存分,DynamicTuner 因 clearTime 过大自动放水) */
|
||||
export const WAVE_TIMEOUT = 75;
|
||||
|
||||
/** Boss 回合超时(秒):Boss 压轴进场(第 20 秒),击杀耗时更长,放宽兜底阈值 */
|
||||
export const WAVE_TIMEOUT_BOSS = 90;
|
||||
|
||||
/**
|
||||
* 获取指定回合的回合类型
|
||||
* @param wave 回合(1 起)
|
||||
* @returns WaveType
|
||||
*/
|
||||
export function getWaveType(wave: number): WaveType {
|
||||
if (wave % 5 === 0) return WaveType.Pressure;
|
||||
if (wave % 5 === 4) return WaveType.Relax; // 大战前夜的收割补给:清杂攒金币备战 Boss
|
||||
return WaveType.Normal;
|
||||
}
|
||||
|
||||
// ======================== 2. 动态难度调节器 ========================
|
||||
|
||||
/**
|
||||
* 动态难度调节器(系统控制放水 / 加压)
|
||||
* 与硬编码系数并存,用于根据战况实时微调难度。
|
||||
*
|
||||
* 用法示例(MissionComp 每回合结束时调用):
|
||||
* DynamicTuner.adjust(effectiveClearTime, heroDeathCount);
|
||||
*
|
||||
* 设计原则(真隐形 DDA):
|
||||
* - 连续映射:desired = 1 + (0.8 - clearTime/WAVE_DURATION) × K,清场越快要价越高,非离散跳变
|
||||
* - 滞回:连续同方向判定满 HYSTERESIS 回合才生效,偶发超神/崩盘不立即拉阀门
|
||||
* - 指数靠拢:每回合向 desired 移动 50%,避免突变被玩家察觉
|
||||
* - 总幅度钳制 [0.7, 1.3](±30%),防止橡皮筋效应
|
||||
* - 英雄死亡直接锚定 desired=0.8(温和放水),不与慢清场放水叠加
|
||||
*/
|
||||
export const DynamicTuner = {
|
||||
/** 当前难度系数(默认 1.0,>1 加压,<1 放水) */
|
||||
factor: 1.0,
|
||||
|
||||
/** 系数下限(最多放水到 70%) */
|
||||
MIN_FACTOR: 0.7,
|
||||
/** 系数上限(最多加压到 130%) */
|
||||
MAX_FACTOR: 1.3,
|
||||
/** 连续映射增益:clearTime 每偏离基准 100% 时长,factor 偏移 K */
|
||||
K: 0.5,
|
||||
/** 滞回:连续同方向判定满 N 回合才生效 */
|
||||
HYSTERESIS: 2,
|
||||
/** 连续方向计数(>0 加压倾向,<0 放水倾向) */
|
||||
streak: 0,
|
||||
|
||||
/**
|
||||
* 根据上一回合战况自动调节难度
|
||||
* @param clearTime 清场耗时(秒,已含清场加速提前量的还原口径)
|
||||
* @param heroDeathCount 英雄死亡数
|
||||
* @returns 本回合是否实际调整了 factor
|
||||
*/
|
||||
adjust(clearTime: number, heroDeathCount: number): boolean {
|
||||
// 1) 连续映射期望系数:基准 0.8×时长不动,更快加压、更慢放水;死亡锚定 0.8
|
||||
let desired: number;
|
||||
if (heroDeathCount > 0) {
|
||||
desired = 1 + (0.8 - 1.2) * this.K; // = 0.8
|
||||
} else {
|
||||
desired = 1 + (0.8 - clearTime / WAVE_DURATION) * this.K;
|
||||
}
|
||||
desired = Math.min(this.MAX_FACTOR, Math.max(this.MIN_FACTOR, desired));
|
||||
|
||||
// 2) 滞回:连续同方向满 HYSTERESIS 回合才向 desired 靠拢
|
||||
const dir = Math.sign(desired - 1);
|
||||
if (dir === 0) {
|
||||
this.streak = 0;
|
||||
return false;
|
||||
}
|
||||
this.streak = (Math.sign(this.streak) === dir) ? this.streak + dir : dir;
|
||||
if (Math.abs(this.streak) < this.HYSTERESIS) return false;
|
||||
|
||||
// 3) 指数靠拢:每回合向 desired 移动 50%,避免跳变
|
||||
const old = this.factor;
|
||||
this.factor = old + (desired - old) * 0.5;
|
||||
this.factor = Math.min(this.MAX_FACTOR, Math.max(this.MIN_FACTOR, this.factor));
|
||||
return this.factor !== old;
|
||||
},
|
||||
|
||||
/** 重置调节器(每局开始时调用) */
|
||||
reset(): void {
|
||||
this.factor = 1.0;
|
||||
this.streak = 0;
|
||||
},
|
||||
};
|
||||
|
||||
// ======================== 3. 怪物 UUID 池 ========================
|
||||
// ======================== 2. 怪物 UUID 池 ========================
|
||||
|
||||
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
|
||||
export const MonList: Record<number, number[]> = {};
|
||||
@@ -211,7 +67,7 @@ for (const key in HeroInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 3.5 怪物金币掉落配置 ========================
|
||||
// ======================== 2.5 怪物金币掉落配置 ========================
|
||||
|
||||
/**
|
||||
* 怪物金币掉落配置(按 MonType 分类)
|
||||
@@ -241,35 +97,78 @@ export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
|
||||
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
|
||||
}
|
||||
|
||||
// ======================== 4. 小队模板库 ========================
|
||||
// ======================== 3. Boss 组合库 ========================
|
||||
|
||||
/** 小队内单种怪物的槽位定义 */
|
||||
export interface SquadSlot {
|
||||
/** Boss 组合中小怪的槽位定义(type + 数量,Boss 另由 BOSS_COUNT 提供) */
|
||||
export interface BossComboMinionSlot {
|
||||
type: MonType;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** 小队模板(3-4 只怪的组合单元) */
|
||||
export interface SquadConfig {
|
||||
/** Boss 组合配置:一场战斗随机抽一组登场(1 Boss + minions 小怪) */
|
||||
export interface BossComboConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
slots: SquadSlot[];
|
||||
weight: number; // 在小队池中的抽取权重
|
||||
/** 小怪构成(合计数量建议等于 MINION_COUNT,超出截断、不足自动补近战) */
|
||||
minions: BossComboMinionSlot[];
|
||||
/** Boss 技能池 id(引用 BossSkillPool,可选) */
|
||||
boss_skill_pool?: string[];
|
||||
/** 小怪技能池 id(引用 MonSkillPool,可选) */
|
||||
skill_pool?: string[];
|
||||
/** 在组合库中的抽取权重 */
|
||||
weight: number;
|
||||
}
|
||||
|
||||
/** 小队模板库(硬编码) */
|
||||
export const SquadLibrary: Record<string, SquadConfig> = {
|
||||
melee_grunt: { id: "melee_grunt", name: "近战步兵组", weight: 10, slots: [{ type: MonType.Melee, count: 3 }] },
|
||||
assassin_squad: { id: "assassin_squad", name: "刺客突袭组", weight: 6, slots: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Support, count: 1 }] },
|
||||
mixed_balanced: { id: "mixed_balanced", name: "平衡混合组", weight: 8, slots: [{ type: MonType.Melee, count: 1 }, { type: MonType.Long, count: 1 }, { type: MonType.Heavy, count: 1 }] },
|
||||
long_line: { id: "long_line", name: "远程线列组", weight: 7, slots: [{ type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }] },
|
||||
heavy_shield: { id: "heavy_shield", name: "重盾堡垒组", weight: 5, slots: [{ type: MonType.Heavy, count: 1 }, { type: MonType.Melee, count: 2 }] },
|
||||
summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] },
|
||||
/** Boss 护卫队(weight=0 不进随机池,仅供引擎在 Boss 回合直接引用,与 Boss 同批压轴进场) */
|
||||
boss_guard: { id: "boss_guard", name: "Boss 护卫队", weight: 0, slots: [{ type: MonType.Heavy, count: 1 }, { type: MonType.Melee, count: 2 }] },
|
||||
/** Boss 组合库(硬编码):每场战斗按权重随机抽一组 */
|
||||
export const BossComboLibrary: Record<string, BossComboConfig> = {
|
||||
rage_charge: {
|
||||
id: "rage_charge", name: "狂暴冲锋组", weight: 10,
|
||||
minions: [{ type: MonType.Melee, count: 3 }, { type: MonType.Heavy, count: 1 }],
|
||||
boss_skill_pool: ["boss_rage"],
|
||||
},
|
||||
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"],
|
||||
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"],
|
||||
skill_pool: ["warcry"],
|
||||
},
|
||||
assassin_raid: {
|
||||
id: "assassin_raid", name: "刺客突袭组", weight: 7,
|
||||
minions: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Melee, count: 2 }],
|
||||
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 }],
|
||||
boss_skill_pool: ["boss_iron", "boss_doom"],
|
||||
skill_pool: ["leech"],
|
||||
},
|
||||
};
|
||||
|
||||
// ======================== 5. 怪物技能池 ========================
|
||||
/**
|
||||
* 按权重随机抽取一组 Boss 组合
|
||||
* @returns 组合配置,库为空时返回 null
|
||||
*/
|
||||
export function pickBossCombo(): BossComboConfig | null {
|
||||
const combos = Object.values(BossComboLibrary).filter(c => c && c.weight > 0);
|
||||
if (combos.length === 0) return null;
|
||||
const total = combos.reduce((s, c) => s + c.weight, 0);
|
||||
let r = Math.random() * total;
|
||||
for (const c of combos) {
|
||||
r -= c.weight;
|
||||
if (r <= 0) return c;
|
||||
}
|
||||
return combos[combos.length - 1];
|
||||
}
|
||||
|
||||
// ======================== 4. 怪物技能池 ========================
|
||||
|
||||
/**
|
||||
* 怪物技能套装(一套完整的触发技能配置)
|
||||
@@ -300,8 +199,6 @@ export interface MonSkillSet {
|
||||
|
||||
/**
|
||||
* 怪物技能池(硬编码)
|
||||
* 压力回合 / Boss 可配置专属技能,普通回合随机挂载增加变数。
|
||||
*
|
||||
* 技能 uuid 引用 SkillSet 中的 6000~6500 段触发技能。
|
||||
*/
|
||||
export const MonSkillPool: Record<string, MonSkillSet> = {
|
||||
@@ -342,7 +239,7 @@ export const MonSkillPool: Record<string, MonSkillSet> = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Boss 专属技能池(压力回合 Boss 随机挂载) */
|
||||
/** Boss 专属技能池 */
|
||||
export const BossSkillPool: Record<string, MonSkillSet> = {
|
||||
/** 狂暴领主:攻击触发全队攻击提升 */
|
||||
boss_rage: {
|
||||
@@ -373,130 +270,55 @@ export const BossSkillPool: Record<string, MonSkillSet> = {
|
||||
},
|
||||
};
|
||||
|
||||
// ======================== 6. 回合配置表 ========================
|
||||
|
||||
/** 单回合完整配置 */
|
||||
export interface WaveConfig {
|
||||
/** 基础怪物总数(普通回合 72 为上限,放松回合自动 × 1.5 = 108,均已含全局 MON_COUNT_MUL = 2) */
|
||||
base_count: number;
|
||||
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
|
||||
squad_pool: string[];
|
||||
/** HP 成长乘区(并入强度缩放分子,终值 = heroPower × 系数 × hp_mul ÷ Σ基础强度) */
|
||||
hp_mul: number;
|
||||
/** @deprecated 已废弃:ap 改为挂钩玩家输出 × getApWaveGrowth,本字段不再参与计算(保留仅为兼容配置表结构) */
|
||||
ap_mul: number;
|
||||
/** 强度微调(放水 / 加压,默认 1.0) */
|
||||
power_adjust?: number;
|
||||
/** 是否 Boss 回合(压力回合必为 true) */
|
||||
boss_wave?: boolean;
|
||||
/** 普通怪技能池 id(可选,随机挂载) */
|
||||
skill_pool?: string[];
|
||||
/** Boss 技能池 id(Boss 回合专用,随机挂载) */
|
||||
boss_skill_pool?: string[];
|
||||
}
|
||||
// ======================== 5. 配置校验 ========================
|
||||
|
||||
/**
|
||||
* 回合配置表(硬编码 wave 1~20)
|
||||
*
|
||||
* 心流循环(5 回合一循环):
|
||||
* wave % 5 === 0 → 压力回合(必带 Boss,强度高、数量少)
|
||||
* wave % 5 === 4 → 放松回合(数量 × 1.5,强度低,大战前夜收割补给)
|
||||
* 其余 → 普通回合(标准强度)
|
||||
*
|
||||
* 强度递进:hp_mul / ap_mul 每 5 回合一档,压力回合额外提升。
|
||||
*/
|
||||
export const WaveConfigs: Record<number, WaveConfig> = {
|
||||
// ===== 第一循环:教学期 =====
|
||||
1: { base_count: 36, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 },
|
||||
2: { base_count: 42, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 },
|
||||
3: { base_count: 48, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 },
|
||||
// 放松回合:量大好清,Boss 前收割补给
|
||||
4: { base_count: 54, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 },
|
||||
// 压力回合:第一 Boss
|
||||
5: { base_count: 42, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], hp_mul: 1.15, ap_mul: 1.10, boss_wave: true, boss_skill_pool: ["boss_rage"] },
|
||||
|
||||
// ===== 第二循环:引入技能怪 =====
|
||||
6: { base_count: 60, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 },
|
||||
7: { base_count: 60, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] },
|
||||
8: { base_count: 66, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] },
|
||||
// 放松回合:量大好清,Boss 前收割补给
|
||||
9: { base_count: 66, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] },
|
||||
// 压力回合:第二 Boss
|
||||
10: { base_count: 48, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], hp_mul: 1.30, ap_mul: 1.25, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron"] },
|
||||
|
||||
// ===== 第三循环:组合多样化 =====
|
||||
11: { base_count: 66, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] },
|
||||
12: { base_count: 66, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.30, ap_mul: 1.25, skill_pool: ["tough", "warcry"] },
|
||||
13: { base_count: 72, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] },
|
||||
// 放松回合:量大好清,Boss 前收割补给
|
||||
14: { base_count: 72, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], hp_mul: 1.40, ap_mul: 1.35, skill_pool: ["berserk", "tough", "legacy"] },
|
||||
// 压力回合:第三 Boss(中期高潮)
|
||||
15: { base_count: 54, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.50, ap_mul: 1.40, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"] },
|
||||
|
||||
// ===== 第四循环:终极阶段(17~19 power_adjust 逐步爬坡,为最终 Boss 蓄势) =====
|
||||
16: { base_count: 72, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] },
|
||||
17: { base_count: 72, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"], power_adjust: 1.05 },
|
||||
18: { base_count: 72, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"], power_adjust: 1.10 },
|
||||
// 放松回合:量大好清,最终 Boss 前收割补给
|
||||
19: { base_count: 72, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"], power_adjust: 1.20 },
|
||||
// 压力回合:最终 Boss
|
||||
20: { base_count: 60, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], hp_mul: 1.80, ap_mul: 1.70, boss_wave: true, boss_skill_pool: ["boss_doom", "boss_rage"] },
|
||||
};
|
||||
|
||||
// ======================== 7. 配置校验 ========================
|
||||
|
||||
/**
|
||||
* 校验 WaveConfigs / SquadLibrary / MonSkillPool 引用一致性
|
||||
* 校验 BossComboLibrary / MonSkillPool 引用一致性
|
||||
* 建议在游戏启动时调用一次,便于发现配置错误
|
||||
* @returns 错误信息数组,空数组表示校验通过
|
||||
*/
|
||||
export function validateRogueConfig(): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 1. 校验 WaveConfigs 引用与范围
|
||||
for (const wave in WaveConfigs) {
|
||||
const cfg = WaveConfigs[wave];
|
||||
for (const squadId of cfg.squad_pool) {
|
||||
if (!SquadLibrary[squadId]) {
|
||||
errors.push(`Wave ${wave} 引用了不存在的小队: ${squadId}`);
|
||||
// 1. 校验组合引用与数量
|
||||
for (const id in BossComboLibrary) {
|
||||
const combo = BossComboLibrary[id];
|
||||
let minionTotal = 0;
|
||||
for (const slot of combo.minions) {
|
||||
minionTotal += slot.count;
|
||||
if (!MonList[slot.type] || MonList[slot.type].length === 0) {
|
||||
errors.push(`组合 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`);
|
||||
}
|
||||
}
|
||||
if (cfg.skill_pool) {
|
||||
for (const skillId of cfg.skill_pool) {
|
||||
if (minionTotal !== MINION_COUNT) {
|
||||
errors.push(`组合 ${id} 小怪数量=${minionTotal} 与 MINION_COUNT=${MINION_COUNT} 不一致`);
|
||||
}
|
||||
if (combo.skill_pool) {
|
||||
for (const skillId of combo.skill_pool) {
|
||||
if (!MonSkillPool[skillId]) {
|
||||
errors.push(`Wave ${wave} 引用了不存在的技能: ${skillId}`);
|
||||
errors.push(`组合 ${id} 引用了不存在的技能: ${skillId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cfg.boss_skill_pool) {
|
||||
for (const skillId of cfg.boss_skill_pool) {
|
||||
if (combo.boss_skill_pool) {
|
||||
for (const skillId of combo.boss_skill_pool) {
|
||||
if (!BossSkillPool[skillId]) {
|
||||
errors.push(`Wave ${wave} 引用了不存在的 Boss 技能: ${skillId}`);
|
||||
errors.push(`组合 ${id} 引用了不存在的 Boss 技能: ${skillId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cfg.base_count < 1 || cfg.base_count > MAX_MONSTERS) {
|
||||
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~${MAX_MONSTERS})`);
|
||||
}
|
||||
if (cfg.power_adjust !== undefined && (cfg.power_adjust < 0.8 || cfg.power_adjust > 1.3)) {
|
||||
errors.push(`Wave ${wave} power_adjust=${cfg.power_adjust} 越界 (0.8~1.3)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 校验 SquadLibrary 中所有 type 在 MonList 中有怪
|
||||
for (const id in SquadLibrary) {
|
||||
const squad = SquadLibrary[id];
|
||||
for (const slot of squad.slots) {
|
||||
if (!MonList[slot.type] || MonList[slot.type].length === 0) {
|
||||
errors.push(`小队 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`);
|
||||
}
|
||||
}
|
||||
// 2. 校验 Boss 类型在 MonList 中有怪
|
||||
if ((!MonList[MonType.MeleeBoss] || MonList[MonType.MeleeBoss].length === 0)
|
||||
&& (!MonList[MonType.LongBoss] || MonList[MonType.LongBoss].length === 0)) {
|
||||
errors.push("MonList 中无任何 Boss 类型(MeleeBoss/LongBoss)可用 uuid");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ======================== 8. 生成结果接口 ========================
|
||||
// ======================== 6. 生成结果接口 ========================
|
||||
|
||||
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
|
||||
export interface GeneratedMonster {
|
||||
@@ -505,10 +327,10 @@ export interface GeneratedMonster {
|
||||
hp: number;
|
||||
ap: number;
|
||||
isBoss: boolean;
|
||||
/** 是否为 Boss 护卫队成员(与 Boss 同批压轴进场,供 UI/统计识别) */
|
||||
/** 是否为 Boss 护卫队成员(供 UI/统计识别) */
|
||||
isBossGuard?: boolean;
|
||||
spawnIndex: number;
|
||||
/** 本怪所属批次(0~2,由 MissionMonComp 按 BATCH_INTERVAL 释放) */
|
||||
/** 本怪所属批次(保留字段,单场全量登场恒为 0) */
|
||||
batch: number;
|
||||
|
||||
/**
|
||||
@@ -527,13 +349,12 @@ export interface GeneratedMonster {
|
||||
};
|
||||
}
|
||||
|
||||
// ======================== 9. 测试模式配置 ========================
|
||||
// ======================== 7. 测试模式配置 ========================
|
||||
|
||||
export const TestModeConfig = {
|
||||
enable: false, // 默认关闭测试模式
|
||||
baseHp: 150,
|
||||
baseAp: 12,
|
||||
growthRatePerWave: 0.2,
|
||||
monType: MonType.Melee,
|
||||
monUuid: 6001,
|
||||
spawnCount: 1,
|
||||
@@ -546,145 +367,80 @@ export const TestModeConfig = {
|
||||
fend: undefined as TriggerGrouped | undefined,
|
||||
};
|
||||
|
||||
// ======================== 10. 生成引擎 ========================
|
||||
// ======================== 8. 生成引擎 ========================
|
||||
|
||||
/**
|
||||
* 肉鸽刷怪生成引擎
|
||||
* 按英雄强度反推怪物强度,结合回合类型系数与动态调节器生成怪物列表
|
||||
* Boss 组合生成引擎
|
||||
* 随机抽一组 Boss 组合,按英雄强度反推怪物强度(最终 Boss 战档位)
|
||||
*/
|
||||
export class RogueSpawningEngine {
|
||||
|
||||
/**
|
||||
* 生成指定回合的怪物列表
|
||||
* @param waveNumber 回合(1 起,>MAX_WAVE 时复用 wave MAX_WAVE 配置)
|
||||
* @returns 怪物列表,长度 ≤ MAX_MONSTERS
|
||||
* 生成一场战斗的怪物列表:随机一组 Boss 组合(1 Boss + 组合小怪)。
|
||||
* Boss 压队尾(登场时由 MissionMonComp 统一布位)。
|
||||
* @returns 怪物列表(小怪在前、Boss 在尾)
|
||||
*/
|
||||
generateWave(waveNumber: number): GeneratedMonster[] {
|
||||
if (waveNumber < 1) return [];
|
||||
|
||||
generateBossCombo(): GeneratedMonster[] {
|
||||
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
|
||||
if (TestModeConfig.enable) {
|
||||
return this.generateTestWave(waveNumber);
|
||||
return this.generateTestWave();
|
||||
}
|
||||
|
||||
const wave = Math.min(waveNumber, MAX_WAVE);
|
||||
const cfg = WaveConfigs[wave];
|
||||
const waveType = getWaveType(wave);
|
||||
const typeRatio = WAVE_TYPE_POWER_RATIO[waveType];
|
||||
const combo = pickBossCombo();
|
||||
const monsters: GeneratedMonster[] = [];
|
||||
|
||||
// 1. 计算目标强度(MON_POWER_MUL = 2/3:数量翻倍后单怪强度整体削弱三分之一,避免总压力膨胀)
|
||||
if (combo) {
|
||||
for (const slot of combo.minions) {
|
||||
for (let i = 0; i < slot.count; i++) {
|
||||
monsters.push(this.makeMonster(slot.type, monsters.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 组合库为空或数量不足兜底:补近战小怪至 MINION_COUNT
|
||||
while (monsters.length < MINION_COUNT) {
|
||||
monsters.push(this.makeMonster(MonType.Melee, monsters.length));
|
||||
}
|
||||
|
||||
// Boss 压队尾
|
||||
for (let i = 0; i < BOSS_COUNT; i++) {
|
||||
monsters.push(this.makeBoss());
|
||||
}
|
||||
|
||||
// 属性缩放:直接取最终 Boss 战档位系数
|
||||
const heroPower = this.getCurrentHeroPower();
|
||||
const powerAdjust = cfg.power_adjust ?? 1.0;
|
||||
const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor * MON_POWER_MUL;
|
||||
|
||||
// 2. 确定怪物总数(放松回合 × 1.5;普通/压力回合预留收尾高潮批名额,防 slice 截掉)
|
||||
let totalCount = cfg.base_count;
|
||||
if (waveType === WaveType.Relax) {
|
||||
totalCount = Math.round(totalCount * RELAX_COUNT_MUL);
|
||||
} else {
|
||||
totalCount += this.estimateFinaleCount(cfg.squad_pool);
|
||||
}
|
||||
totalCount = Math.min(totalCount, MAX_MONSTERS);
|
||||
|
||||
// 3. Boss 位(压力回合必带 Boss):先记录延后挂载,使其压轴进场而非第 0 秒开场
|
||||
let boss: GeneratedMonster | null = null;
|
||||
let remaining = totalCount;
|
||||
if (cfg.boss_wave) {
|
||||
boss = this.makeBoss(wave, cfg);
|
||||
remaining -= 1 + BOSS_GUARD_COUNT; // Boss 1 只 + 护卫队名额
|
||||
}
|
||||
|
||||
// 4. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
|
||||
const monsters: GeneratedMonster[] = this.assembleSquads(cfg.squad_pool, remaining, wave);
|
||||
|
||||
// 4.5 收尾高潮批:非放松回合额外补入最强小队,与 Boss 一样压轴(先补入再统一缩放,保证强度自洽)
|
||||
const finaleSquad = waveType !== WaveType.Relax ? this.pickStrongestSquad(cfg.squad_pool) : null;
|
||||
if (finaleSquad) {
|
||||
for (let s = 0; s < FINALE_SQUAD_COUNT; s++) {
|
||||
for (const slot of finaleSquad.slots) {
|
||||
for (let c = 0; c < slot.count; c++) {
|
||||
const m = this.makeMonster(slot.type, wave, 0);
|
||||
m.batch = BATCH_COUNT - 1; // 标记收尾批,第 8 步不再覆盖
|
||||
monsters.push(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.6 Boss 与护卫队压队尾,使其在批次分配后落入最后一批(回合内高潮点)
|
||||
if (boss) {
|
||||
monsters.push(...this.makeBossGuards(wave));
|
||||
monsters.push(boss);
|
||||
}
|
||||
|
||||
// 5. 属性计算:
|
||||
// - hp 走总强度缩放框架(自动匹配玩家战力):hp = base_hp × hpScale
|
||||
// - ap 与总强度脱钩,挂钩玩家输出 × 回合递进(解决"ap 随回合涨不动":
|
||||
// 原方案 ap 被总强度封顶 + hp/ap 共用分母,增长被熨平到每回合 5~8%)
|
||||
const targetPower = heroPower * BOSS_TYPE_RATIO * MON_POWER_MUL;
|
||||
const totalBasePower = monsters.reduce((sum, m) => {
|
||||
const info = HeroInfo[m.uuid];
|
||||
return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap);
|
||||
}, 0);
|
||||
|
||||
if (totalBasePower > 0 && targetPower > 0) {
|
||||
const hpScale = (targetPower * cfg.hp_mul) / totalBasePower;
|
||||
const hpScale = (targetPower * FINAL_HP_MUL) / totalBasePower;
|
||||
const heroAvgAp = this.getCurrentHeroAttack();
|
||||
const apGrowth = getApWaveGrowth(wave);
|
||||
for (const m of monsters) {
|
||||
m.hp = Math.max(1, Math.round(m.hp * hpScale));
|
||||
// ap = 玩家平均攻击 × 回合递进 × 基础系数 × 类型系数 × 全局强度系数(削弱三分之一)
|
||||
m.ap = Math.max(1, Math.round(heroAvgAp * apGrowth * AP_RELATIVE * MON_POWER_MUL * this.getApTypeFactor(m)));
|
||||
// ap = 玩家平均攻击 × 最终递进 × 基础系数 × 全局强度系数 × 类型系数
|
||||
m.ap = Math.max(1, Math.round(heroAvgAp * AP_GROWTH_FINAL * AP_RELATIVE * MON_POWER_MUL * this.getApTypeFactor(m)));
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 挂载技能(普通怪随机技能池,Boss 专属技能池)
|
||||
// 挂载组合技能(Boss 专属池 / 小怪普通池)
|
||||
for (const m of monsters) {
|
||||
if (m.isBoss && cfg.boss_skill_pool) {
|
||||
m.skills = this.pickSkillSet(cfg.boss_skill_pool, BossSkillPool);
|
||||
} else if (!m.isBoss && cfg.skill_pool) {
|
||||
m.skills = this.pickSkillSet(cfg.skill_pool, MonSkillPool);
|
||||
if (m.isBoss && combo?.boss_skill_pool) {
|
||||
m.skills = this.pickSkillSet(combo.boss_skill_pool, BossSkillPool);
|
||||
} else if (!m.isBoss && combo?.skill_pool) {
|
||||
m.skills = this.pickSkillSet(combo.skill_pool, MonSkillPool);
|
||||
}
|
||||
m.batch = 0;
|
||||
}
|
||||
|
||||
// 8. 分配批次:按 BATCH_RATIO 递增加权(铺垫 → 加压 → 高潮),收尾小队/Boss/护卫保持最后一批
|
||||
this.assignBatches(monsters);
|
||||
if (boss) {
|
||||
const lastBatch = BATCH_COUNT - 1;
|
||||
for (const m of monsters) {
|
||||
if (m.isBoss || m.isBossGuard) m.batch = lastBatch;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < monsters.length; i++) {
|
||||
monsters[i].spawnIndex = i;
|
||||
}
|
||||
|
||||
// 9. 硬上限保护
|
||||
return monsters.slice(0, MAX_MONSTERS);
|
||||
return monsters;
|
||||
}
|
||||
|
||||
/** 重置引擎状态(每局开始时调用) */
|
||||
/** 重置引擎状态(每局开始时调用,预留) */
|
||||
reset(): void {
|
||||
DynamicTuner.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定回合的怪物总数
|
||||
* @param waveNumber 目标回合数
|
||||
* @returns 预计生成的怪物总数
|
||||
*/
|
||||
getWaveMonsterCount(waveNumber: number): number {
|
||||
if (waveNumber < 1) return 0;
|
||||
if (TestModeConfig.enable) {
|
||||
return Math.max(1, TestModeConfig.spawnCount || 1);
|
||||
}
|
||||
const wave = Math.min(waveNumber, MAX_WAVE);
|
||||
const cfg = WaveConfigs[wave];
|
||||
const waveType = getWaveType(wave);
|
||||
let count = cfg.base_count;
|
||||
if (waveType === WaveType.Relax) {
|
||||
count = Math.round(count * RELAX_COUNT_MUL);
|
||||
}
|
||||
return Math.min(count, MAX_MONSTERS);
|
||||
}
|
||||
|
||||
// ======================== 私有生成子算法 ========================
|
||||
@@ -710,8 +466,7 @@ export class RogueSpawningEngine {
|
||||
|
||||
/**
|
||||
* 计算场上存活英雄的输出强度(用于怪物 ap 挂钩玩家,独立于总强度框架)。
|
||||
* 取玩家英雄的"实时 ap × 数量",代表玩家当前能打出/承受的攻击量级。
|
||||
* 怪物 ap 以此为锚,保证玩家输出越高、被反击越痛(威胁感跟随成长)。
|
||||
* 取玩家英雄的"实时 ap 平均",怪物 ap 以此为锚,保证玩家输出越高、被反击越痛。
|
||||
*/
|
||||
private getCurrentHeroAttack(): number {
|
||||
let totalAp = 0;
|
||||
@@ -728,8 +483,7 @@ export class RogueSpawningEngine {
|
||||
}
|
||||
|
||||
/** 测试模式:完全绕过引擎逻辑 */
|
||||
private generateTestWave(waveNumber: number): GeneratedMonster[] {
|
||||
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
|
||||
private generateTestWave(): GeneratedMonster[] {
|
||||
const count = Math.max(1, TestModeConfig.spawnCount || 1);
|
||||
const monsters: GeneratedMonster[] = [];
|
||||
|
||||
@@ -737,11 +491,11 @@ export class RogueSpawningEngine {
|
||||
monsters.push({
|
||||
uuid: TestModeConfig.monUuid,
|
||||
type: TestModeConfig.monType,
|
||||
hp: Math.round(TestModeConfig.baseHp * growth),
|
||||
ap: Math.round(TestModeConfig.baseAp * growth),
|
||||
hp: TestModeConfig.baseHp,
|
||||
ap: TestModeConfig.baseAp,
|
||||
isBoss: false,
|
||||
spawnIndex: i,
|
||||
batch: i % BATCH_COUNT,
|
||||
batch: 0,
|
||||
skills: {
|
||||
skill: TestModeConfig.skill,
|
||||
atking: TestModeConfig.atking,
|
||||
@@ -755,33 +509,8 @@ export class RogueSpawningEngine {
|
||||
return monsters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小队拼装:按权重从 squad_pool 抽小队,累计到 target 数量
|
||||
* 超出部分按剩余位截断
|
||||
*/
|
||||
private assembleSquads(pool: string[], target: number, wave: number): GeneratedMonster[] {
|
||||
const result: GeneratedMonster[] = [];
|
||||
let filled = 0;
|
||||
let safety = 20; // 防御性循环上限
|
||||
|
||||
while (filled < target && safety-- > 0) {
|
||||
const squad = this.pickWeightedSquad(pool);
|
||||
if (!squad) break;
|
||||
|
||||
const remaining = target - filled;
|
||||
for (const slot of squad.slots) {
|
||||
for (let i = 0; i < slot.count && filled < remaining; i++) {
|
||||
result.push(this.makeMonster(slot.type, wave, filled));
|
||||
filled++;
|
||||
}
|
||||
if (filled >= remaining) break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 生成 Boss(压轴位:batch/spawnIndex 为占位值,由 generateWave 统一分配并强制最后一批) */
|
||||
private makeBoss(wave: number, _cfg: WaveConfig): GeneratedMonster {
|
||||
/** 生成 Boss(基础血量带最终战加成,属性随后由总强度框架统一缩放) */
|
||||
private makeBoss(): GeneratedMonster {
|
||||
const isMeleeBoss = Math.random() < 0.5;
|
||||
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
||||
|
||||
@@ -796,23 +525,19 @@ export class RogueSpawningEngine {
|
||||
const baseHp = baseInfo ? baseInfo.hp : 500;
|
||||
const baseAp = baseInfo ? baseInfo.ap : 30;
|
||||
|
||||
// Boss 自带额外血量加成:每 5 回合递增 20%(wave 5=1.0, 10=1.2, 15=1.4...)
|
||||
const bossBonusHpMul = 1 + 0.20 * Math.floor(Math.max(0, wave - 5) / 5);
|
||||
|
||||
return {
|
||||
uuid,
|
||||
type,
|
||||
hp: Math.round(baseHp * bossBonusHpMul),
|
||||
hp: Math.round(baseHp * BOSS_HP_BONUS_MUL),
|
||||
ap: baseAp,
|
||||
isBoss: true,
|
||||
spawnIndex: 0, // 占位值,由 generateWave 第 8 步统一分配
|
||||
batch: 0, // 占位值,generateWave 会强制 Boss 进入最后一批
|
||||
spawnIndex: 0, // 占位值,由 generateBossCombo 统一分配
|
||||
batch: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 怪物 ap 类型系数:Boss/护卫上浮,远程/召唤稍降(避免玻璃大炮互秒),近战/重甲基准。
|
||||
* 配合 getApWaveGrowth 使用,让"谁更疼"有明确的类型区分。
|
||||
* 怪物 ap 类型系数:Boss 上浮,远程/召唤稍降(避免玻璃大炮互秒),近战/重甲基准。
|
||||
*/
|
||||
private getApTypeFactor(m: GeneratedMonster): number {
|
||||
if (m.isBoss) return 2.2; // Boss 单发重,符合"压场"定位
|
||||
@@ -825,77 +550,8 @@ export class RogueSpawningEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成 Boss 护卫队(复用 boss_guard 小队模板),与 Boss 同批压轴进场 */
|
||||
private makeBossGuards(wave: number): GeneratedMonster[] {
|
||||
const squad = SquadLibrary["boss_guard"];
|
||||
const guards: GeneratedMonster[] = [];
|
||||
for (const slot of squad.slots) {
|
||||
for (let i = 0; i < slot.count; i++) {
|
||||
const g = this.makeMonster(slot.type, wave, 0);
|
||||
g.isBossGuard = true;
|
||||
guards.push(g);
|
||||
}
|
||||
}
|
||||
return guards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批次分配:按 BATCH_RATIO 递增加权切分(铺垫 → 加压 → 高潮)。
|
||||
* 已预标记 batch 的怪(收尾小队 / Boss / 护卫)不参与切分,保持最后一批。
|
||||
*/
|
||||
private assignBatches(monsters: GeneratedMonster[]): void {
|
||||
const normal = monsters.filter(m => m.batch !== BATCH_COUNT - 1 && !m.isBoss && !m.isBossGuard);
|
||||
const n = normal.length;
|
||||
if (n === 0) return;
|
||||
|
||||
let cursor = 0;
|
||||
for (let b = 0; b < BATCH_COUNT - 1; b++) {
|
||||
const quota = Math.round(n * BATCH_RATIO[b]);
|
||||
for (let k = 0; k < quota && cursor < n; k++, cursor++) {
|
||||
normal[cursor].batch = b;
|
||||
}
|
||||
}
|
||||
// 剩余全部进入高潮批
|
||||
for (; cursor < n; cursor++) {
|
||||
normal[cursor].batch = BATCH_COUNT - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 预估收尾高潮批额外补入的怪物数量(用于 totalCount 预留名额) */
|
||||
private estimateFinaleCount(pool: string[]): number {
|
||||
const squad = this.pickStrongestSquad(pool);
|
||||
if (!squad) return 0;
|
||||
let per = 0;
|
||||
for (const slot of squad.slots) per += slot.count;
|
||||
return per * FINALE_SQUAD_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别小队池中最强小队:按槽位 MonType 基础强度(calcHeroPower 1 级样本)× 数量加权求和。
|
||||
* 注意不能用 squad.weight——它是"出现频率"语义而非强度。
|
||||
*/
|
||||
private pickStrongestSquad(pool: string[]): SquadConfig | null {
|
||||
let best: SquadConfig | null = null;
|
||||
let bestScore = -1;
|
||||
for (const id of pool) {
|
||||
const sq = SquadLibrary[id];
|
||||
if (!sq) continue;
|
||||
let score = 0;
|
||||
for (const slot of sq.slots) {
|
||||
const uuids = MonList[slot.type];
|
||||
const sample = uuids && uuids.length ? HeroInfo[uuids[0]] : null;
|
||||
score += (sample ? calcHeroPower(sample, 1) : 100) * slot.count;
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = sq;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** 生成普通怪物(wave 保留参数位,供后续按回合差异化基础属性扩展) */
|
||||
private makeMonster(type: MonType, _wave: number, spawnIndex: number): GeneratedMonster {
|
||||
/** 生成普通怪物 */
|
||||
private makeMonster(type: MonType, spawnIndex: number): GeneratedMonster {
|
||||
let uuids = MonList[type];
|
||||
if (!uuids || uuids.length === 0) {
|
||||
// 兜底 Melee
|
||||
@@ -914,7 +570,7 @@ export class RogueSpawningEngine {
|
||||
ap: baseAp,
|
||||
isBoss: false,
|
||||
spawnIndex,
|
||||
batch: 0, // 后续统一分配
|
||||
batch: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -936,13 +592,6 @@ export class RogueSpawningEngine {
|
||||
};
|
||||
}
|
||||
|
||||
/** 按权重从小队 id 池抽 1 个小队 */
|
||||
private pickWeightedSquad(pool: string[]): SquadConfig | null {
|
||||
const valid = pool.map(id => SquadLibrary[id]).filter(s => s);
|
||||
if (valid.length === 0) return null;
|
||||
return this.pickWeighted(valid);
|
||||
}
|
||||
|
||||
/** 通用加权随机抽取 */
|
||||
private pickWeighted<T extends { weight: number }>(items: T[]): T | null {
|
||||
if (items.length === 0) return null;
|
||||
@@ -957,57 +606,6 @@ export class RogueSpawningEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 11. 全局单例 & 向后兼容导出 ========================
|
||||
// ======================== 9. 全局单例 ========================
|
||||
|
||||
export const spawningEngine = new RogueSpawningEngine();
|
||||
|
||||
/** 历史接口:回合槽位(保留兼容) */
|
||||
export interface IWaveSlot {
|
||||
type: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定回合预计的怪物总数量
|
||||
* @param waveNumber 目标回合数
|
||||
*/
|
||||
export function getWaveMonsterCount(waveNumber: number): number {
|
||||
return spawningEngine.getWaveMonsterCount(waveNumber);
|
||||
}
|
||||
|
||||
/** 获取回合槽位配置(向后兼容,从 generateWave 反推) */
|
||||
export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
||||
const generated = spawningEngine.generateWave(waveNumber);
|
||||
const slotMap = new Map<number, number>();
|
||||
|
||||
for (const m of generated) {
|
||||
slotMap.set(m.type, (slotMap.get(m.type) || 0) + 1);
|
||||
}
|
||||
|
||||
return Array.from(slotMap.entries()).map(([type, count]) => ({ type, count }));
|
||||
}
|
||||
|
||||
export const DefaultWaveSlot: IWaveSlot[] = [
|
||||
{ type: MonType.Melee, count: 20 },
|
||||
{ type: MonType.Long, count: 15 },
|
||||
{ type: MonType.Support, count: 5 },
|
||||
];
|
||||
|
||||
/** 回合槽位配置代理(按需生成) */
|
||||
export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy(
|
||||
{} as { [wave: number]: IWaveSlot[] },
|
||||
{
|
||||
get(_target, prop: string) {
|
||||
const wave = parseInt(prop, 10);
|
||||
if (!isNaN(wave) && wave >= 1) {
|
||||
return getWaveSlotConfig(wave);
|
||||
}
|
||||
if (prop === "toJSON") return () => ({});
|
||||
return undefined;
|
||||
},
|
||||
has(_target, prop: string) {
|
||||
const wave = parseInt(prop, 10);
|
||||
return !isNaN(wave) && wave >= 1;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,101 +1,74 @@
|
||||
/**
|
||||
* @file WaveHudComp.ts
|
||||
* @description 波次进度 HUD 组件(表现层)
|
||||
* @description 战斗进度 HUD 组件(表现层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 本回合清剿进度:剩余怪数(mon_num + pending_mon_num)+ 进度条。
|
||||
* 2. 全局 20 回合进度格:静态生成,Boss 回合(wave%5==0)置旗帜样式,当前回合脉动、已过回合置灰。
|
||||
* 1. 清剿进度:剩余怪数(mon_num + pending_mon_num)+ 进度条。
|
||||
* 2. Boss 战旗帜:单场 Boss 战定胜负,仅 1 格旗帜,战斗中脉动。
|
||||
*
|
||||
* 数据流:NewWave 事件缓存 total/wave;update 0.2s 降频轮询 vmdata(与 syncMonsterSpawnState 同节奏)。
|
||||
* 使用 oops-framework 模块:oops.message(事件解耦)。
|
||||
*
|
||||
* 编辑器绑定:waveLab / remainLab / progress / flagsRoot(水平 Layout 容器,20 格代码生成)。
|
||||
* 编辑器绑定:waveLab / remainLab / progress / flagsRoot(水平 Layout 容器,1 格代码生成)。
|
||||
*/
|
||||
import { _decorator, Node, Label, ProgressBar, Sprite, Color, tween, Tween, v3, UITransform } from "cc";
|
||||
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { getWaveType, WaveType, MAX_WAVE } from "./RogueConfig";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('WaveHudComp')
|
||||
export class WaveHudComp extends CCComp {
|
||||
@property({ type: Label, tooltip: "回合文本(第 x/20 回合)" })
|
||||
@property({ type: Label, tooltip: "战斗标题文本" })
|
||||
waveLab: Label | null = null;
|
||||
|
||||
@property({ type: Label, tooltip: "剩余怪数文本" })
|
||||
remainLab: Label | null = null;
|
||||
|
||||
@property({ type: ProgressBar, tooltip: "本回合清剿进度条" })
|
||||
@property({ type: ProgressBar, tooltip: "清剿进度条" })
|
||||
progress: ProgressBar | null = null;
|
||||
|
||||
@property({ type: Node, tooltip: "全局回合旗帜容器(水平 Layout,20 格代码生成)" })
|
||||
@property({ type: Node, tooltip: "Boss 战旗帜容器(水平 Layout,1 格代码生成)" })
|
||||
flagsRoot: Node | null = null;
|
||||
|
||||
/** 本回合怪物总数(NewWave 事件载荷缓存) */
|
||||
/** 本场怪物总数(NewWave 事件载荷缓存) */
|
||||
private waveTotal: number = 0;
|
||||
/** HUD 刷新节流计时器 */
|
||||
private hudTimer: number = 0;
|
||||
/** 20 格旗帜节点缓存(Boss 位为旗帜样式) */
|
||||
private flagNodes: Node[] = [];
|
||||
/** Boss 战旗帜节点 */
|
||||
private flagNode: Node | null = null;
|
||||
|
||||
onLoad() {
|
||||
// oops.message 全局事件总线:新回合
|
||||
// oops.message 全局事件总线:新战斗
|
||||
oops.message.on(GameEvent.NewWave, this.onNewWave, this);
|
||||
this.buildFlags();
|
||||
this.buildFlag();
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
oops.message.off(GameEvent.NewWave, this.onNewWave, this);
|
||||
}
|
||||
|
||||
/** 静态生成 20 格回合进度(Boss 位按 getWaveType 静态预知,无需运行数据) */
|
||||
private buildFlags() {
|
||||
/** 静态生成 1 格 Boss 战旗帜 */
|
||||
private buildFlag() {
|
||||
if (!this.flagsRoot) return;
|
||||
for (let i = 1; i <= MAX_WAVE; i++) {
|
||||
const cell = new Node(`flag_${i}`);
|
||||
const cell = new Node("flag_boss");
|
||||
cell.addComponent(UITransform).setContentSize(14, 14);
|
||||
const sp = cell.addComponent(Sprite);
|
||||
// Boss 位用警示色方块,普通位用圆点色(无美术资源时以颜色区分,后续可换 spriteFrame)
|
||||
const isBoss = getWaveType(i) === WaveType.Pressure;
|
||||
sp.color = isBoss ? new Color(220, 50, 50) : new Color(120, 120, 120);
|
||||
sp.color = new Color(220, 50, 50);
|
||||
sp.sizeMode = Sprite.SizeMode.CUSTOM;
|
||||
cell.parent = this.flagsRoot;
|
||||
this.flagNodes.push(cell);
|
||||
}
|
||||
this.flagNode = cell;
|
||||
}
|
||||
|
||||
private onNewWave(event: string, data: { wave: number; total: number; bossWave: boolean }) {
|
||||
this.waveTotal = data.total;
|
||||
if (this.waveLab) this.waveLab.string = `第 ${data.wave}/${MAX_WAVE} 回合`;
|
||||
this.refreshFlags(data.wave);
|
||||
if (this.waveLab) this.waveLab.string = "最终 Boss 战";
|
||||
if (this.flagNode) this.playFlagPulse(this.flagNode);
|
||||
}
|
||||
|
||||
/** 刷新旗帜状态:已过回合置暗,当前回合 Boss 旗脉动 */
|
||||
private refreshFlags(currentWave: number) {
|
||||
for (let i = 0; i < this.flagNodes.length; i++) {
|
||||
const cell = this.flagNodes[i];
|
||||
const wave = i + 1;
|
||||
const sp = cell.getComponent(Sprite)!;
|
||||
const isBoss = getWaveType(wave) === WaveType.Pressure;
|
||||
if (wave < currentWave) {
|
||||
sp.color = new Color(70, 70, 70); // 已过回合置暗
|
||||
cell.setScale(v3(1, 1, 1));
|
||||
Tween.stopAllByTarget(cell);
|
||||
} else if (wave === currentWave) {
|
||||
sp.color = isBoss ? new Color(255, 80, 80) : new Color(255, 220, 100);
|
||||
if (isBoss) this.playFlagPulse(cell);
|
||||
} else {
|
||||
sp.color = isBoss ? new Color(220, 50, 50) : new Color(120, 120, 120);
|
||||
cell.setScale(v3(1, 1, 1));
|
||||
Tween.stopAllByTarget(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前回合 Boss 旗缩放脉动 */
|
||||
/** Boss 战旗帜缩放脉动 */
|
||||
private playFlagPulse(cell: Node) {
|
||||
Tween.stopAllByTarget(cell);
|
||||
tween(cell)
|
||||
|
||||
Reference in New Issue
Block a user