1. 重构全局配置,移除刷新石相关定义,新增银币刷新消耗常量 2. 新增局外持久货币银币的数据结构与管理逻辑 3. 重构卡牌刷新经济系统,统一使用银币进行刷新消耗 4. 更新UI显示与事件通知,适配银币系统变更 5. 调整清屏奖励逻辑,将刷新石奖励改为金币奖励
146 lines
6.2 KiB
TypeScript
146 lines
6.2 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;
|
||
/** 清屏奖励:全员存活(allAlive)额外金币 */
|
||
const ALL_ALIVE_COIN = 3;
|
||
/** 连杀分级金币爆发(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.addCoin(ALL_ALIVE_COIN);
|
||
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();
|
||
}
|
||
}
|