feat: 添加战斗结算奖励系统
1. 新增结算奖励配置常量,定义金币、天赋经验、英雄碎片的发放规则与胜利加成系数 2. 新增RewardItemComp通用奖励条目组件,实现数量展示与弹跳入场动画 3. 为VictoryComp添加结算奖励逻辑:计算发放奖励、渲染奖励UI并添加入场动画效果 4. 新增金币、天赋经验奖励预制体并配置到结算弹窗中 5. 调整ui3.plist的图片边框参数适配新奖励图标
This commit is contained in:
@@ -211,3 +211,20 @@ export function getLvColor(lv: number): Color {
|
||||
* 用于 BG_node 下子节点的显隐切换
|
||||
*/
|
||||
export const CARD_LV_BG_COLORS: string[] = ["green", "blue", "purple", "yellow", "red"];
|
||||
|
||||
/**
|
||||
* 战斗结算奖励配置(每局结束后发放:英雄碎片 + 金币 + 天赋经验)。
|
||||
* 数值均为"失败基准",胜利时整体乘以 victory_multiplier。
|
||||
*/
|
||||
export const VICTORY_REWARD_SET = {
|
||||
/** 金币:base + wave_win_count × per_wave */
|
||||
gold: { base: 100, per_wave: 50 },
|
||||
/** 天赋经验:base + wave_win_count × per_wave */
|
||||
talent_exp: { base: 20, per_wave: 10 },
|
||||
/** 每个出场英雄保底碎片数 */
|
||||
played_fragment: 1,
|
||||
/** 随机英雄碎片数量(发放给随机抽中的 1 个英雄) */
|
||||
random_fragment: 2,
|
||||
/** 胜利加成系数(失败 1.0) */
|
||||
victory_multiplier: 1.2,
|
||||
};
|
||||
|
||||
0
assets/script/game/common/config/ItemSet.ts
Normal file
0
assets/script/game/common/config/ItemSet.ts
Normal file
9
assets/script/game/common/config/ItemSet.ts.meta
Normal file
9
assets/script/game/common/config/ItemSet.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "18fae899-4daa-406a-806a-2857fad8b648",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
60
assets/script/game/map/RewardItemComp.ts
Normal file
60
assets/script/game/map/RewardItemComp.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file RewardItemComp.ts
|
||||
* @description 结算奖励条目组件(UI 视图层)
|
||||
*
|
||||
* 挂载在 reward_item.prefab 上,用于展示单个非英雄碎片类奖励(金币 / 天赋经验等)。
|
||||
* 图标在编辑器内直接配置好(Sprite 静态图),组件只负责:
|
||||
* 1. 设置数量文本("+N")。
|
||||
* 2. 播放入场弹跳动画(scale 0 → 1,backOut)。
|
||||
*
|
||||
* 英雄碎片奖励不使用本组件,而是复用 clips.prefab + ChipComp(见 VictoryComp.renderRewards)。
|
||||
*/
|
||||
import { _decorator, Label, tween, v3, Tween } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 奖励条目入场动画时长(秒) */
|
||||
const ENTER_DURATION = 0.35;
|
||||
|
||||
/**
|
||||
* RewardItemComp —— 单个结算奖励条目视图组件
|
||||
*/
|
||||
@ccclass('RewardItemComp')
|
||||
@ecs.register('RewardItemComp', false)
|
||||
export class RewardItemComp extends CCComp {
|
||||
|
||||
@property({ type: Label, tooltip: "数量文本(count_label)" })
|
||||
count_label: Label = null!;
|
||||
|
||||
/**
|
||||
* 设置奖励数量并播放入场动画。
|
||||
* @param count 奖励数量(展示为 "+N")
|
||||
*/
|
||||
setReward(count: number) {
|
||||
if (this.count_label) {
|
||||
this.count_label.string = `+${count}`;
|
||||
}
|
||||
this.playEnter();
|
||||
}
|
||||
|
||||
/** 入场动画:从 0 缩放弹至 1 */
|
||||
private playEnter() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
this.node.setScale(0, 0, 1);
|
||||
tween(this.node)
|
||||
.to(ENTER_DURATION, { scale: v3(1, 1, 1) }, { easing: 'backOut' })
|
||||
.start();
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
}
|
||||
9
assets/script/game/map/RewardItemComp.ts.meta
Normal file
9
assets/script/game/map/RewardItemComp.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7e48d6d6-b09c-4036-b9e5-0003510186f2",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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', "释放胜利界面");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user