feat: 添加战斗结算奖励系统

1.  新增结算奖励配置常量,定义金币、天赋经验、英雄碎片的发放规则与胜利加成系数
2.  新增RewardItemComp通用奖励条目组件,实现数量展示与弹跳入场动画
3.  为VictoryComp添加结算奖励逻辑:计算发放奖励、渲染奖励UI并添加入场动画效果
4.  新增金币、天赋经验奖励预制体并配置到结算弹窗中
5.  调整ui3.plist的图片边框参数适配新奖励图标
This commit is contained in:
pan
2026-09-01 10:18:55 +08:00
parent 89cc7e25b2
commit f40ff033c2
13 changed files with 3492 additions and 2300 deletions

View File

@@ -16,6 +16,11 @@ import { getRankByScore, getNextRank, getRankProgress, ScoreRankInfo } from "../
import { LangPrefix, lang, langf } from "../common/LangUtil";
import { mLogger } from "../common/Logger";
import { UIID } from "../common/config/GameUIConfig";
import { VICTORY_REWARD_SET } from "../common/config/GameSet";
import { HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { ChipComp } from "./ChipComp";
import { RewardItemComp } from "./RewardItemComp";
const { ccclass, property } = _decorator;
@@ -23,6 +28,28 @@ const { ccclass, property } = _decorator;
const SCORE_TWEEN_DURATION = 1.6;
/** 段位晋升动效时长(秒) */
const RANK_UP_DURATION = 0.5;
/** 奖励条目逐个入场的间隔(秒) */
const REWARD_ENTER_INTERVAL = 0.12;
/** 单个英雄碎片奖励 */
interface FragmentReward {
/** 英雄模板 uuid */
uuid: number;
/** 碎片数量 */
count: number;
/** 是否为本局出场英雄(否则为随机英雄) */
isPlayed: boolean;
}
/** 结算奖励结果 */
interface RewardResult {
/** 发放的金币数量 */
gold: number;
/** 发放的天赋经验数量 */
talentExp: number;
/** 英雄碎片奖励列表 */
fragments: FragmentReward[];
}
/**
* VictoryComp —— 战斗结算弹窗视图组件
@@ -69,6 +96,20 @@ export class VictoryComp extends CCComp {
@property({ type: Prefab, tooltip: "亮点成就标签预制体" })
highlight_prefab: Prefab = null!;
// ======================== 结算奖励绑定 ========================
@property({ type: Node, tooltip: "结算奖励条目容器(rewards_container,横向 Layout)" })
rewards_container: Node = null!;
@property({ type: Prefab, tooltip: "英雄碎片条目预制体(clips.prefab,挂 ChipComp,num 子节点显示数量)" })
clip_prefab: Prefab = null!;
@property({ type: Prefab, tooltip: "金币奖励条目预制体(reward_item_gold.prefab,挂 RewardItemComp)" })
reward_gold_prefab: Prefab = null!;
@property({ type: Prefab, tooltip: "天赋经验奖励条目预制体(reward_item_exp.prefab,挂 RewardItemComp)" })
reward_exp_prefab: Prefab = null!;
/** 调试日志开关 */
debugMode: boolean = false;
/** 奖励等级(预留) */
@@ -84,6 +125,11 @@ export class VictoryComp extends CCComp {
diamond: 0
}
// ======================== 结算奖励(运行时) ========================
/** 本次结算实际发放的奖励结果(供渲染) */
private _rewardResult: RewardResult = null!;
// ======================== 动画状态 ========================
/** 动画期间实时显示的分数(tween 驱动) */
@@ -128,6 +174,9 @@ export class VictoryComp extends CCComp {
// 计算总分并写入历史纪录
this.calculateTotalScore();
// 计算并发放结算奖励(英雄碎片 + 金币 + 天赋经验)
this.grantRewards(!!args.victory);
// 每次战斗结束都标记数据脏并触发云端同步(防抖 3 秒后自动上传)
smc.updateCloudData();
@@ -136,6 +185,9 @@ export class VictoryComp extends CCComp {
// 渲染亮点标签
this.renderHighlights();
// 渲染结算奖励条目
this.renderRewards();
}
// ======================== 分数计算(逻辑层,保持不变) ========================
@@ -440,6 +492,145 @@ export class VictoryComp extends CCComp {
.start();
}
// ======================== 结算奖励 ========================
/**
* 收集本局出场英雄的 uuid 列表(去重)。
* 来源 = 存活英雄(heroGrid 中实体的 hero_uuid)+ 死亡英雄快照(dead_heroes)。
*/
private collectPlayedHeroUuids(): number[] {
const uuidSet = new Set<number>();
// 存活英雄:heroGrid 存 eid,反查实体属性
for (const eid of smc.mission.heroGrid) {
if (eid < 0) continue;
const entity = ecs.getEntityByEid(eid);
const attrs = entity?.get(HeroAttrsComp);
if (attrs && attrs.fac === 0) {
uuidSet.add(attrs.hero_uuid);
}
}
// 死亡英雄快照
const deadList = smc.mission.dead_heroes || [];
for (const rec of deadList) {
if (rec && rec.uuid > 0) uuidSet.add(rec.uuid);
}
return [...uuidSet];
}
/**
* 从全英雄池中随机抽取 1 个英雄 uuid(优先未出场,避免与出场碎片重复)。
*/
private pickRandomHeroUuid(exclude: Set<number>): number {
const all = Object.keys(HeroInfo).map(k => Number(k)).filter(u => u > 0);
const candidates = all.filter(u => !exclude.has(u));
const pool = candidates.length > 0 ? candidates : all;
if (pool.length === 0) return 0;
return pool[Math.floor(Math.random() * pool.length)];
}
/**
* 计算并发放结算奖励,结果存入 _rewardResult。
* @param isVictory 是否胜利(胜利整体 ×1.2)
*/
private grantRewards(isVictory: boolean) {
const s = smc.vmdata.scores;
const cfg = VICTORY_REWARD_SET;
const mult = isVictory ? cfg.victory_multiplier : 1.0;
const wave = Math.max(0, s.wave_win_count || 0);
// 1. 金币 / 天赋经验(按波次计算,胜利加成)
const gold = Math.floor((cfg.gold.base + wave * cfg.gold.per_wave) * mult);
const talentExp = Math.floor((cfg.talent_exp.base + wave * cfg.talent_exp.per_wave) * mult);
// 2. 英雄碎片:出场英雄各保底 + 1 个随机英雄
const playedUuids = this.collectPlayedHeroUuids();
const playedSet = new Set(playedUuids);
const fragments: FragmentReward[] = playedUuids.map(uuid => ({
uuid,
count: cfg.played_fragment,
isPlayed: true,
}));
const randomUuid = this.pickRandomHeroUuid(playedSet);
if (randomUuid > 0) {
fragments.push({ uuid: randomUuid, count: cfg.random_fragment, isPlayed: false });
}
// 3. 落地发放
if (gold > 0) {
smc.data.gold = (smc.data.gold || 0) + gold; // 持久化(存档口径)
smc.updateGold(gold); // 驱动 MVVM 展示
}
if (talentExp > 0) {
smc.addTalentExp(talentExp);
}
for (const f of fragments) {
smc.addHeroFragments(f.uuid, f.count);
}
this._rewardResult = { gold, talentExp, fragments };
mLogger.log(this.debugMode, 'VictoryComp', '[VictoryComp] 结算奖励发放', {
isVictory, wave, gold, talentExp, fragments,
});
}
/**
* 渲染结算奖励条目(金币 / 天赋经验 / 英雄碎片),逐个弹跳入场。
*/
private renderRewards() {
if (!this.rewards_container || !this._rewardResult) return;
this.rewards_container.removeAllChildren();
const items: Node[] = [];
const r = this._rewardResult;
// 金币条目
if (this.reward_gold_prefab && r.gold > 0) {
const node = instantiate(this.reward_gold_prefab);
node.getComponent(RewardItemComp)?.setReward(r.gold);
items.push(node);
}
// 天赋经验条目
if (this.reward_exp_prefab && r.talentExp > 0) {
const node = instantiate(this.reward_exp_prefab);
node.getComponent(RewardItemComp)?.setReward(r.talentExp);
items.push(node);
}
// 英雄碎片条目(复用 clips.prefab + ChipComp,num 子节点显示数量)
if (this.clip_prefab) {
for (const f of r.fragments) {
const node = instantiate(this.clip_prefab);
node.getComponent(ChipComp)?.setHeroByUuid(f.uuid);
const numLabel = node.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `+${f.count}`;
numLabel.node.active = true;
}
items.push(node);
}
}
// 逐个入场(初始隐藏,间隔弹出)
items.forEach((node, i) => {
node.active = false;
this.rewards_container.addChild(node);
this.scheduleOnce(() => {
node.active = true;
// 碎片条目没有 RewardItemComp,手动播一次弹跳
if (!node.getComponent(RewardItemComp)) {
Tween.stopAllByTarget(node);
node.setScale(0, 0, 1);
tween(node).to(0.35, { scale: v3(1, 1, 1) }, { easing: 'backOut' }).start();
}
}, REWARD_ENTER_INTERVAL * (i + 1));
});
}
// ======================== 亮点成就 ========================
/**
@@ -524,9 +715,10 @@ export class VictoryComp extends CCComp {
}
protected onDestroy(): void {
// 停止所有动画,防止节点销毁后 tween 回调访问无效对象
// 停止所有动画与延迟回调,防止节点销毁后 tween/scheduleOnce 访问无效对象
if (this.rank_icon) Tween.stopAllByTarget(this.rank_icon.node);
if (this.gain_label) Tween.stopAllByTarget(this.gain_label.node);
this.unscheduleAllCallbacks();
super.onDestroy();
mLogger.log(this.debugMode, 'VictoryComp', "释放胜利界面");
}