新增连杀系统、金币飞行特效、波次HUD、屏幕震动等表现功能,重构怪物刷出逻辑与难度动态调节,调整回合回血比例,优化游戏节奏与体验。 主要变更: 1. 调整回合回血比例从0.5到0.4,优化前期节奏 2. 新增连杀计数与奖励系统,支持5/10/20连杀触发对应表现 3. 实现怪物死亡掉落金币的抛物线飞行特效 4. 增加波次进度HUD,显示剩余怪物与全局回合进度 5. 新增屏幕震动工具与战斗横幅统一展示系统 6. 重构怪物刷出逻辑,支持按回合类型调整刷怪间隔,加入清场加速机制 7. 优化动态难度调节算法,增加滞回与指数平滑,避免难度突变 8. 新增Boss预警、登场事件与回合清屏事件,完善事件总线 9. 调整怪物数量阈值与回合倒计时档位,适配新的节奏设计
144 lines
6.1 KiB
TypeScript
144 lines
6.1 KiB
TypeScript
/**
|
||
* @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(音效)。
|
||
* 编辑器绑定:bannerNode(Label+背景节点,初始 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();
|
||
}
|
||
}
|