From 4938e8468e03761d74a0106f21c21feadd8afaee Mon Sep 17 00:00:00 2001 From: panFD Date: Tue, 4 Aug 2026 22:18:20 +0800 Subject: [PATCH] feat(hero): add hero upgrade function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增英雄升级相关UI控件与事件流程 2. 实现升级消耗计算与统一扣费逻辑 3. 添加升级预览确认弹窗与属性成长展示 4. 开放tryUpgradeHeroByEid与applyHeroLevel为公共方法 --- assets/resources/gui/element/herobox.prefab | 3 + assets/script/game/common/config/GameEvent.ts | 2 + assets/script/game/map/HeroBoxComp.ts | 106 +++++++++++++++++- assets/script/game/map/MissionCardComp.ts | 28 ++++- assets/script/game/map/MissionEconomy.ts | 18 +++ 5 files changed, 153 insertions(+), 4 deletions(-) diff --git a/assets/resources/gui/element/herobox.prefab b/assets/resources/gui/element/herobox.prefab index cfaf4813..33183b58 100644 --- a/assets/resources/gui/element/herobox.prefab +++ b/assets/resources/gui/element/herobox.prefab @@ -12964,6 +12964,9 @@ "sell_btn": { "__id__": 722 }, + "upgrade_btn": { + "__id__": 556 + }, "sell_price_label": { "__id__": 739 }, diff --git a/assets/script/game/common/config/GameEvent.ts b/assets/script/game/common/config/GameEvent.ts index 3b0b0970..7196b3ee 100644 --- a/assets/script/game/common/config/GameEvent.ts +++ b/assets/script/game/common/config/GameEvent.ts @@ -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", diff --git a/assets/script/game/map/HeroBoxComp.ts b/assets/script/game/map/HeroBoxComp.ts index 79cbb10f..7ee742e3 100644 --- a/assets/script/game/map/HeroBoxComp.ts +++ b/assets/script/game/map/HeroBoxComp.ts @@ -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: `${heroName} Lv.${heroLv} → Lv.${nextLv}\n消耗 ${cost} 金币\n\n升级后获得:\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[] = [ + `攻击 ${nextAp} 生命 ${nextHp}` + ]; + + // ---- 新能力(触发技能 / 驻场光环 / 复活 / 额外加成,取目标等级生效档) ---- + 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 = ""; } diff --git a/assets/script/game/map/MissionCardComp.ts b/assets/script/game/map/MissionCardComp.ts index d74c7123..79147556 100644 --- a/assets/script/game/map/MissionCardComp.ts +++ b/assets/script/game/map/MissionCardComp.ts @@ -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))); diff --git a/assets/script/game/map/MissionEconomy.ts b/assets/script/game/map/MissionEconomy.ts index 69f10ff2..66cdfd83 100644 --- a/assets/script/game/map/MissionEconomy.ts +++ b/assets/script/game/map/MissionEconomy.ts @@ -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)); + } + /** * 计算英雄出售金币(按英雄等级缩放) */