refactor: 重构关卡经济与战斗流程

1.  移除战斗阶段限制,允许玩家在战斗中操作卡牌
2.  新增波次间5秒自动倒计时机制
3.  将固定波次金币奖励改为怪物死亡掉落
4.  重构卡牌面板逻辑,统一抽卡按钮状态判断
5.  清理冗余的战斗阶段状态变量与相关代码
This commit is contained in:
pan
2026-07-20 15:59:59 +08:00
parent 279e239e3a
commit f0952ef82c
4 changed files with 249 additions and 199 deletions

View File

@@ -15,6 +15,8 @@ import { FieldSkillType } from "../common/config/SkillSet";
import { mLogger } from "../common/Logger";
import { SkillTriggerHelper } from "./SkillTriggerHelper";
import { getMonsterGoldDrop } from "../map/RogueConfig";
import { MissionEconomy } from "../map/MissionEconomy";
/** 最终伤害数据接口
* 用于封装一次攻击计算的所有结果数据
@@ -55,8 +57,8 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
* 处理伤害队列中的所有伤害事件
*/
update(e: ecs.Entity): void {
if(!smc.mission.play ) return
if(smc.mission.pause) return
if (!smc.mission.play) return
if (smc.mission.pause) return
const TAttrsComp = e.get(HeroAttrsComp)
const damageQueue = e.get(DamageQueueComp)
@@ -109,9 +111,9 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
private doAttack(target: ecs.Entity, damageEvent: DamageEvent): FinalData {
const TAttrsComp = target.get(HeroAttrsComp);
const targetView = target.get(HeroViewComp);
let reDate:FinalData={
damage:0,
isCrit:false,
let reDate: FinalData = {
damage: 0,
isCrit: false,
}
if (!TAttrsComp || TAttrsComp.is_dead || TAttrsComp.is_reviving) return reDate;
@@ -130,20 +132,20 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
const isCrit = this.checkChance(criticalChance);
// 计算基础伤害
let damage = this.dmgCount(damageEvent,TAttrsComp);
let damage = this.dmgCount(damageEvent, TAttrsComp);
mLogger.log(this.debugMode, 'HeroAtkSystem', " dmgCount",damage)
mLogger.log(this.debugMode, 'HeroAtkSystem', " dmgCount", damage)
if (isCrit) {
const critDamageBonus = damageEvent.Attrs[Attrs.critical_damage] || 0;
damage = Math.floor(damage * (1 + (FightSet.CRIT_DAMAGE + critDamageBonus) / 100));
reDate.isCrit=true;
reDate.isCrit = true;
if (damageEvent.Attrs.fac === FacSet.HERO) {
// 【评分系统 - 输出分】统计暴击次数与暴击造成的总伤害
smc.vmdata.scores.crt_count++;
smc.vmdata.scores.crit_dmg_total += damage;
}
}
mLogger.log(this.debugMode, 'HeroAtkSystem', " after crit",damage)
mLogger.log(this.debugMode, 'HeroAtkSystem', " after crit", damage)
// 护盾吸收
const shieldResult = this.absorbShield(TAttrsComp, damage);
damage = shieldResult.remainingDamage;
@@ -151,7 +153,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
// 【评分系统 - 防御分】统计护盾成功抵挡伤害的次数
smc.vmdata.scores.shield_block_count += shieldResult.absorbedDamage;
}
mLogger.log(this.debugMode, 'HeroAtkSystem', " after shield",damage)
mLogger.log(this.debugMode, 'HeroAtkSystem', " after shield", damage)
// 显示护盾吸收飘字
if (shieldResult.absorbedDamage > 0 && targetView) {
targetView.shield_tip(shieldResult.absorbedDamage);
@@ -178,7 +180,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
// targetView.back();
// }
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 英雄${TAttrsComp.hero_name} (uuid: ${TAttrsComp.hero_uuid}) 受到 eid:${casterEid} 的 伤害 ${damage},${isCrit?"暴击":"普通"}攻击,技能ID ${damageEvent.s_uuid}`);
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 英雄${TAttrsComp.hero_name} (uuid: ${TAttrsComp.hero_uuid}) 受到 eid:${casterEid} 的 伤害 ${damage},${isCrit ? "暴击" : "普通"}攻击,技能ID ${damageEvent.s_uuid}`);
// 冰冻判定
const freezeChance = (damageEvent.Attrs[Attrs.freeze_chance] || 0) - (TAttrsComp.freeze_res || 0);
@@ -273,7 +275,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
mLogger.log(this.debugMode, 'HeroAtkSystem', ` ${TAttrsComp.hero_name} 受到 ${damage} 点伤害 (暴击: ${isCrit})`);
reDate.damage=damage;
reDate.damage = damage;
return reDate;
}
@@ -295,16 +297,16 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
* @important 注意事项:
*/
private dmgCount(damageEvent:DamageEvent,TAttrsComp:HeroAttrsComp){
private dmgCount(damageEvent: DamageEvent, TAttrsComp: HeroAttrsComp) {
// 1. 获取技能配置 - 如果技能不存在直接返回0伤害
const CAttrs=damageEvent.Attrs;
const TAttrs=TAttrsComp;
const CAttrs = damageEvent.Attrs;
const TAttrs = TAttrsComp;
let sConf = SkillSet[damageEvent.s_uuid];
if (!sConf) return 0;
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 伤害处理对象`,CAttrs,TAttrs);
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 伤害处理对象`, CAttrs, TAttrs);
// 2. 计算原始物理伤害和魔法伤害
// 物理伤害基础值 = 技能物理倍率 * (施法者物理攻击力 + 额外伤害) / 100 * 额外伤害比例
let apBase = (sConf.ap||0)*(CAttrs[Attrs.ap]+damageEvent.ext_dmg)/100*damageEvent.dmg_ratio;
let apBase = (sConf.ap || 0) * (CAttrs[Attrs.ap] + damageEvent.ext_dmg) / 100 * damageEvent.dmg_ratio;
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 物理伤害基础值: ${apBase}, 技能ap=${sConf.ap},施法者物理攻击力: ${CAttrs[Attrs.ap]},}
额外伤害:${damageEvent.ext_dmg}, 额外伤害比例:${damageEvent.dmg_ratio}`);
@@ -405,10 +407,10 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
* @param damage 原始伤害值
* @returns {remainingDamage, absorbedDamage} 剩余伤害值和吸收的护盾值
*/
private absorbShield(TAttrsComp: HeroAttrsComp, damage: number): {remainingDamage: number, absorbedDamage: number} {
private absorbShield(TAttrsComp: HeroAttrsComp, damage: number): { remainingDamage: number, absorbedDamage: number } {
if (TAttrsComp.shield <= 0) {
mLogger.log(this.debugMode, 'HeroAtkSystem', " 护盾值小于等于0无法吸收伤害");
return {remainingDamage: damage, absorbedDamage: 0};
return { remainingDamage: damage, absorbedDamage: 0 };
}
TAttrsComp.shield = Math.max(0, TAttrsComp.shield - 1);
if (TAttrsComp.shield <= 0) {
@@ -416,7 +418,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
}
TAttrsComp.dirty_shield = true;
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 护盾抵挡1次伤害剩余次数 ${TAttrsComp.shield}`);
return {remainingDamage: 0, absorbedDamage: 1};
return { remainingDamage: 0, absorbedDamage: 1 };
}
/**
@@ -473,24 +475,28 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
}
/**
* 延迟执行掉落逻辑
* 怪物死亡掉落金币
*
* 采用延迟执行的原因
* 1. 避免在伤害计算过程中阻塞主线程
* 2. 给死亡动画播放留出时间
* 3. 可以批量处理多个掉落,优化性能
* 实现说明
* - 通过 hero_uuid 查 HeroInfo 获取 monType
* - 按怪物类型查 MonsterGoldSet 获取固定金币掉落数Boss 有独立的高额奖励)
* - 调用 MissionEconomy.addCoin 统一发放金币(会自动触发 UI 更新和评分统计)
*
* @param entity 死亡的怪物实体
*
* @todo 具体实现可以包括:
* - 根据怪物等级计算基础掉落
* - 幸运值影响掉落品质
* - 特殊事件(双倍掉落、稀有掉落等)
* - 掉落物在场景中的生成位置计算
*/
private scheduleDrop(entity: ecs.Entity): void {
// 这里可以添加掉落逻辑
// 例如:延迟一段时间后生成掉落物品
const TAttrsComp = entity.get(HeroAttrsComp);
if (!TAttrsComp) return;
// 通过 hero_uuid 查配置获取怪物类型
const info = HeroInfo[TAttrsComp.hero_uuid];
const monType = info?.monType ?? 0;
const gold = getMonsterGoldDrop(monType, !!TAttrsComp.is_boss);
if (gold <= 0) return;
MissionEconomy.addCoin(gold);
mLogger.log(this.debugMode, 'HeroAtkSystem',
` ${TAttrsComp.hero_name} 死亡掉落金币 ${gold}monType=${monType}, isBoss=${TAttrsComp.is_boss}`);
}
/**

View File

@@ -135,9 +135,6 @@ export class MissionCardComp extends CCComp {
// ======================== 运行时状态 ========================
/** 当前是否为战斗阶段 */
private isBattlePhase: boolean = false;
/** 四个槽位对应的 CardComp 控制器缓存(有序数组) */
private cardComps: CardComp[] = [];
/** 技能卡槽控制器缓存 */
@@ -203,7 +200,6 @@ export class MissionCardComp extends CCComp {
* 6. 执行首次抽卡并分发到 4 个槽位。
*/
onMissionStart() {
this.isBattlePhase = false;
this.enterPreparePhase();
this.poolLv = CARD_POOL_INIT_LEVEL;
const missionData = this.getMissionData();
@@ -326,11 +322,9 @@ export class MissionCardComp extends CCComp {
}
}
/** 战斗开始:不收起面板,不再强制清空卡牌 */
/** 战斗开始:保留卡牌面板,允许玩家在战斗阶段继续抽卡和召唤英雄 */
private onFightStart() {
this.isBattlePhase = true;
this.enterBattlePhase();
this.clearAllCards();
// 第一次进入战斗阶段关闭guide4
if (!smc.finish_guides.includes(4)) {
@@ -418,7 +412,6 @@ export class MissionCardComp extends CCComp {
/** 新一波:展开面板 → 刷新费用 UI → 重新抽卡分发 */
private onNewWave() {
this.isBattlePhase = false;
this.enterPreparePhase();
this.updateCoinAndCostUI();
this.layoutCardSlots();
@@ -589,12 +582,7 @@ export class MissionCardComp extends CCComp {
const payload = args ?? event;
if (!payload) return;
if (this.isBattlePhase) {
payload.cancel = true;
payload.reason = "battle_phase";
oops.gui.toast("战斗阶段无法召唤英雄");
return;
}
// 战斗阶段也允许召唤英雄(无需额外费用),仅校验英雄数量上限
const current = this.getAliveHeroCount();
this.syncMissionHeroData(current);
@@ -781,10 +769,7 @@ export class MissionCardComp extends CCComp {
* 3. 重新布局槽位 → 从卡池构建 4 张卡 → 分发到槽位。
*/
private onClickDraw() {
if (this.isBattlePhase) {
oops.gui.toast("战斗阶段无法抽卡");
return;
}
// 战斗阶段和倒计时阶段均允许刷新抽卡
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
@@ -868,22 +853,13 @@ export class MissionCardComp extends CCComp {
private enterBattlePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
// 战斗阶段允许抽卡nobg 按"金币是否足够"判断,而非强制置灰
if (this.cards_chou && this.cards_chou.isValid) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = true;
nobg.active = !this.canDrawCards();
}
}
// 战斗阶段不再隐藏抽卡面板
// Tween.stopAllByTarget(this.cards_node);
// tween(this.cards_node)
// .to(this.cardsPanelMoveDuration, { scale: this.cardsHideScale })
// .call(() => {
// if (this.cards_node && this.cards_node.isValid) {
// this.cards_node.active = false;
// }
// })
// .start();
}
/** 构建本次抽卡结果保证最终可分发3条数据 */
@@ -1098,10 +1074,11 @@ export class MissionCardComp extends CCComp {
}
private updateDrawCostUI() {
// 战斗阶段也允许抽卡nobg 统一按"金币是否足够"判断
if (this.cards_chou) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = this.isBattlePhase ? true : !this.canDrawCards();
nobg.active = !this.canDrawCards();
}
const coinNode = this.cards_chou.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);

View File

@@ -49,7 +49,6 @@ import { Timer } from "db://oops-framework/core/common/timer/Timer";
import { FieldSkillType } from "../common/config/SkillSet";
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
import { spawningEngine } from "./RogueConfig";
import { MissionEconomy } from "./MissionEconomy";
const { ccclass, property } = _decorator;
/** 任务(关卡)生命周期阶段 */
@@ -129,6 +128,10 @@ export class MissionComp extends CCComp {
}
/**秒计时 */
PhaseTime: Timer = new Timer(1)
/** 波次间倒计时(秒) */
private waveCountdown: number = 0;
/** 波次间倒计时总时长(秒) */
private readonly WAVE_COUNTDOWN_DURATION: number = 5;
/** 上一次显示的时间字符串(避免重复设置) */
private lastTimeStr: string = "";
/** 上一次显示的秒数(避免重复计算) */
@@ -159,10 +162,10 @@ export class MissionComp extends CCComp {
private currentWave: number = 0;
/** 是否为Boss波次 */
private isBossWave: boolean = false;
/** 上一次发放金币奖励的波数(防止重复发放) */
private lastPrepareCoinWave: number = 0;
/** 当前任务阶段 */
public currentPhase: MissionPhase = MissionPhase.None;
/** 是否处于波次间倒计时状态 */
private isWaveCountdown: boolean = false;
// ======================== ECS 查询匹配器(预缓存) ========================
@@ -219,9 +222,18 @@ export class MissionComp extends CCComp {
// 如果是暂停状态,且不在 BattleEnd 阶段(全灭时需要播放完 fend 技能动画并自动流转),才真正停止 update 逻辑
if (smc.mission.pause && this.currentPhase !== MissionPhase.BattleEnd) return
// 波次间倒计时PrepareStart 复用为倒计时阶段5 秒后自动进入下一波
if (this.currentPhase === MissionPhase.PrepareStart) {
this.waveCountdown -= dt;
this.updateCountdownUI();
if (this.waveCountdown <= 0) {
this.autoNextPhase();
}
return;
}
// 处理过渡阶段的计时
if (this.currentPhase === MissionPhase.PrepareStart ||
this.currentPhase === MissionPhase.PrepareEnd ||
if (this.currentPhase === MissionPhase.PrepareEnd ||
this.currentPhase === MissionPhase.BattleStart ||
this.currentPhase === MissionPhase.BattleEnd) {
if (this.PhaseTime.update(dt)) {
@@ -260,6 +272,39 @@ export class MissionComp extends CCComp {
}
}
// ======================== 波次倒计时 ========================
/** 进入波次间倒计时:重置倒计时并显示提示 */
private startWaveCountdown() {
this.waveCountdown = this.WAVE_COUNTDOWN_DURATION;
this.isWaveCountdown = true;
this.updateCountdownUI(true);
}
/** 更新倒计时 UI显示剩余秒数 */
private updateCountdownUI(force: boolean = false) {
if (!this.isWaveCountdown) return;
const remain = Math.max(0, Math.ceil(this.waveCountdown));
if (!force && remain === this.lastTimeSecond) return;
this.lastTimeSecond = remain;
if (this.time_node && this.time_node.isValid) {
const phaseNode = this.time_node.getChildByPath("Phase/Label");
if (phaseNode) {
const label = phaseNode.getComponent(Label);
if (label) {
label.string = `下一波 ${remain}s`;
}
}
}
}
/** 结束波次间倒计时 */
private stopWaveCountdown() {
this.isWaveCountdown = false;
this.lastTimeSecond = -1;
}
// ======================== 奖励与广告 ========================
/** 奖励发放(预留) */
@@ -425,7 +470,13 @@ export class MissionComp extends CCComp {
smc.mission.in_fight = false;
smc.vmdata.mission_data.in_fight = false;
smc.mission.stop_spawn_mon = true;
// 不隐藏开始按钮,点击事件在 onStartFightBtnClick 内部做了阶段拦截
// 波次间倒计时隐藏开始按钮nobg 激活表示不可点击状态)
if (this.start_btn && this.start_btn.isValid) {
const nobg = this.start_btn.getChildByName("nobg");
if (nobg) nobg.active = true;
}
// 启动 5 秒倒计时,由 update 驱动自动进入下一波
this.startWaveCountdown();
oops.message.dispatchEvent("PhasePrepareStart");
break;
@@ -526,7 +577,9 @@ export class MissionComp extends CCComp {
private autoNextPhase() {
switch (this.currentPhase) {
case MissionPhase.PrepareStart:
this.changePhase(MissionPhase.Prepare);
// 波次间倒计时结束,停止倒计时状态,直接进入 PrepareEnd不再等待玩家点击
this.stopWaveCountdown();
this.changePhase(MissionPhase.PrepareEnd);
break;
case MissionPhase.PrepareEnd:
this.changePhase(MissionPhase.BattleStart);
@@ -728,7 +781,6 @@ export class MissionComp extends CCComp {
this.heapTrendTimer = 0;
this.heapTrendBaseMB = -1;
this.monsterCountSyncTimer = 0;
this.lastPrepareCoinWave = 0;
spawningEngine.reset();
@@ -746,8 +798,9 @@ export class MissionComp extends CCComp {
* 新一波事件回调:
* 1. 进入准备阶段。
* 2. 更新当前波数。
* 3. 发放本波金币奖励
* 4. 刷新时间显示。
* 3. 刷新时间显示
*
* 注意:金币不再按波次固定发放,改为怪物死亡时掉落(见 HeroAtkSystem
*
* @param event 事件名
* @param data { wave: number }
@@ -772,7 +825,7 @@ export class MissionComp extends CCComp {
this.currentWave = wave;
smc.vmdata.mission_data.level = wave;
this.grantPrepareCoinByWave(wave);
// 金币改为怪物死亡掉落(见 HeroAtkSystem不再每波固定发放
this.lastTimeSecond = -1;
this.clearTime = 0;
this.update_time();
@@ -794,26 +847,6 @@ export class MissionComp extends CCComp {
}
}
/**
* 按波数发放固定金币奖励(外加技能加成)。
* 第1波不发放使用初始金币从第2波起发放。
* 仅在波数首次到达时发放,防止重复。
*
* @param wave 当前波数
*/
private grantPrepareCoinByWave(wave: number) {
if (wave <= 1) return;
if (wave <= this.lastPrepareCoinWave) return;
// 波次金币公式: baseReward + (wave-1) * waveGrow且不超过 prepareMaxCoinReward
const calculatedReward = FightSet.WAVE_COIN_BASE + (wave - 1) * FightSet.WAVE_COIN_GROW;
const waveReward = Math.min(FightSet.WAVE_COIN_MAX, calculatedReward);
const reward = MissionEconomy.executeWaveGold(waveReward);
this.lastPrepareCoinWave = wave;
mLogger.log(this.debugMode, "MissionComp", "grantPrepareCoinByWave", { wave, waveReward, reward, coin: smc.vmdata.mission_data.coin });
}
// ======================== 怪物数量管理 ========================
/**

View File

@@ -76,6 +76,40 @@ for (const key in HeroInfo) {
}
}
// ======================== 2.5 怪物金币掉落配置 ========================
/**
* 怪物金币掉落配置(按 MonType 分类)
* - base: 普通怪物死亡时掉落的固定金币数
* - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效)
*
* 设计说明:
* 金币不再按波次固定发放,改为每只怪物死亡时掉落。
* Boss 提供高额固定金币奖励,作为战斗收益的核心来源。
*/
export const MonsterGoldSet: Record<number, { base: number; boss: number }> = {
[MonType.Melee]: { base: 1, boss: 0 },
[MonType.Heavy]: { base: 2, boss: 0 },
[MonType.Long]: { base: 2, boss: 0 },
[MonType.Support]: { base: 3, boss: 0 },
[MonType.Summoner]: { base: 3, boss: 0 },
[MonType.Assassin]: { base: 3, boss: 0 },
[MonType.MeleeBoss]: { base: 0, boss: 15 },
[MonType.LongBoss]: { base: 0, boss: 15 },
};
/**
* 获取指定怪物类型的金币掉落数量
* @param monType 怪物类型MonType
* @param isBoss 是否为 Boss
* @returns 掉落的金币数量≥0
*/
export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
const cfg = MonsterGoldSet[monType];
if (!cfg) return 0;
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
}
// ======================== 3. 波次级强化库 ========================
/**
@@ -162,11 +196,11 @@ export const WaveConfigs: Record<number, WaveConfig> = {
6: { base_count: 7, squad_pool: ["assassin_squad", "long_line", "mixed_balanced"], enchant_pool: ["frenzy"], elite_base_rate: 0.10 },
7: { base_count: 8, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.12 },
8: { base_count: 8, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.14 },
9: { base_count: 9, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"],elite_base_rate: 0.16 },
9: { base_count: 9, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.16 },
// ===== 第二 Boss =====
10: { base_count: 10, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide"],boss_wave: true, elite_base_rate: 0.18 },
10: { base_count: 10, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.18 },
// ===== 组合多样化 =====
11: { base_count: 10, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"],enchant_pool: ["swift", "frenzy"], elite_base_rate: 0.18 },
11: { base_count: 10, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], enchant_pool: ["swift", "frenzy"], elite_base_rate: 0.18 },
12: { base_count: 10, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.20 },
13: { base_count: 11, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.22 },
14: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], enchant_pool: ["ironhide", "swift"], elite_base_rate: 0.24 },