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

@@ -0,0 +1,143 @@
/**
* @file BattleBannerComp.ts
* @description 战斗横幅统一通道组件(表现层)
*
* 职责:
* 1. 监听清屏WaveClear/ Boss 预警BossWarning/ 连杀ComboReach事件播分级横幅。
* 2. 横幅动画右侧飞入backOut→ 中央停留 → 左侧飞出backIn与 MissionComp.playTooltipAnim 同风格。
* 3. 清屏奖励结算fastClear 金币 / allAlive 刷新石),走 MissionEconomy 静态接口。
* 4. Boss 登场BossSpawn触发震屏 + 音效。
*
* 使用 oops-framework 模块oops.message事件解耦、oops.audio音效
* 编辑器绑定bannerNodeLabel+背景节点,初始 active=false
*/
import { _decorator, Node, Label, tween, Tween, v3, Color } 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 { MissionEconomy } from "./MissionEconomy";
import { ScreenShake } from "../common/ScreenShake";
import { WAVE_DURATION } from "./RogueConfig";
const { ccclass, property } = _decorator;
/** 清屏奖励:迅速清场(< 75% 时长) */
const FAST_CLEAR_COIN = 2;
/** 清屏奖励:极速清场(< 50% 时长) */
const ULTRA_CLEAR_COIN = 5;
/** 连杀分级金币爆发tier 0/1/2 对应 5/10/20 连杀) */
const COMBO_COIN = [2, 5, 10];
@ccclass('BattleBannerComp')
export class BattleBannerComp extends CCComp {
/** 横幅节点Label+背景,初始隐藏),编辑器拖拽绑定 */
@property({ type: Node, tooltip: "横幅节点Label+背景,初始 active=false" })
bannerNode: Node | null = null;
/** 上次横幅播放时间戳去抖0.3s 内的新横幅直接打断旧的,不叠加 tween */
private lastBannerTime: number = 0;
onLoad() {
// oops.message 全局事件总线:清屏 / Boss 预警 / Boss 登场 / 连杀
oops.message.on(GameEvent.WaveClear, this.onWaveClear, this);
oops.message.on(GameEvent.BossWarning, this.onBossWarning, this);
oops.message.on(GameEvent.BossSpawn, this.onBossSpawn, this);
oops.message.on(GameEvent.ComboReach, this.onComboReach, this);
}
onDestroy() {
oops.message.off(GameEvent.WaveClear, this.onWaveClear, this);
oops.message.off(GameEvent.BossWarning, this.onBossWarning, this);
oops.message.off(GameEvent.BossSpawn, this.onBossSpawn, this);
oops.message.off(GameEvent.ComboReach, this.onComboReach, this);
}
// ======================== 事件处理 ========================
/** 清屏庆祝:分级文案 + 奖励结算 */
private onWaveClear(event: string, data: { wave: number; clearTime: number; allAlive: boolean; fastClear: number }) {
if (data.allAlive) {
this.playBanner("Perfect!", new Color(255, 80, 80), 1.2);
MissionEconomy.addRefreshStone(1);
oops.audio.playEffect("music/flash");
} else if (data.fastClear === 2) {
this.playBanner("极速清场!", new Color(255, 170, 0), 1.15);
MissionEconomy.addCoin(ULTRA_CLEAR_COIN);
oops.audio.playEffect("music/flash");
} else if (data.fastClear === 1) {
this.playBanner("迅速清场!", new Color(255, 220, 60), 1.05);
MissionEconomy.addCoin(FAST_CLEAR_COIN);
} else {
this.playBanner("Clear!", new Color(255, 255, 255), 1.0);
}
}
/** Boss 回合预警:红字横幅 */
private onBossWarning(event: string, data: { wave: number; eta: number }) {
this.playBanner("强敌来袭!", new Color(255, 60, 60), 1.3);
oops.audio.playEffect("music/flash");
}
/** Boss 登场:震屏 + 闷响 */
private onBossSpawn() {
ScreenShake.shake(10, 0.4);
oops.audio.playEffect("music/dun");
}
/** 连杀达阈值:分级文案 + 震屏 + 金币爆发 */
private onComboReach(event: string, data: { count: number; tier: number }) {
const tier = Math.min(data.tier, 2);
const colors = [new Color(255, 230, 80), new Color(255, 150, 30), new Color(255, 60, 60)];
const scales = [1.0, 1.2, 1.4];
this.playBanner(`${data.count} 连杀!`, colors[tier], scales[tier]);
if (tier === 1) ScreenShake.shake(4, 0.2);
if (tier === 2) ScreenShake.shake(8, 0.3);
MissionEconomy.addCoin(COMBO_COIN[tier]);
// 占位音效分级(后续可替换为升调 combo 音效)
oops.audio.playEffect(tier === 2 ? "music/Fire" : tier === 1 ? "music/Critical" : "music/Hit");
}
// ======================== 横幅播放 ========================
/**
* 播放横幅:右进 → 中央停留 → 左出
* @param text 文案
* @param color 文字颜色
* @param scale 整体缩放(分级表现力)
*/
private playBanner(text: string, color: Color, scale: number = 1.0) {
if (!this.bannerNode || !this.bannerNode.isValid) return;
// 去抖0.3s 内的新横幅直接打断旧的
const now = Date.now();
if (now - this.lastBannerTime < 300) {
Tween.stopAllByTarget(this.bannerNode);
}
this.lastBannerTime = now;
this.bannerNode.active = true;
this.bannerNode.setScale(v3(scale, scale, 1));
const label = this.bannerNode.getComponentInChildren(Label);
if (label) {
label.string = text;
label.color = color;
label.updateRenderData(true);
}
Tween.stopAllByTarget(this.bannerNode);
const startPos = v3(1200, 0, 0);
const centerPos = v3(0, 0, 0);
const endPos = v3(-1200, 0, 0);
this.bannerNode.setPosition(startPos);
tween(this.bannerNode)
.to(0.4, { position: centerPos }, { easing: "backOut" })
.to(0.8, { position: v3(-40, 0, 0) }, { easing: "sineInOut" })
.to(0.35, { position: endPos }, { easing: "backIn" })
.call(() => {
this.bannerNode!.active = false;
})
.start();
}
}