feat: 完成肉鸽回合制游戏核心玩法迭代

新增连杀系统、金币飞行特效、波次HUD、屏幕震动等表现功能,重构怪物刷出逻辑与难度动态调节,调整回合回血比例,优化游戏节奏与体验。

主要变更:
1.  调整回合回血比例从0.5到0.4,优化前期节奏
2.  新增连杀计数与奖励系统,支持5/10/20连杀触发对应表现
3.  实现怪物死亡掉落金币的抛物线飞行特效
4.  增加波次进度HUD,显示剩余怪物与全局回合进度
5.  新增屏幕震动工具与战斗横幅统一展示系统
6.  重构怪物刷出逻辑,支持按回合类型调整刷怪间隔,加入清场加速机制
7.  优化动态难度调节算法,增加滞回与指数平滑,避免难度突变
8.  新增Boss预警、登场事件与回合清屏事件,完善事件总线
9.  调整怪物数量阈值与回合倒计时档位,适配新的节奏设计
This commit is contained in:
pan
2026-08-11 19:00:23 +08:00
parent d57f4c14e0
commit 409113e269
12 changed files with 904 additions and 80 deletions

View File

@@ -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 } from "./RogueConfig";
import { spawningEngine, MAX_WAVE, DynamicTuner, WAVE_TIMEOUT, WAVE_TIMEOUT_BOSS, BATCH_INTERVAL, BATCH_COUNT, WAVE_DURATION } from "./RogueConfig";
const { ccclass, property } = _decorator;
/** 任务(关卡)生命周期阶段 */
@@ -82,10 +82,10 @@ export class MissionComp extends CCComp {
// ======================== 配置参数 ========================
/** 怪物数量上限(超过后暂停刷怪) */
private maxMonsterCount: number = 80;
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
private resumeMonsterCount: number = 45;
/** 怪物数量上限(超过后暂停刷怪):略高于单回合上限 MAX_MONSTERS(54),只有跨回合堆积才触发泄压 */
private maxMonsterCount: number = 60;
/** 怪物数量恢复阈值(降至此值以下恢复刷怪,需留空间接纳后续批次,单批最大约 18 只 */
private resumeMonsterCount: number = 40;
// ======================== 编辑器绑定节点 ========================
@@ -128,8 +128,12 @@ export class MissionComp extends CCComp {
PhaseTime: Timer = new Timer(1)
/** 回合间倒计时(秒) */
private waveCountdown: number = 0;
/** 回合间倒计时总时长(秒 */
private readonly WAVE_COUNTDOWN_DURATION: number = 5;
/** 倒计时档位:快清场(压缩垃圾时间,奖励强 build */
private static readonly COUNTDOWN_FAST = 2.5;
/** 倒计时档位:普通 */
private static readonly COUNTDOWN_NORMAL = 4.0;
/** 倒计时档位:有英雄死亡(保留满运营窗口:调整阵型/复活/买卡) */
private static readonly COUNTDOWN_FULL = 5.0;
/** 上一次显示的时间字符串(避免重复设置) */
private lastTimeStr: string = "";
/** 上一次显示的秒数(避免重复计算) */
@@ -160,6 +164,12 @@ export class MissionComp extends CCComp {
private currentWave: number = 0;
/** 是否为Boss回合 */
private isBossWave: boolean = false;
/** 超时回合已在 onBattleTimeout 预扣留存分BattleEnd 需跳过重复累加 */
private skipRemainScoreOnBattleEnd: boolean = false;
/** 上回合英雄死亡数快照BattleEnd 写入,供下一回合倒计时档位判定) */
private lastWaveDeathCount: number = 0;
/** 上回合计时口径清场时间快照(含清场加速还原,供下一回合倒计时档位判定) */
private lastWaveClearTime: number = 0;
/** 当前任务阶段 */
public currentPhase: MissionPhase = MissionPhase.None;
/** 是否处于回合间倒计时状态 */
@@ -248,6 +258,12 @@ export class MissionComp extends CCComp {
smc.vmdata.mission_data.fight_time += dt
this.clearTime += dt
this.update_time();
// 回合超时兜底:僵局(坦克 vs 高血 Boss 互磨不死时强制收束回合Boss 回合阈值放宽
const timeout = this.isBossWave ? WAVE_TIMEOUT_BOSS : WAVE_TIMEOUT;
if (this.clearTime >= timeout) {
this.onBattleTimeout();
}
}
}
@@ -275,13 +291,20 @@ export class MissionComp extends CCComp {
// ======================== 回合倒计时 ========================
/** 进入回合间倒计时:重置倒计时并显示提示 */
/** 进入回合间倒计时:按上回合战况分档(快清场压缩垃圾时间,有死亡保留运营窗口) */
private startWaveCountdown() {
this.waveCountdown = this.WAVE_COUNTDOWN_DURATION;
this.waveCountdown = this.computeCountdown();
this.isWaveCountdown = true;
this.updateCountdownUI(true);
}
/** 计算下一回合倒计时档位:读 BattleEnd 写入的快照onNewWave 会清零 clearTime不能现读 */
private computeCountdown(): number {
if (this.lastWaveDeathCount > 0) return MissionComp.COUNTDOWN_FULL;
if (this.lastWaveClearTime > 0 && this.lastWaveClearTime < WAVE_DURATION * 0.65) return MissionComp.COUNTDOWN_FAST;
return MissionComp.COUNTDOWN_NORMAL;
}
/** 更新倒计时 UI显示剩余秒数 */
private updateCountdownUI(force: boolean = false) {
if (!this.isWaveCountdown) return;
@@ -491,12 +514,21 @@ export class MissionComp extends CCComp {
case MissionPhase.PrepareEnd:
// 不隐藏开始按钮
// 开闸刷怪MissionMonComp 在本阶段启动分批释放,需解除暂停标志让第一批按时出场
smc.mission.stop_spawn_mon = false;
oops.message.dispatchEvent("PhasePrepareEnd");
break;
case MissionPhase.BattleStart:
// 触发战斗开始技能fstart
this.triggerHeroBattleSkills(true);
// Boss 回合预警战斗正式开始、Boss 压轴进场(最后一批),提前给 UI 表现窗口
if (this.isBossWave) {
oops.message.dispatchEvent(GameEvent.BossWarning, {
wave: this.currentWave,
eta: BATCH_INTERVAL * (BATCH_COUNT - 1),
});
}
break;
case MissionPhase.Battle:
@@ -520,7 +552,11 @@ export class MissionComp extends CCComp {
// 【评分系统 - 战绩分】每回合胜利加分
smc.vmdata.scores.wave_win_count++;
// 【评分系统 - 战绩分】记录每回合结束时场上留存的敌人数量(扣分项)
smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num;
// 超时回合已在 onBattleTimeout 预扣,跳过防重
if (!this.skipRemainScoreOnBattleEnd) {
smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num;
}
this.skipRemainScoreOnBattleEnd = false;
let allAlive = true;
let hasHero = false;
@@ -537,8 +573,15 @@ export class MissionComp extends CCComp {
});
// 【动态难度调节】根据本回合战况自动放水 / 加压
DynamicTuner.adjust(this.clearTime, heroDeathCount);
mLogger.log(this.debugMode, 'MissionComp', `[DynamicTuner] wave=${this.currentWave} clearTime=${this.clearTime.toFixed(1)}s deaths=${heroDeathCount} factor=${DynamicTuner.factor.toFixed(2)}`);
// 清场加速节省的时间还原进口径,避免"打得好"被节奏加速+强度加压双重惩罚
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必须提前快照
this.lastWaveDeathCount = heroDeathCount;
this.lastWaveClearTime = effectiveClearTime;
// 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分)
if (hasHero && allAlive) {
smc.vmdata.scores.wave_all_alive_count++;
@@ -553,7 +596,7 @@ export class MissionComp extends CCComp {
// 触发战斗结束技能fend
this.triggerHeroBattleSkills(false);
// 战斗结束阶段,给予所有英雄恢复70%血量的技能效果
// 战斗结束阶段,按 FightSet.WAVE_HEAL_RATE 恢复所有英雄血量
this.healAllHeroes();
// 【新增】派发每回合战斗结束事件,供卡牌技能监听(区别于整局结束的 MissionEnd
@@ -674,7 +717,7 @@ export class MissionComp extends CCComp {
}
/**
* 战斗结束阶段治疗所有英雄(包括墓地英雄),恢复70%最大生命值
* 战斗结束阶段治疗所有英雄(包括墓地英雄),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例
*/
private healAllHeroes() {
const healRateBoost = FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.WaveHeal);
@@ -788,6 +831,9 @@ export class MissionComp extends CCComp {
this.currentPhase = MissionPhase.None;
this.currentWave = 1;
this.isBossWave = false;
this.skipRemainScoreOnBattleEnd = false;
this.lastWaveDeathCount = 0;
this.lastWaveClearTime = 0;
this.rewards = []
this.revive_times = 1;
this.lastTimeStr = "";
@@ -901,6 +947,16 @@ export class MissionComp extends CCComp {
// 20 回合通关
this.open_Victory(null, false);
} else {
// 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;
oops.message.dispatchEvent(GameEvent.WaveClear, {
wave: this.currentWave,
clearTime: this.clearTime,
allAlive,
fastClear,
});
oops.message.dispatchEvent("TimeUpAdvanceWave");
}
return;
@@ -919,6 +975,20 @@ export class MissionComp extends CCComp {
if (monsterCount >= max) smc.mission.stop_spawn_mon = true;
}
/** 检测场上英雄是否全员存活(清屏 Perfect 判定用,独立于评分统计逻辑) */
private checkAllHeroAlive(): boolean {
let hasHero = false;
let allAlive = true;
ecs.query(this.heroAttrsMatcher).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
if (attrs && attrs.fac === FacSet.HERO) {
hasHero = true;
if (attrs.is_dead) allAlive = false;
}
});
return hasHero && allAlive;
}
/**
* 英雄全灭检测:若场上无存活英雄且处于战斗中,触发结算弹窗。
* @param heroCount 当前存活英雄数量
@@ -930,6 +1000,36 @@ export class MissionComp extends CCComp {
this.open_Victory(null, true);
}
/**
* 回合超时强制结束(僵局兜底):
* 1. 预扣留存分(残留怪销毁后 mon_num 会被下一次同步清零BattleEnd 的累加会漏扣,故在此显式预扣并置防重标志)。
* 2. 销毁全部残留怪(不触发 MonDead、不掉金币——超时是对"清不掉"的惩罚)。
* 3. 复用正常回合推进流DynamicTuner 因 clearTime 远超阈值会自动放水,无需特殊处理。
*/
private onBattleTimeout() {
// 防重入:同一回合只触发一次
if (this.currentPhase !== MissionPhase.Battle) return;
smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num;
this.skipRemainScoreOnBattleEnd = true;
ecs.query(this.heroAttrsMatcher).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) {
entity.destroy();
}
});
smc.vmdata.mission_data.mon_num = 0;
mLogger.log(this.debugMode, 'MissionComp', `[WaveTimeout] wave=${this.currentWave} 超时强制结束`);
if (this.currentWave >= MAX_WAVE) {
this.open_Victory(null, false);
} else {
oops.message.dispatchEvent("TimeUpAdvanceWave");
}
}
// ======================== 清理 ========================
/** 清理所有英雄和技能 ECS 实体 */