feat(hero): add hero upgrade function
1. 新增英雄升级相关UI控件与事件流程 2. 实现升级消耗计算与统一扣费逻辑 3. 添加升级预览确认弹窗与属性成长展示 4. 开放tryUpgradeHeroByEid与applyHeroLevel为公共方法
This commit is contained in:
@@ -12964,6 +12964,9 @@
|
||||
"sell_btn": {
|
||||
"__id__": 722
|
||||
},
|
||||
"upgrade_btn": {
|
||||
"__id__": 556
|
||||
},
|
||||
"sell_price_label": {
|
||||
"__id__": 739
|
||||
},
|
||||
|
||||
@@ -68,6 +68,8 @@ export enum GameEvent {
|
||||
MonDead = "MonDead",
|
||||
HeroDead = "HeroDead",
|
||||
HeroSell = "HeroSell",
|
||||
/** 英雄面板升级按钮点击事件(payload: { eid },由 MissionCardComp 执行升级) */
|
||||
HeroUpgrade = "HeroUpgrade",
|
||||
GOLD_UPDATE = "GOLD_UPDATE",
|
||||
DIAMOND_UPDATE = "DIAMOND_UPDATE",
|
||||
MEAT_UPDATE = "MEAT_UPDATE",
|
||||
|
||||
@@ -20,8 +20,8 @@ import { GameEvent } from "../common/config/GameEvent";
|
||||
import { HeroInfo } from "../common/config/heroSet";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { Hero } from "../hero/Hero";
|
||||
import { FacSet, getLvColor } from "../common/config/GameSet";
|
||||
import { buildSkillDescRich } from "../common/config/HeroSkillDesc";
|
||||
import { FacSet, FightSet, getLvColor } from "../common/config/GameSet";
|
||||
import { buildSkillDesc, buildSkillDescRich, ISkillDescSource } from "../common/config/HeroSkillDesc";
|
||||
import { MissionEconomy } from "./MissionEconomy";
|
||||
import { UIID } from "../common/config/GameUIConfig";
|
||||
|
||||
@@ -60,6 +60,10 @@ export class HeroBoxComp extends CCComp {
|
||||
@property(Node)
|
||||
private sell_btn: Node = null;
|
||||
|
||||
/** 升级按钮(绑定英雄时激活,点击弹出升级预览确认框) */
|
||||
@property(Node)
|
||||
private upgrade_btn: Node = null;
|
||||
|
||||
/** 出售价格标签(按英雄等级显示当前卖价,含驻场加成) */
|
||||
@property(Label)
|
||||
private sell_price_label: Label = null;
|
||||
@@ -167,6 +171,8 @@ export class HeroBoxComp extends CCComp {
|
||||
this.noHero?.on(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
|
||||
// 出售按钮点击 → 出售当前绑定英雄
|
||||
this.sell_btn?.on(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
||||
// 升级按钮点击 → 弹出升级预览确认框
|
||||
this.upgrade_btn?.on(NodeEventType.TOUCH_END, this.onUpgradeHeroClick, this);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
@@ -181,6 +187,96 @@ export class HeroBoxComp extends CCComp {
|
||||
if (this.sell_btn && this.sell_btn.isValid) {
|
||||
this.sell_btn.off(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
||||
}
|
||||
if (this.upgrade_btn && this.upgrade_btn.isValid) {
|
||||
this.upgrade_btn.off(NodeEventType.TOUCH_END, this.onUpgradeHeroClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级按钮点击:构建升级后新能力预览,弹出公共确认框。
|
||||
* Why: 升级消耗金币不可逆,需展示收益并二次确认。
|
||||
*/
|
||||
private onUpgradeHeroClick() {
|
||||
if (!this.eid || !this.model) return;
|
||||
const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
|
||||
// 已满级:气泡提示,复用英雄栏 smalltip 挂载点
|
||||
if (heroLv >= FightSet.HERO_MAX_LV) {
|
||||
oops.message.dispatchEvent(GameEvent.ShowSmallTip, { type: "hero_full", text: "已达最高等级" });
|
||||
return;
|
||||
}
|
||||
oops.audio.playEffect("music/button");
|
||||
|
||||
const heroName = this.model.hero_name ?? "";
|
||||
const nextLv = heroLv + 1;
|
||||
const cost = MissionEconomy.getUpgradeCost(heroLv);
|
||||
const preview = this.buildUpgradePreview(nextLv);
|
||||
|
||||
// oops.gui 公共确认窗口(自定义 CommonPrompt:内容支持富文本高亮)
|
||||
oops.gui.open(UIID.Window, {
|
||||
title: `升级英雄`,
|
||||
content: `<color=#C0392B>${heroName}</color> Lv.${heroLv} → <color=#1E8449>Lv.${nextLv}</color>\n消耗 <color=#E67E22>${cost}</color> 金币\n\n<b>升级后获得:</b>\n${preview}`,
|
||||
okWord: "升级",
|
||||
cancelWord: "取消",
|
||||
needCancel: true,
|
||||
okFunc: () => this.executeUpgradeHero()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行升级:校验状态后派发 HeroUpgrade 事件,由 MissionCardComp 扣费并应用等级。
|
||||
* Why: 扣费/属性重算统一走 MissionCardComp(与升级卡共用 tryUpgradeHeroByEid),
|
||||
* 本组件仅负责 UI 交互与预览,保证单一数据源。
|
||||
*/
|
||||
private executeUpgradeHero() {
|
||||
// 确认窗口打开期间英雄可能已死亡/被移除,需二次校验
|
||||
if (!this.eid || !this.model) return;
|
||||
oops.message.dispatchEvent(GameEvent.HeroUpgrade, { eid: this.eid });
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建升级后新能力预览富文本。
|
||||
* 数据源取 HeroInfo 静态配置(lv 置为目标等级),由描述标准层统一渲染,
|
||||
* 与面板技能描述同一套文案规则,杜绝数值漂移。
|
||||
*
|
||||
* @param nextLv 目标等级
|
||||
* @returns 多行富文本:属性成长 + 下一档触发技能/光环/复活/加成
|
||||
*/
|
||||
private buildUpgradePreview(nextLv: number): string {
|
||||
const hero = HeroInfo[this.model?.hero_uuid ?? 0];
|
||||
if (!hero) return "";
|
||||
|
||||
// ---- 属性成长(按 HERO_LV_MULTIPLIER 幂成长 + 等级额外加成) ----
|
||||
const mult = Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
|
||||
const nextAp = Math.floor(hero.ap * mult + HeroBoxComp.sumBonus(hero.ap_bonus, nextLv));
|
||||
const nextHp = Math.floor(hero.hp * mult + HeroBoxComp.sumBonus(hero.hp_bonus, nextLv));
|
||||
const lines: string[] = [
|
||||
`攻击 <color=#27AE60>${nextAp}</color> 生命 <color=#27AE60>${nextHp}</color>`
|
||||
];
|
||||
|
||||
// ---- 新能力(触发技能 / 驻场光环 / 复活 / 额外加成,取目标等级生效档) ----
|
||||
const source: ISkillDescSource = {
|
||||
lv: nextLv,
|
||||
call: hero.call,
|
||||
dead: hero.dead,
|
||||
fstart: hero.fstart,
|
||||
fend: hero.fend,
|
||||
atking: hero.atking,
|
||||
atked: hero.atked,
|
||||
field: hero.field,
|
||||
revive: hero.revive,
|
||||
// 预览仅展示增量,额外加成由上方属性成长体现,避免重复
|
||||
};
|
||||
const skillDesc = buildSkillDesc(source);
|
||||
if (skillDesc) lines.push(skillDesc);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** 累加等级额外加成(≤目标等级的所有档位) */
|
||||
private static sumBonus(entries: { lv: number; value: number }[] | undefined, lv: number): number {
|
||||
if (!entries) return 0;
|
||||
let total = 0;
|
||||
for (const e of entries) { if (e.lv <= lv) total += e.value; }
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,6 +416,9 @@ export class HeroBoxComp extends CCComp {
|
||||
if (this.sell_btn && this.sell_btn.isValid) {
|
||||
this.sell_btn.active = true;
|
||||
}
|
||||
if (this.upgrade_btn && this.upgrade_btn.isValid) {
|
||||
this.upgrade_btn.active = true;
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
@@ -337,6 +436,9 @@ export class HeroBoxComp extends CCComp {
|
||||
if (this.sell_btn && this.sell_btn.isValid) {
|
||||
this.sell_btn.active = false;
|
||||
}
|
||||
if (this.upgrade_btn && this.upgrade_btn.isValid) {
|
||||
this.upgrade_btn.active = false;
|
||||
}
|
||||
if (this.sell_price_label && this.sell_price_label.isValid) {
|
||||
this.sell_price_label.string = "";
|
||||
}
|
||||
|
||||
@@ -371,6 +371,7 @@ export class MissionCardComp extends CCComp {
|
||||
oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
|
||||
oops.message.on(GameEvent.CardUsed, this.onCardUsed, this);
|
||||
oops.message.on(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
|
||||
oops.message.on(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
|
||||
|
||||
/** 按钮触控事件:抽卡 */
|
||||
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
||||
@@ -614,6 +615,7 @@ export class MissionCardComp extends CCComp {
|
||||
oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
|
||||
oops.message.off(GameEvent.CardUsed, this.onCardUsed, this);
|
||||
oops.message.off(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
|
||||
oops.message.off(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
|
||||
if (this.cards_chou && this.cards_chou.isValid) {
|
||||
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
||||
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
|
||||
@@ -694,6 +696,28 @@ export class MissionCardComp extends CCComp {
|
||||
this.refreshHeroBoxSlots();
|
||||
}
|
||||
|
||||
/**
|
||||
* 英雄面板升级请求回调(由 HeroBoxComp 确认弹窗后派发)。
|
||||
* 扣费成功后按 eid 精确升级;金币不足或不可升级时 toast 提示。
|
||||
*/
|
||||
private onHeroUpgrade(event: string, args: any) {
|
||||
const payload = args ?? event;
|
||||
const eid: number = payload?.eid ?? 0;
|
||||
if (!eid) return;
|
||||
const actor = this.queryAliveHeroActors().find(item => item.eid === eid);
|
||||
if (!actor) return;
|
||||
if (actor.model.lv >= FightSet.HERO_MAX_LV) {
|
||||
oops.gui.toast(`已达最高等级`);
|
||||
return;
|
||||
}
|
||||
// 统一经济管理入口:按当前等级扣费
|
||||
if (!MissionEconomy.executeUpgradeCost(actor.model.lv)) {
|
||||
oops.gui.toast(`金币不足`);
|
||||
return;
|
||||
}
|
||||
this.tryUpgradeHeroByEid(eid);
|
||||
}
|
||||
|
||||
/** 英雄升级事件回调:刷新英雄信息盒显示 */
|
||||
private onHeroLvUp() {
|
||||
this.refreshHeroBoxSlots();
|
||||
@@ -1818,7 +1842,7 @@ export class MissionCardComp extends CCComp {
|
||||
* @param heroEid 要升级的英雄实体 eid
|
||||
* @returns true = 升级成功
|
||||
*/
|
||||
private tryUpgradeHeroByEid(heroEid: number): boolean {
|
||||
public tryUpgradeHeroByEid(heroEid: number): boolean {
|
||||
if (!heroEid) return false;
|
||||
const actor = this.queryAliveHeroActors().find(item => item.eid === heroEid);
|
||||
if (!actor) return false;
|
||||
@@ -1835,7 +1859,7 @@ export class MissionCardComp extends CCComp {
|
||||
* 应用英雄等级变化(属性按 HERO_LV_MULTIPLIER 重新计算)。
|
||||
* 沿用旧版本 SkillLvUp 的技能等级映射规则。
|
||||
*/
|
||||
private applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
|
||||
public applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
|
||||
const hero = HeroInfo[model.hero_uuid];
|
||||
if (!hero) return;
|
||||
const nextLv = Math.max(1, Math.min(FightSet.HERO_MAX_LV, Math.floor(targetLv)));
|
||||
|
||||
@@ -109,6 +109,24 @@ export class MissionEconomy {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算英雄升级金币消耗(按当前等级线性缩放:Lv×5)
|
||||
* @param heroLevel 当前英雄等级(升级前)
|
||||
*/
|
||||
static getUpgradeCost(heroLevel: number = 1): number {
|
||||
const lv = Math.max(1, Math.floor(heroLevel));
|
||||
return lv * 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行英雄升级扣费
|
||||
* @param heroLevel 当前英雄等级(升级前)
|
||||
* @returns true = 扣费成功(金币足够)
|
||||
*/
|
||||
static executeUpgradeCost(heroLevel: number = 1): boolean {
|
||||
return this.spendCoin(this.getUpgradeCost(heroLevel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算英雄出售金币(按英雄等级缩放)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user