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:
143
assets/script/game/map/BattleBannerComp.ts
Normal file
143
assets/script/game/map/BattleBannerComp.ts
Normal 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(音效)。
|
||||
* 编辑器绑定: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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user