Compare commits
7 Commits
2ae48afb99
...
53fa0d7dc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53fa0d7dc0 | ||
|
|
11ae66fe1d | ||
|
|
ad38e9904e | ||
|
|
4532cb5c61 | ||
|
|
096cae527b | ||
|
|
74860c6bf9 | ||
|
|
175b93c7fa |
@@ -15,7 +15,8 @@
|
||||
"WebSearch",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(npm install *)"
|
||||
"Bash(npm install *)",
|
||||
"Bash(npx tsc *)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf *)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -600,7 +600,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": -5.704,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -999,7 +999,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 52.112,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -1398,7 +1398,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 21.504,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -1875,7 +1875,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 21.504,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -2480,7 +2480,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 183.281,
|
||||
"y": 263.281,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -2596,7 +2596,7 @@
|
||||
"_left": 0,
|
||||
"_right": 0,
|
||||
"_top": 0,
|
||||
"_bottom": 233.281,
|
||||
"_bottom": 313.281,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
@@ -2652,7 +2652,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": -230,
|
||||
"y": -150,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -11910,7 +11910,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 183.281,
|
||||
"y": 263.281,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -12026,7 +12026,7 @@
|
||||
"_left": 0,
|
||||
"_right": 0,
|
||||
"_top": 0,
|
||||
"_bottom": 233.281,
|
||||
"_bottom": 313.281,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
@@ -12082,7 +12082,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": -230,
|
||||
"y": -150,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
"value": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 230,
|
||||
"height": 300
|
||||
"height": 320
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -239,7 +239,7 @@
|
||||
"value": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 230,
|
||||
"height": 300
|
||||
"height": 320
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -259,7 +259,7 @@
|
||||
"value": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 460,
|
||||
"height": 600
|
||||
"height": 640
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -279,7 +279,7 @@
|
||||
"value": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 428,
|
||||
"height": 568
|
||||
"height": 608
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -344,7 +344,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 122.888,
|
||||
"y": 132.888,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -1059,7 +1059,7 @@
|
||||
"value": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 58.485,
|
||||
"y": 68.485,
|
||||
"z": 0
|
||||
}
|
||||
},
|
||||
@@ -2727,7 +2727,7 @@
|
||||
"value": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": -112,
|
||||
"y": -122,
|
||||
"z": 0
|
||||
}
|
||||
},
|
||||
@@ -3035,7 +3035,7 @@
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 190,
|
||||
"height": 26.46
|
||||
"height": 23.94
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -3060,11 +3060,11 @@
|
||||
"__prefab": {
|
||||
"__id__": 195
|
||||
},
|
||||
"_lineHeight": 21,
|
||||
"_lineHeight": 19,
|
||||
"_string": "<color=#00ff00>Rich</color><color=#0fffff>Text</color>",
|
||||
"_horizontalAlign": 0,
|
||||
"_verticalAlign": 0,
|
||||
"_fontSize": 20,
|
||||
"_fontSize": 18,
|
||||
"_fontColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 32,
|
||||
@@ -3114,7 +3114,7 @@
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 230,
|
||||
"height": 300
|
||||
"height": 320
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -232,7 +232,7 @@
|
||||
"_top": 0,
|
||||
"_bottom": 546.004,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 63,
|
||||
"_verticalCenter": 143,
|
||||
"_isAbsLeft": true,
|
||||
"_isAbsRight": true,
|
||||
"_isAbsTop": true,
|
||||
@@ -276,7 +276,7 @@
|
||||
"value": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 703,
|
||||
"y": 783,
|
||||
"z": 0
|
||||
}
|
||||
},
|
||||
|
||||
226
assets/script/game/common/CommonPrompt.ts
Normal file
226
assets/script/game/common/CommonPrompt.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* @file CommonPrompt.ts
|
||||
* @description 自定义公共确认窗口组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 替换框架插件 CommonPrompt(extensions/oops-plugin-framework/.../CommonPrompt.ts),
|
||||
* 脚本类名、onTouchEnd 处理器与 4 个属性名保持同名,window.prefab 无需改动即可自动重链。
|
||||
* 2. 内容区由 cc.Label 升级为 cc.RichText,支持 <color>/<size>/<b> 等富文本标签。
|
||||
* 3. 扩展标题样式、窗体缩放动画、遮罩点击关闭等自定义能力。
|
||||
*
|
||||
* 使用(oops.gui 模块):
|
||||
* oops.gui.open(UIID.Window, {
|
||||
* title: "标题",
|
||||
* content: "支持 <color=#ff0000>富文本</color>",
|
||||
* okWord: "确定", cancelWord: "取消", needCancel: true,
|
||||
* okFunc: () => {}, cancelFunc: () => {},
|
||||
* titleColor: "#FF0000", // 可选:标题颜色
|
||||
* maskClose: true, // 可选:点击空白遮罩关闭(等同取消)
|
||||
* closeFunc: () => {} // 可选:任意方式关闭后的统一回调
|
||||
* });
|
||||
*/
|
||||
import { _decorator, Button, Color, Component, EventTouch, Label, Node, NodeEventType, RichText, Tween, tween, Vec3 } from "cc";
|
||||
import { oops } from "db://oops-framework/core/Oops";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 确认窗口参数 */
|
||||
export interface WindowPromptParams {
|
||||
/** 标题(普通文本) */
|
||||
title?: string;
|
||||
/** 内容(RichText 富文本,支持 <color>/<size>/<b>/<i>/<outline> 等标签) */
|
||||
content?: string;
|
||||
/** 确定按钮文字 */
|
||||
okWord?: string;
|
||||
/** 确定回调 */
|
||||
okFunc?: () => void;
|
||||
/** 取消按钮文字 */
|
||||
cancelWord?: string;
|
||||
/** 取消回调 */
|
||||
cancelFunc?: () => void;
|
||||
/** 是否显示取消按钮 */
|
||||
needCancel?: boolean;
|
||||
/** 标题颜色(如 "#FF0000",不传用默认色) */
|
||||
titleColor?: string;
|
||||
/** 点击窗体外空白遮罩是否关闭(等同取消,默认 false) */
|
||||
maskClose?: boolean;
|
||||
/** 任意方式关闭后的统一回调(在 okFunc/cancelFunc 之后触发) */
|
||||
closeFunc?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* CommonPrompt —— 自定义公共确认窗口
|
||||
*
|
||||
* 与 window.prefab 的绑定约定:
|
||||
* - 根节点按钮事件 handler:onTouchEnd(btn_ok / btn_cancel 已绑定)
|
||||
* - 属性名:lab_title / lab_content / lab_ok / lab_cancel
|
||||
* - lab_content 需将 prefab 中的 cc.Label 替换为 cc.RichText 后拖入
|
||||
*/
|
||||
@ccclass("CommonPrompt")
|
||||
export class CommonPrompt extends Component {
|
||||
/** 窗口标题 */
|
||||
@property(Label)
|
||||
private lab_title: Label | null = null;
|
||||
|
||||
/** 窗口内容(富文本,需在 prefab 中将 Label 换为 RichText 后重新拖入) */
|
||||
@property(RichText)
|
||||
private lab_content: RichText | null = null;
|
||||
|
||||
/** 确定按钮文本 */
|
||||
@property(Label)
|
||||
private lab_ok: Label | null = null;
|
||||
|
||||
/** 取消按钮文本 */
|
||||
@property(Label)
|
||||
private lab_cancel: Label | null = null;
|
||||
|
||||
/** 运行时参数 */
|
||||
private config: WindowPromptParams = {};
|
||||
|
||||
/** 弹窗打开动画已播放标记(防止 onAdded 重复触发) */
|
||||
private animPlayed: boolean = false;
|
||||
|
||||
/** 代码动态绑定的确定按钮(prefab clickEvents 失效时的兜底) */
|
||||
private btnOk: Node | null = null;
|
||||
/** 代码动态绑定的取消按钮 */
|
||||
private btnCancel: Node | null = null;
|
||||
|
||||
onLoad() {
|
||||
// 兜底:prefab 中 clickEvents 引用的旧插件脚本被移除后,代码动态绑定保证按钮可用
|
||||
this.btnOk = this.node.getChildByName("Node")?.getChildByName("btn_ok") ?? null;
|
||||
this.btnCancel = this.node.getChildByName("Node")?.getChildByName("btn_cancel") ?? null;
|
||||
this.btnOk?.on(Button.EventType.CLICK, this.onOk, this);
|
||||
this.btnCancel?.on(Button.EventType.CLICK, this.onCancel, this);
|
||||
// 遮罩点击关闭(btn_sky 为 btn_ok 内嵌子节点,无 Button 组件,直接监听触摸事件)
|
||||
this.btnOk?.getChildByName("btn_sky")?.on(NodeEventType.TOUCH_END, this.onMaskClick, this);
|
||||
|
||||
// 序列化属性槽引用的是旧插件 LanguageLabel 组件实例,脚本替换后已失效;
|
||||
// 这里按节点名查找 Label/RichText 作为兜底,保证文字一定写入
|
||||
if (!this.lab_title) {
|
||||
this.lab_title = this.node.getChildByName("lab_title")?.getComponent(Label) ?? null;
|
||||
}
|
||||
if (!this.lab_ok) {
|
||||
this.lab_ok = this.btnOk?.getChildByName("lab_ok")?.getComponent(Label) ?? null;
|
||||
}
|
||||
if (!this.lab_cancel) {
|
||||
this.lab_cancel = this.btnCancel?.getChildByName("lab_cancel")?.getComponent(Label) ?? null;
|
||||
}
|
||||
if (!this.lab_content) {
|
||||
this.lab_content = this.node.getChildByName("lab_content")?.getComponent(RichText) ?? null;
|
||||
}
|
||||
|
||||
// 禁用旧插件残留的 LanguageLabel 组件:其 update 会用空 dataID 每帧覆盖 Label 文本
|
||||
this.disableLegacyLanguageLabels(this.lab_title?.node);
|
||||
this.disableLegacyLanguageLabels(this.lab_ok?.node);
|
||||
this.disableLegacyLanguageLabels(this.lab_cancel?.node);
|
||||
this.disableLegacyLanguageLabels(this.lab_content?.node);
|
||||
}
|
||||
|
||||
/** 禁用节点上残留的 LanguageLabel(旧插件组件,类名带 LanguageLabel 后缀) */
|
||||
private disableLegacyLanguageLabels(node: Node | null | undefined) {
|
||||
if (!node || !node.isValid) return;
|
||||
for (const comp of node.components) {
|
||||
if (comp.constructor.name === "LanguageLabel") {
|
||||
comp.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.btnOk?.off(Button.EventType.CLICK, this.onOk, this);
|
||||
this.btnCancel?.off(Button.EventType.CLICK, this.onCancel, this);
|
||||
Tween.stopAllByTarget(this.node);
|
||||
this.config = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* oops.gui 添加到舞台回调(框架约定入口)
|
||||
* @param params 见 WindowPromptParams
|
||||
*/
|
||||
onAdded(params: WindowPromptParams = {}) {
|
||||
this.config = params || {};
|
||||
this.setTitle();
|
||||
this.setContent();
|
||||
this.setBtnOkLabel();
|
||||
this.setBtnCancelLabel();
|
||||
this.node.active = true;
|
||||
this.playOpenAnim();
|
||||
}
|
||||
|
||||
/** 按钮点击处理器(window.prefab 中 btn_ok / btn_cancel 的 clickEvents 绑定此方法) */
|
||||
private onTouchEnd(event: EventTouch) {
|
||||
switch (event.target.name) {
|
||||
case "btn_ok":
|
||||
this.onOk();
|
||||
break;
|
||||
case "btn_cancel":
|
||||
this.onCancel();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** 遮罩(btn_sky)点击:仅当 maskClose 开启时按取消处理 */
|
||||
private onMaskClick() {
|
||||
if (this.config.maskClose) {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
private setTitle() {
|
||||
if (!this.lab_title || !this.lab_title.isValid) return;
|
||||
this.lab_title.string = this.config.title ?? "";
|
||||
if (this.config.titleColor) {
|
||||
this.lab_title.color = new Color().fromHEX(this.config.titleColor);
|
||||
}
|
||||
}
|
||||
|
||||
private setContent() {
|
||||
if (!this.lab_content || !this.lab_content.isValid) return;
|
||||
this.lab_content.string = this.config.content ?? "";
|
||||
}
|
||||
|
||||
private setBtnOkLabel() {
|
||||
if (!this.lab_ok || !this.lab_ok.isValid) return;
|
||||
this.lab_ok.string = this.config.okWord ?? "确定";
|
||||
}
|
||||
|
||||
private setBtnCancelLabel() {
|
||||
if (!this.lab_cancel || !this.lab_cancel.isValid) return;
|
||||
this.lab_cancel.string = this.config.cancelWord ?? "取消";
|
||||
// 取消按钮挂在 lab_cancel 的父节点上
|
||||
const cancelBtn = this.lab_cancel.node?.parent;
|
||||
if (cancelBtn && cancelBtn.isValid) {
|
||||
cancelBtn.active = this.config.needCancel || false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开动画:缩放 0.8 → 1 回弹 */
|
||||
private playOpenAnim() {
|
||||
if (this.animPlayed) return;
|
||||
this.animPlayed = true;
|
||||
Tween.stopAllByTarget(this.node);
|
||||
this.node.setScale(0.8, 0.8, 1);
|
||||
tween(this.node)
|
||||
.to(0.15, { scale: new Vec3(1, 1, 1) }, { easing: "backOut" })
|
||||
.start();
|
||||
}
|
||||
|
||||
private onOk() {
|
||||
oops.audio.playEffect("music/button");
|
||||
this.config.okFunc?.();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private onCancel() {
|
||||
oops.audio.playEffect("music/button");
|
||||
this.config.cancelFunc?.();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private close() {
|
||||
this.config.closeFunc?.();
|
||||
oops.gui.removeByNode(this.node);
|
||||
}
|
||||
}
|
||||
9
assets/script/game/common/CommonPrompt.ts.meta
Normal file
9
assets/script/game/common/CommonPrompt.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8c571795-0ff9-4765-ad7e-1de7f1cdcb3c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export enum BoxSet {
|
||||
LETF_END = -360,
|
||||
RIGHT_END = 360,
|
||||
//游戏地平线
|
||||
GAME_LINE = 80,
|
||||
GAME_LINE = 160,
|
||||
/** 技能/装备各自独立的上限 */
|
||||
BOX_MAX = 6,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ export enum UIID {
|
||||
Loading = 1,
|
||||
/** 加载与延时提示界面 */
|
||||
Netinstable,
|
||||
/** 公共确认窗口(标题 + 内容 + 确定/取消) */
|
||||
Window,
|
||||
/** 角色控制 */
|
||||
Role_Controller,
|
||||
Victory,
|
||||
@@ -34,6 +36,7 @@ export enum UIID {
|
||||
export var UIConfigData: { [key: number]: UIConfig } = {
|
||||
[UIID.Loading]: { layer: LayerType.UI, prefab: "loading/prefab/loading", bundle: "resources" },
|
||||
[UIID.Netinstable]: { layer: LayerType.PopUp, prefab: "common/prefab/netinstable" },
|
||||
[UIID.Window]: { layer: LayerType.Dialog, prefab: "common/prefab/window" },
|
||||
[UIID.Role_Controller]: { layer: LayerType.UI, prefab: "gui/role_controller" },
|
||||
[UIID.Victory]: { layer: LayerType.UI, prefab: "gui/element/victory" },
|
||||
[UIID.IBox]: { layer: LayerType.PopUp, prefab: "gui/element/ibox" },
|
||||
|
||||
@@ -229,7 +229,7 @@ function buildFieldLine(head: string, uuid: number, showName: boolean = true): I
|
||||
* 复活(revive)结构化行:复刻战斗公式,高亮复活次数与回血百分比
|
||||
* 复活次数 = r_num + floor((lv - 1) * upr);回血百分比 = SkillSet[s_uuid].ap
|
||||
*/
|
||||
function buildReviveLine(source: ISkillDescSource): ISkillLine | null {
|
||||
function buildReviveLine(source: ISkillDescSource, showName: boolean = true): ISkillLine | null {
|
||||
const revive = source.revive;
|
||||
if (!revive) return null;
|
||||
const base = SkillSet[revive.s_uuid];
|
||||
@@ -238,9 +238,10 @@ function buildReviveLine(source: ISkillDescSource): ISkillLine | null {
|
||||
const maxCount = revive.r_num + Math.floor((lv - 1) * revive.upr);
|
||||
const hpPct = base.ap ?? 0;
|
||||
const tpl = SkillTriggerName[SkillTriggerType.Revive] ?? "复活";
|
||||
const nameSeg = showName ? `「${base.name}」 ` : "";
|
||||
// 高亮片段选回血百分比(数值中最关键的收益指标)
|
||||
return {
|
||||
prefix: `${tpl}:「${base.name}」 可复活${maxCount}次,每次恢复`,
|
||||
prefix: `${tpl}:${nameSeg}可复活${maxCount}次,每次恢复`,
|
||||
value: `${hpPct}%`,
|
||||
suffix: "生命",
|
||||
};
|
||||
@@ -268,8 +269,9 @@ function buildEvolveBonusLines(source: ISkillDescSource): ISkillLine[] {
|
||||
/**
|
||||
* 构建全部能力结构化行(触发技能 + 驻场光环 + 复活 + evolve 加成)。
|
||||
* 纯文本与富文本两个公开接口共用此结果,仅渲染阶段不同。
|
||||
* @param showName 是否在行中包含「技能名」片段(英雄盒面板传 false 隐藏技能名)
|
||||
*/
|
||||
function buildSkillLines(source: ISkillDescSource): ISkillLine[] {
|
||||
function buildSkillLines(source: ISkillDescSource, showName: boolean = true): ISkillLine[] {
|
||||
const lines: ISkillLine[] = [];
|
||||
|
||||
// ---- 6 种标准触发技能 ----
|
||||
@@ -285,7 +287,8 @@ function buildSkillLines(source: ISkillDescSource): ISkillLine[] {
|
||||
if (!base) continue;
|
||||
const skill = mergeSkillParams(base, item.overrides);
|
||||
const trigger = needCount ? `${name}×${item.t_num}` : name;
|
||||
lines.push(buildEffectLine(`${trigger}:「${base.name}」 `, skill));
|
||||
const nameSeg = showName ? `「${base.name}」 ` : "";
|
||||
lines.push(buildEffectLine(`${trigger}:${nameSeg}`, skill));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,13 +297,13 @@ function buildSkillLines(source: ISkillDescSource): ISkillLine[] {
|
||||
if (fieldUuids?.length) {
|
||||
const tpl = SkillTriggerName[SkillTriggerType.Field] ?? "光环";
|
||||
for (const uuid of fieldUuids) {
|
||||
const line = buildFieldLine(`${tpl}:`, uuid, true);
|
||||
const line = buildFieldLine(`${tpl}:`, uuid, showName);
|
||||
if (line) lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 复活(revive)----
|
||||
const reviveLine = buildReviveLine(source);
|
||||
const reviveLine = buildReviveLine(source, showName);
|
||||
if (reviveLine) lines.push(reviveLine);
|
||||
|
||||
// ---- evolve 额外属性加成(ap/hp)----
|
||||
@@ -323,10 +326,11 @@ export function buildSkillDesc(source: ISkillDescSource): string {
|
||||
/**
|
||||
* 富文本描述(RichText 用,BBCode):技能名用 <color=#2E86C1> 蓝色、数值用 <color=#27AE60> 亮绿色高亮。
|
||||
* @param source 触发技能数据源(同 buildSkillDesc)
|
||||
* @param showName 是否包含「技能名」片段(英雄盒面板传 false 隐藏技能名,默认 true)
|
||||
* @returns 多行富文本串,技能名片段包裹 <color=#2E86C1>、数值片段包裹 <color=#27AE60>,用 \n 分隔
|
||||
*/
|
||||
export function buildSkillDescRich(source: ISkillDescSource): string {
|
||||
return buildSkillLines(source)
|
||||
export function buildSkillDescRich(source: ISkillDescSource, showName: boolean = true): string {
|
||||
return buildSkillLines(source, showName)
|
||||
.map(l => {
|
||||
const prefix = l.prefix.replace(SKILL_NAME_PATTERN, `「<color=${RICH_NAME_COLOR}>$1</color>」`);
|
||||
const text = l.value
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { v3 } from "cc"
|
||||
import { BoxSet, FacSet } from "./GameSet"
|
||||
import { SkillOverrides, TGroup } from "./SkillSet"
|
||||
import { BoxSet, FacSet, FightSet } from "./GameSet"
|
||||
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams } from "./SkillSet"
|
||||
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
|
||||
import { Attrs } from "./HeroAttrs"
|
||||
|
||||
export enum HType {
|
||||
Melee = 0,
|
||||
@@ -139,7 +141,7 @@ export const SkillTriggerDesc = {
|
||||
[SkillTriggerType.FEnd]: "战斗结束时",
|
||||
[SkillTriggerType.Field]: "光环",
|
||||
[SkillTriggerType.Atking]: "攻击n次",
|
||||
[SkillTriggerType.Atked]: "受击n次",
|
||||
[SkillTriggerType.Atked]: "受伤n次",
|
||||
[SkillTriggerType.Revive]: "复活时",
|
||||
}
|
||||
|
||||
@@ -241,15 +243,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
]
|
||||
},
|
||||
},
|
||||
info: "每受击3次为自身添加护盾(层数随等级4→5→6);6级起每受击5次回复攻击力250%生命",
|
||||
infos: [
|
||||
"受击3次获得4次格挡护盾",
|
||||
"受击3次获得5次格挡护盾",
|
||||
"额外+150最大生命",
|
||||
"受击3次获得6次格挡护盾",
|
||||
"额外+150最大生命,+10攻击力",
|
||||
"受击3次获得6次格挡护盾;受击5次回复攻击力250%生命",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>受伤获得护盾\n<color=#9B59B6>✦</color>受伤恢复生命",
|
||||
},
|
||||
5012: {
|
||||
uuid: 5012, name: "不死小强", path: "hk2", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, hp: 600, ap: 57,
|
||||
@@ -267,43 +261,35 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
]
|
||||
},
|
||||
},
|
||||
info: "每受击3次回复自身攻击力250%生命;2级治疗量300%,4级350%,5级触发阈值降至2次;6级起每受击5次为自身添加4层护盾",
|
||||
infos: [
|
||||
"受击3次回复攻击力250%生命",
|
||||
"受击3次回复攻击力300%生命",
|
||||
"额外+150最大生命",
|
||||
"受击3次回复攻击力350%生命",
|
||||
"受击2次回复攻击力350%生命",
|
||||
"受击2次回复攻击力350%生命;受击5次获得4次格挡护盾",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>受伤恢复生命\n<color=#9B59B6>✦</color>受伤获得护盾",
|
||||
},
|
||||
// 以下英雄暂时注释,保留待后续扩展
|
||||
// 5012:{uuid:5012,name:"不死小强",path:"hk2", fac:FacSet.HERO,card_lv:2,lv:1,type:HType.Melee,hp:600,ap:57,
|
||||
// skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
|
||||
// atked:[{s_uuid:6302,t_num:3,overrides:{TGroup:TGroup.Self,ap:250}}],
|
||||
// info:"每受击3次为自身回复攻击力250%的生命值"},
|
||||
// info:"s3次为自身回复攻击力250%的生命值"},
|
||||
// 5013:{uuid:5013,name:"铁骨头",path:"hk3", fac:FacSet.HERO,card_lv:2,lv:1,type:HType.Melee,hp:600,ap:57,
|
||||
// skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
|
||||
// atked:[{s_uuid:6402,t_num:5,overrides:{TGroup:TGroup.Self,ap:100}}],
|
||||
// info:"每受击5次永久提升自身最大生命值100点"},
|
||||
// info:"s5次永久提升自身最大生命值100点"},
|
||||
// 5014:{uuid:5014,name:"怒火武者",path:"hk4", fac:FacSet.HERO,card_lv:3,lv:1,type:HType.Melee,hp:900,ap:85,
|
||||
// skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow1].cd,ccd:0}},
|
||||
// atked:[{s_uuid:6401,t_num:3,overrides:{TGroup:TGroup.Self,ap:12}}],
|
||||
// info:"每受击3次永久提升自身攻击力12点"},
|
||||
// info:"s3次永久提升自身攻击力12点"},
|
||||
// 5015:{uuid:5015,name:"血刃武者",path:"hk5", fac:FacSet.HERO,card_lv:4,lv:1,type:HType.Melee,hp:1200,ap:113,
|
||||
// skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
|
||||
// atked:[
|
||||
// {s_uuid:6301,t_num:3,overrides:{TGroup:TGroup.Self,ap:3}},
|
||||
// {s_uuid:6401,t_num:5,overrides:{TGroup:TGroup.Self,ap:15}}
|
||||
// ],
|
||||
// info:"每受击3次加3层护盾,每受击5次永久+15攻击力"},
|
||||
// info:"s3次加3层护盾,s5次永久+15攻击力"},
|
||||
// 5016:{uuid:5016,name:"狂血战士",path:"hc1", fac:FacSet.HERO,card_lv:5,lv:1,type:HType.Melee,hp:1500,ap:142,
|
||||
// skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
|
||||
// atked:[
|
||||
// {s_uuid:6401,t_num:3,overrides:{TGroup:TGroup.Self,ap:10}},
|
||||
// {s_uuid:6403,t_num:5,overrides:{TGroup:TGroup.Self,ap:15}}
|
||||
// ],
|
||||
// info:"每受击3次永久+10攻击力,每受击5次永久+15%暴击率"},
|
||||
// info:"s3次永久+10攻击力,s5次永久+15%暴击率"},
|
||||
|
||||
// ========== atking 类 — 刺客(自身强化) ==========
|
||||
5021: {
|
||||
@@ -324,15 +310,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
]
|
||||
},
|
||||
},
|
||||
info: "每攻击5次永久提升自身攻击力(8→10→12,5级起每4次);6级起每攻击7次永久+20%暴击伤害",
|
||||
infos: [
|
||||
"攻击5次永久+8攻击力",
|
||||
"攻击5次永久+10攻击力",
|
||||
"额外+80最大生命,+20攻击力",
|
||||
"攻击5次永久+12攻击力",
|
||||
"攻击4次永久+12攻击力",
|
||||
"攻击4次永久+12攻击力;攻击7次永久+20%暴击伤害",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>攻击永久提升攻击\n<color=#9B59B6>✦</color>攻击永久提升暴伤",
|
||||
},
|
||||
5022: {
|
||||
uuid: 5022, name: "嗜血剑客", path: "hc2", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Melee, hp: 900, ap: 85,
|
||||
@@ -369,15 +347,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
]
|
||||
},
|
||||
},
|
||||
info: "每攻击5次自身获得+15%暴击率(持续5秒),每攻击7次永久+12攻击力;2级暴击+20%,4级持续6秒且攻击+15,5级暴击触发阈值降至4次;6级起每攻击7次永久+15%暴伤",
|
||||
infos: [
|
||||
"攻击5次获得+15%暴击率5秒;攻击7次永久+12攻击力",
|
||||
"攻击5次获得+20%暴击率5秒;攻击7次永久+12攻击力",
|
||||
"额外+80最大生命,+20攻击力",
|
||||
"攻击5次获得+20%暴击率6秒;攻击7次永久+15攻击力",
|
||||
"攻击4次获得+20%暴击率6秒;攻击7次永久+15攻击力",
|
||||
"攻击4次获得+20%暴击率6秒;攻击7次永久+15攻击力;攻击7次永久+15%暴击伤害",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>攻击提升暴击攻击\n<color=#9B59B6>✦</color>攻击永久提升暴伤",
|
||||
},
|
||||
// 5022: {
|
||||
// uuid: 5022, name: "嗜血剑客", path: "hc2", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Melee, hp: 900, ap: 85,
|
||||
@@ -386,7 +356,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
// { s_uuid: 6403, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 10 } },
|
||||
// { s_uuid: 6401, t_num: 7, overrides: { TGroup: TGroup.Self, ap: 12 } }
|
||||
// ],
|
||||
// info: "每攻击5次永久+10%暴击率,每攻击7次永久+12攻击力"
|
||||
// info: "攻击5次永久+10%暴击率,攻击7次永久+12攻击力"
|
||||
// },
|
||||
// 5023: {
|
||||
// uuid: 5023, name: "暗影杀手", path: "hc3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Melee, hp: 1500, ap: 142,
|
||||
@@ -395,7 +365,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
// { s_uuid: 6403, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 10 } },
|
||||
// { s_uuid: 6404, t_num: 7, overrides: { TGroup: TGroup.Self, ap: 15 } }
|
||||
// ],
|
||||
// info: "每攻击5次永久+10%暴击率,每攻击7次永久+15%暴伤"
|
||||
// info: "攻击5次永久+10%暴击率,攻击7次永久+15%暴伤"
|
||||
// },
|
||||
|
||||
// ========== atking 类 — 射手(队友强化,hit_count 控制目标数) ==========
|
||||
@@ -412,15 +382,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
5: { atking: [{ s_uuid: 6401, t_num: 5, overrides: { TGroup: TGroup.Team, hit_count: 2, ap: 12 } }] },
|
||||
6: { field: [7006] },
|
||||
},
|
||||
info: "每攻击5次为随机队友永久提升攻击力(8→10→12,5级起2名);6级起全队攻击速度+10%",
|
||||
infos: [
|
||||
"攻击5次为随机1名队友永久+8攻击力",
|
||||
"攻击5次为随机1名队友永久+10攻击力",
|
||||
"额外+60最大生命",
|
||||
"攻击5次为随机1名队友永久+12攻击力",
|
||||
"攻击5次为随机2名队友永久+12攻击力",
|
||||
"攻击5次为随机2名队友永久+12攻击力;驻场光环:全队攻击速度+10%",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>攻击提升队友攻击\n<color=#9B59B6>✦</color>光环全队提升攻速",
|
||||
},
|
||||
5032: {
|
||||
uuid: 5032, name: "战术弓手", path: "ha2", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, hp: 430, ap: 120,
|
||||
@@ -438,21 +400,13 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
]
|
||||
},
|
||||
},
|
||||
info: "每攻击5次为随机3名队友提供+10%暴击率(持续5秒);2级+12%,4级目标数4名且持续6秒,5级触发阈值降至4次;6级起每攻击7次为随机3名队友永久+12%暴伤",
|
||||
infos: [
|
||||
"攻击5次为随机3名队友+10%暴击率5秒",
|
||||
"攻击5次为随机3名队友+12%暴击率5秒",
|
||||
"额外+80最大生命",
|
||||
"攻击5次为随机4名队友+12%暴击率6秒",
|
||||
"攻击4次为随机4名队友+12%暴击率6秒",
|
||||
"攻击4次为随机4名队友+12%暴击率6秒;攻击7次为随机3名队友永久+12%暴击伤害",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>攻击提升队友暴击\n<color=#9B59B6>✦</color>攻击提升队友暴伤",
|
||||
},
|
||||
// 5032: {
|
||||
// uuid: 5032, name: "战术弓手", path: "ha2", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, hp: 430, ap: 120,
|
||||
// skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal1].cd, ccd: 0 } },
|
||||
// atking: [{ s_uuid: 6403, t_num: 5, overrides: { TGroup: TGroup.Team, hit_count: 3, ap: 10 } }],
|
||||
// info: "每攻击5次为随机3名队友永久提升暴击率10%"
|
||||
// info: "攻击5次为随机3名队友永久提升暴击率10%"
|
||||
// },
|
||||
// 5033: {
|
||||
// uuid: 5033, name: "鹰眼弓将", path: "ha3", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, hp: 717, ap: 200,
|
||||
@@ -461,7 +415,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
// { s_uuid: 6401, t_num: 5, overrides: { TGroup: TGroup.Team, hit_count: 6, ap: 8 } },
|
||||
// { s_uuid: 6404, t_num: 7, overrides: { TGroup: TGroup.Team, hit_count: 6, ap: 12 } }
|
||||
// ],
|
||||
// info: "每攻击5次为随机6名队友永久+8攻击力,每攻击7次永久+12%暴伤"
|
||||
// info: "攻击5次为随机6名队友永久+8攻击力,攻击7次永久+12%暴伤"
|
||||
// },
|
||||
|
||||
// ========== dead 类(战士+刺客 · 死亡遗产) ==========
|
||||
@@ -524,15 +478,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
// 6级终极遗志:追加 AP+30;终极涅槃:复活 2 次血量 70%
|
||||
6: { dead: [{ s_uuid: 6301, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 8 } }, { s_uuid: 6402, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 150 } }, { s_uuid: 6401, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 30 } }], revive: { s_uuid: 6501, r_num: 2, upr: 0.7 } },
|
||||
},
|
||||
info: "死亡时为全队提供 80 永久生命值与 5 层护盾;6级解锁 30 永久攻击力,且可复活 2 次",
|
||||
infos: [
|
||||
"死亡时全队永久+80最大生命、获得5次格挡护盾;可复活1次(50%血量)",
|
||||
"死亡时全队永久+100最大生命、获得6次格挡护盾;可复活1次(50%血量)",
|
||||
"死亡时全队永久+100最大生命、获得6次格挡护盾;可复活1次(60%血量)",
|
||||
"死亡时全队永久+120最大生命、获得7次格挡护盾;可复活1次(60%血量)",
|
||||
"死亡时全队永久+150最大生命、获得8次格挡护盾;可复活1次(60%血量)",
|
||||
"死亡时全队永久+150最大生命、+30攻击力、获得8次格挡护盾;可复活2次(70%血量)",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>死亡全队加护盾\n<color=#27AE60>✧</color>复活1:50%血量\n<color=#9B59B6>✦</color>死亡加攻击\n<color=#9B59B6>✦</color>复活2:70%血量",
|
||||
},
|
||||
|
||||
// ========== fstart 类(法师 · 战前增益) ==========
|
||||
@@ -549,15 +495,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
5: { fstart: [{ s_uuid: 6401, t_num: 1, overrides: { TGroup: TGroup.Team, timed_buff_id: 7111, buff_value: 90, buff_duration: 10 } }] },
|
||||
6: { field: [7204] },
|
||||
},
|
||||
info: "战斗开始时全队攻击力限时提升(+30/5s→+60/7s→+90/10s);6级起全队暴击率+20%(驻场)",
|
||||
infos: [
|
||||
"战斗开始全队攻击力+30,持续5秒",
|
||||
"战斗开始全队攻击力+30,持续5秒;额外+80最大生命",
|
||||
"战斗开始全队攻击力+60,持续7秒",
|
||||
"战斗开始全队攻击力+60,持续7秒;额外+80最大生命",
|
||||
"战斗开始全队攻击力+90,持续10秒",
|
||||
"战斗开始全队攻击力+90,持续10秒;驻场光环:全队暴击率+20%",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>开局全队提升攻击\n<color=#9B59B6>✦</color>光环全队提升暴击",
|
||||
},
|
||||
5052: {
|
||||
uuid: 5052, name: "护盾牧师", path: "hm2", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, hp: 287, ap: 80,
|
||||
@@ -570,15 +508,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
5: { fstart: [{ s_uuid: 6301, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 6 } }] },
|
||||
6: { field: [7007] },
|
||||
},
|
||||
info: "战斗开始时为全队添加3层护盾;2级4层,4级5层,5级6层;6级起全队最大生命值+10%(驻场)",
|
||||
infos: [
|
||||
"战斗开始全队获得3次格挡护盾",
|
||||
"战斗开始全队获得4次格挡护盾",
|
||||
"战斗开始全队获得4次格挡护盾;额外+80最大生命",
|
||||
"战斗开始全队获得5次格挡护盾",
|
||||
"战斗开始全队获得6次格挡护盾",
|
||||
"战斗开始全队获得6次格挡护盾;驻场光环:全队最大生命+10%",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>开局全队获得护盾\n<color=#9B59B6>✦</color>光环全队提升生命",
|
||||
},
|
||||
// 5052: {
|
||||
// uuid: 5052, name: "护盾牧师", path: "hm2", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, hp: 287, ap: 80,
|
||||
@@ -636,15 +566,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
5: { field: [7202, 7206] },
|
||||
6: { field: [7202, 7206, 7007] },
|
||||
},
|
||||
info: "驻场期间全队攻击力+10%;2级+20%,4级追加攻击速度+10%,5级攻速+20%;6级追加最大生命值+10%",
|
||||
infos: [
|
||||
"驻场光环:全队攻击力+10%",
|
||||
"驻场光环:全队攻击力+20%",
|
||||
"驻场光环:全队攻击力+20%;额外+80最大生命",
|
||||
"驻场光环:全队攻击力+20%、攻击速度+10%",
|
||||
"驻场光环:全队攻击力+20%、攻击速度+20%",
|
||||
"驻场光环:全队攻击力+20%、攻击速度+20%、最大生命+10%",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>光环全队提升攻击\n<color=#9B59B6>✦</color>追加提升攻速生命",
|
||||
},
|
||||
|
||||
// ========== fend + atking 类(辅助 · 治疗续航 + 波次增益) ==========
|
||||
@@ -666,36 +588,28 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
]
|
||||
},
|
||||
},
|
||||
info: "每攻击5次治疗低血量队友(200→300%,5级起全队);6级起每攻击7次为全队添加3层护盾",
|
||||
infos: [
|
||||
"攻击5次治疗低血量队友,回复攻击力200%生命",
|
||||
"攻击5次治疗低血量队友,回复攻击力250%生命",
|
||||
"攻击5次治疗低血量队友,回复攻击力250%生命;额外+80最大生命",
|
||||
"攻击5次治疗低血量队友,回复攻击力300%生命",
|
||||
"攻击5次治疗2名低血量队友,回复攻击力300%生命",
|
||||
"攻击5次治疗2名低血量队友,回复攻击力300%生命;攻击7次全队获得3次格挡护盾",
|
||||
]
|
||||
info: "<color=#27AE60>✧</color>攻击治疗低血队友\n<color=#9B59B6>✦</color>攻击全队获得护盾",
|
||||
},
|
||||
// 5072: {
|
||||
// uuid: 5072, name: "小金库", path: "hh2", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Long, hp: 287, ap: 80,
|
||||
// skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Mid1].cd, ccd: 0 } },
|
||||
// atking: [{ s_uuid: 6302, t_num: 5, overrides: { TGroup: TGroup.Team, ap: 200 } }],
|
||||
// fend: [{ s_uuid: 6303, t_num: 1, overrides: { gold: 1 } }],
|
||||
// info: "每攻击5次治疗全队,每波结束获得1金币"
|
||||
// info: "攻击5次治疗全队,每波结束获得1金币"
|
||||
// },
|
||||
// 5073: {
|
||||
// uuid: 5073, name: "强化牧师", path: "hh3", fac: FacSet.HERO, card_lv: 3, lv: 1, type: HType.Long, hp: 430, ap: 120,
|
||||
// skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal3].cd, ccd: 0 } },
|
||||
// atking: [{ s_uuid: 6302, t_num: 5, overrides: { TGroup: TGroup.Team, ap: 250 } }],
|
||||
// fend: [{ s_uuid: 6401, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 10 } }],
|
||||
// info: "每攻击5次治疗全队,每波结束全队永久+10攻击力"
|
||||
// info: "攻击5次治疗全队,每波结束全队永久+10攻击力"
|
||||
// },
|
||||
// 5074: {
|
||||
// uuid: 5074, name: "生命牧师", path: "hh4", fac: FacSet.HERO, card_lv: 4, lv: 1, type: HType.Long, hp: 573, ap: 160,
|
||||
// skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Normal3].cd, ccd: 0 } },
|
||||
// atking: [{ s_uuid: 6302, t_num: 5, overrides: { TGroup: TGroup.Team, ap: 250 } }],
|
||||
// fend: [{ s_uuid: 6402, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 80 } }],
|
||||
// info: "每攻击5次治疗全队,每波结束全队永久+80最大生命值"
|
||||
// info: "攻击5次治疗全队,每波结束全队永久+80最大生命值"
|
||||
// },
|
||||
// 5075: {
|
||||
// uuid: 5075, name: "全能牧师", path: "hh5", fac: FacSet.HERO, card_lv: 5, lv: 1, type: HType.Long, hp: 717, ap: 200,
|
||||
@@ -705,7 +619,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
// { s_uuid: 6401, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 12 } },
|
||||
// { s_uuid: 6402, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 60 } }
|
||||
// ],
|
||||
// info: "每攻击5次治疗全队300%AP,每波结束全队永久+12攻击力、+60最大生命值"
|
||||
// info: "攻击5次治疗全队300%AP,每波结束全队永久+12攻击力、+60最大生命值"
|
||||
// },
|
||||
|
||||
|
||||
@@ -814,7 +728,293 @@ export const HeroList: number[] = [
|
||||
// 5072, 5073, 5074, 5075,
|
||||
];
|
||||
|
||||
// ==================== 英雄强度评分公式 ====================
|
||||
|
||||
/** 英雄强度公式调参权重(可在编辑器或运行时热调) */
|
||||
export const HERO_POWER_WEIGHTS = {
|
||||
W_HP: 1.0,
|
||||
W_DPS: 30.0,
|
||||
W_atking: 1.0,
|
||||
W_atked: 0.8,
|
||||
W_fstart: 1.0,
|
||||
W_fend: 1.2,
|
||||
W_dead: 0.6,
|
||||
W_field_alive: 1.0,
|
||||
W_revive: 1.0,
|
||||
W_heal: 0.8,
|
||||
W_shield: 1.0,
|
||||
W_control: 0.5,
|
||||
expectedEnemyDmg: 20,
|
||||
};
|
||||
|
||||
/** 计算目标等级下 evolve 提供的累计属性加成 */
|
||||
function getEvolveBonuses(hero: heroInfo, targetLv: number): { hp: number, ap: number } {
|
||||
let hp = 0;
|
||||
let ap = 0;
|
||||
if (!hero.evolve) return { hp, ap };
|
||||
const maxLv = Math.max(1, Math.min(targetLv, FightSet.HERO_MAX_LV));
|
||||
for (let lv = 2; lv <= maxLv; lv++) {
|
||||
const ev = hero.evolve[lv];
|
||||
if (ev) {
|
||||
hp += ev.hp_bonus || 0;
|
||||
ap += ev.ap_bonus || 0;
|
||||
}
|
||||
}
|
||||
return { hp, ap };
|
||||
}
|
||||
|
||||
/** 计算目标等级下的有效 HP/AP(含等级倍率与 evolve 加成) */
|
||||
function getEffectiveStats(hero: heroInfo, targetLv: number): { hp: number, ap: number } {
|
||||
const bonuses = getEvolveBonuses(hero, targetLv);
|
||||
const lv = Math.max(1, targetLv);
|
||||
const multiplier = Math.pow(FightSet.HERO_LV_MULTIPLIER, lv - 1);
|
||||
return {
|
||||
hp: (hero.hp + bonuses.hp) * multiplier,
|
||||
ap: (hero.ap + bonuses.ap) * multiplier,
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取英雄普通攻击间隔(取 skills[0] 的 cd) */
|
||||
function getAttackCd(hero: heroInfo): number {
|
||||
const skillIds = Object.keys(hero.skills);
|
||||
if (skillIds.length === 0) return 1.0;
|
||||
const firstKey = Number(skillIds[0]);
|
||||
const skill = hero.skills[firstKey];
|
||||
return skill?.cd || 1.0;
|
||||
}
|
||||
|
||||
/** 计算目标群体范围系数 */
|
||||
function calcScopeMul(group: TGroup): number {
|
||||
return (group === TGroup.Ally || group === TGroup.Team) ? FightSet.HERO_MAX_NUM : 1;
|
||||
}
|
||||
|
||||
/** 将计时 buff 折算为永久等效价值 */
|
||||
function calcBuffPermanentEquivalent(buff: BuffConfig, apEff: number, cd: number, buffValueOverride?: number): number {
|
||||
const weights = HERO_POWER_WEIGHTS;
|
||||
let value = 0;
|
||||
if (buff.modifiers) {
|
||||
for (const mod of buff.modifiers) {
|
||||
const dps = apEff / cd;
|
||||
const modValue = buffValueOverride ?? mod.value;
|
||||
switch (mod.attr) {
|
||||
case Attrs.ap:
|
||||
if (mod.op === ModOp.Flat) {
|
||||
value += (modValue / cd) * weights.W_DPS;
|
||||
} else if (mod.op === ModOp.PercentAdd) {
|
||||
value += (modValue / 100) * dps * weights.W_DPS;
|
||||
}
|
||||
break;
|
||||
case Attrs.hp:
|
||||
case Attrs.hp_max:
|
||||
value += modValue * weights.W_HP;
|
||||
break;
|
||||
case Attrs.critical:
|
||||
value += (modValue / 100) * dps * 0.5 * weights.W_DPS;
|
||||
break;
|
||||
case Attrs.critical_damage:
|
||||
value += (modValue / 100) * dps * 0.05 * weights.W_DPS;
|
||||
break;
|
||||
case Attrs.speed:
|
||||
value += (modValue / 100) * dps * weights.W_DPS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (buff.tick) {
|
||||
const ticks = Math.max(1, buff.duration / buff.tick.interval);
|
||||
const totalHeal = ticks * (buff.tick.damage_or_heal + (buff.tick.scale_by_ap_pct / 100) * apEff);
|
||||
value += Math.max(0, totalHeal) * weights.W_heal;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 计算 Support 类技能的效果价值 */
|
||||
function calcSupportEffectValue(config: SkillConfig, apEff: number, cd: number): number {
|
||||
const weights = HERO_POWER_WEIGHTS;
|
||||
const value = config.ap || 0;
|
||||
|
||||
// 计时 buff:先算永久等效,再按战斗时长覆盖率折算
|
||||
if (config.timed_buff_id) {
|
||||
const buff = BuffList[config.timed_buff_id];
|
||||
if (buff) {
|
||||
const permanentEquivalent = calcBuffPermanentEquivalent(buff, apEff, cd, config.buff_value);
|
||||
const duration = config.buff_duration ?? buff.duration;
|
||||
const uptime = Math.min(duration / FightSet.FiIGHT_TIME, 1);
|
||||
return permanentEquivalent * uptime;
|
||||
}
|
||||
}
|
||||
|
||||
switch (config.buff_type) {
|
||||
case Attrs.ap:
|
||||
return (value / cd) * weights.W_DPS;
|
||||
case Attrs.hp_max:
|
||||
case Attrs.hp:
|
||||
return value * weights.W_HP;
|
||||
case Attrs.critical:
|
||||
return (value / 100) * (apEff / cd) * 0.5 * weights.W_DPS;
|
||||
case Attrs.critical_damage:
|
||||
return (value / 100) * (apEff / cd) * 0.05 * weights.W_DPS;
|
||||
case Attrs.stun_chance:
|
||||
case Attrs.freeze_chance:
|
||||
case Attrs.knockback_chance:
|
||||
return value * weights.W_control;
|
||||
case Attrs.speed:
|
||||
return (value / 100) * (apEff / cd) * weights.W_DPS;
|
||||
default:
|
||||
return value * weights.W_control;
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算单条技能配置的效果价值 */
|
||||
function calcSkillEffectValue(skill: SkillConfig, overrides: SkillOverrides | undefined, hero: heroInfo, targetLv: number): number {
|
||||
const { ap: apEff } = getEffectiveStats(hero, targetLv);
|
||||
const cd = getAttackCd(hero);
|
||||
const config = overrides ? mergeSkillParams(skill, overrides) : skill;
|
||||
const weights = HERO_POWER_WEIGHTS;
|
||||
|
||||
switch (config.kind) {
|
||||
case SkillKind.Damage:
|
||||
return ((config.ap || 0) / 100) * apEff;
|
||||
case SkillKind.Heal:
|
||||
return ((config.ap || 0) / 100) * apEff * weights.W_heal;
|
||||
case SkillKind.Shield:
|
||||
return (config.ap || 0) * weights.expectedEnemyDmg * weights.W_shield;
|
||||
case SkillKind.Support:
|
||||
return calcSupportEffectValue(config, apEff, cd);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算普通触发类技能(atking/atked/fstart/fend/dead)的价值 */
|
||||
function calcTriggerPower(
|
||||
triggerConfigs: { s_uuid: number, t_num?: number, overrides?: SkillOverrides }[],
|
||||
hero: heroInfo,
|
||||
targetLv: number,
|
||||
type: SkillTriggerType
|
||||
): number {
|
||||
if (!triggerConfigs || triggerConfigs.length === 0) return 0;
|
||||
const weights = HERO_POWER_WEIGHTS;
|
||||
let total = 0;
|
||||
|
||||
for (const trig of triggerConfigs) {
|
||||
const skill = SkillSet[trig.s_uuid];
|
||||
if (!skill) continue;
|
||||
|
||||
const effectValue = calcSkillEffectValue(skill, trig.overrides, hero, targetLv);
|
||||
const overrides = trig.overrides || {};
|
||||
const group = overrides.TGroup ?? skill.TGroup;
|
||||
const scopeMul = calcScopeMul(group);
|
||||
const tNum = trig.t_num || 1;
|
||||
|
||||
let weight = 1;
|
||||
let frequency = 1;
|
||||
switch (type) {
|
||||
case SkillTriggerType.Atking:
|
||||
weight = weights.W_atking;
|
||||
frequency = 1 / tNum;
|
||||
break;
|
||||
case SkillTriggerType.Atked:
|
||||
weight = weights.W_atked;
|
||||
frequency = 1 / tNum;
|
||||
break;
|
||||
case SkillTriggerType.FStart:
|
||||
weight = weights.W_fstart;
|
||||
break;
|
||||
case SkillTriggerType.FEnd:
|
||||
weight = weights.W_fend;
|
||||
break;
|
||||
case SkillTriggerType.Dead:
|
||||
weight = weights.W_dead;
|
||||
break;
|
||||
}
|
||||
|
||||
total += effectValue * frequency * scopeMul * weight;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** 计算驻场光环技能的单位价值 */
|
||||
function calcFieldSkillValue(field: FieldSkillConfig, hpEff: number, apEff: number, cd: number, scopeMul: number): number {
|
||||
const weights = HERO_POWER_WEIGHTS;
|
||||
const dps = apEff / cd;
|
||||
|
||||
switch (field.type) {
|
||||
case FieldSkillType.HeroAtk:
|
||||
return field.value * dps * scopeMul * weights.W_DPS;
|
||||
case FieldSkillType.HeroHp:
|
||||
return field.value * hpEff * scopeMul * weights.W_HP;
|
||||
case FieldSkillType.HeroSpeed:
|
||||
return field.value * dps * scopeMul * weights.W_DPS;
|
||||
case FieldSkillType.HeroCrit:
|
||||
return field.value * dps * scopeMul * 0.5 * weights.W_DPS;
|
||||
case FieldSkillType.HeroCritDamage:
|
||||
return field.value * dps * scopeMul * 0.05 * weights.W_DPS;
|
||||
case FieldSkillType.HeroStun:
|
||||
case FieldSkillType.HeroWindFury:
|
||||
case FieldSkillType.HeroPuncture:
|
||||
return field.value * scopeMul * weights.W_control * 100;
|
||||
default:
|
||||
return field.value * scopeMul * weights.W_control * 10;
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算驻场光环总价值 */
|
||||
function calcFieldPower(fieldUuids: number[] | undefined, hero: heroInfo, targetLv: number): number {
|
||||
if (!fieldUuids || fieldUuids.length === 0) return 0;
|
||||
const weights = HERO_POWER_WEIGHTS;
|
||||
const { hp: hpEff, ap: apEff } = getEffectiveStats(hero, targetLv);
|
||||
const cd = getAttackCd(hero);
|
||||
const scopeMul = FightSet.HERO_MAX_NUM;
|
||||
let total = 0;
|
||||
|
||||
for (const uuid of fieldUuids) {
|
||||
const field = FieldSkillSet[uuid];
|
||||
if (!field) continue;
|
||||
total += calcFieldSkillValue(field, hpEff, apEff, cd, scopeMul);
|
||||
}
|
||||
return total * weights.W_field_alive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算英雄强度评分(power score)。
|
||||
* @param hero 英雄/怪物配置
|
||||
* @param targetLv 目标等级,默认 1
|
||||
* @returns 无量纲强度分,数值越大越强
|
||||
*/
|
||||
export function calcHeroPower(hero: heroInfo, targetLv: number = 1): number {
|
||||
const weights = HERO_POWER_WEIGHTS;
|
||||
const { hp: hpEff, ap: apEff } = getEffectiveStats(hero, targetLv);
|
||||
const cd = getAttackCd(hero);
|
||||
|
||||
const basePower = hpEff * weights.W_HP + (apEff / cd) * weights.W_DPS;
|
||||
let triggerPower = 0;
|
||||
|
||||
if (hero.atking) triggerPower += calcTriggerPower(hero.atking, hero, targetLv, SkillTriggerType.Atking);
|
||||
if (hero.atked) triggerPower += calcTriggerPower(hero.atked, hero, targetLv, SkillTriggerType.Atked);
|
||||
if (hero.fstart) triggerPower += calcTriggerPower(hero.fstart, hero, targetLv, SkillTriggerType.FStart);
|
||||
if (hero.fend) triggerPower += calcTriggerPower(hero.fend, hero, targetLv, SkillTriggerType.FEnd);
|
||||
if (hero.dead) triggerPower += calcTriggerPower(hero.dead, hero, targetLv, SkillTriggerType.Dead);
|
||||
if (hero.field) triggerPower += calcFieldPower(hero.field, hero, targetLv);
|
||||
if (hero.revive) {
|
||||
triggerPower += hpEff * hero.revive.upr * hero.revive.r_num * weights.W_revive;
|
||||
}
|
||||
|
||||
return basePower + triggerPower;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化英雄强度评分。
|
||||
* @param hero 英雄配置
|
||||
* @param targetLv 目标等级
|
||||
* @param baseHeroId 基准英雄 uuid
|
||||
* @param baseScore 基准分
|
||||
* @returns 以基准英雄为 baseScore 的相对分数
|
||||
*/
|
||||
export function calcHeroPowerNormalized(hero: heroInfo, targetLv: number = 1, baseHeroId: number = 5011, baseScore: number = 100): number {
|
||||
const baseHero = HeroInfo[baseHeroId];
|
||||
if (!baseHero) return 0;
|
||||
const basePower = calcHeroPower(baseHero, targetLv);
|
||||
if (basePower === 0) return 0;
|
||||
return calcHeroPower(hero, targetLv) / basePower * baseScore;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
import { CardConfig, CardType } from "../common/config/CardSet";
|
||||
import { HeroInfo } from "../common/config/heroSet";
|
||||
import { buildSkillDescRich } from "../common/config/HeroSkillDesc";
|
||||
import { oops } from "db://oops-framework/core/Oops";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
@@ -67,7 +66,7 @@ export class CHeroComp extends CCComp {
|
||||
@property(Label)
|
||||
private fied3: Label = null;
|
||||
|
||||
/** 描述富文本(多行,数值绿色高亮、技能名蓝色,由 buildSkillDescRich 生成) */
|
||||
/** 描述富文本(单条,直接展示 heroSet.info 手写文案) */
|
||||
@property(RichText)
|
||||
private info_rich: RichText = null;
|
||||
|
||||
@@ -288,9 +287,9 @@ export class CHeroComp extends CCComp {
|
||||
// 背景:按 card_lv 激活对应颜色子节点(1→green 2→blue 3→purple 4→red 5→yellow)
|
||||
this.updateBgNode();
|
||||
|
||||
// 描述富文本(英雄触发技能,数值随等级配置高亮)
|
||||
// 描述富文本:直接取 heroSet 的 info 字段(单条手写文案)
|
||||
if (this.info_rich) {
|
||||
const desc = hero ? buildSkillDescRich(hero) : (this.cardData.info || "");
|
||||
const desc = hero?.info || this.cardData.info || "";
|
||||
this.info_rich.string = desc;
|
||||
this.info_rich.node.active = !!desc;
|
||||
}
|
||||
|
||||
@@ -12,15 +12,18 @@
|
||||
* - HeroInfo(heroSet)—— 英雄静态配置
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Node, Sprite, Label, RichText, resources, AnimationClip, SpriteFrame, NodeEventType, UITransform, Tween, tween, Vec3 } from "cc";
|
||||
import { _decorator, Node, Sprite, Label, RichText, resources, AnimationClip, SpriteFrame, NodeEventType, Tween, tween, Vec3 } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
import { oops } from "db://oops-framework/core/Oops";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { HeroInfo } from "../common/config/heroSet";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { Hero } from "../hero/Hero";
|
||||
import { getLvColor } from "../common/config/GameSet";
|
||||
import { buildSkillDescRich } from "../common/config/HeroSkillDesc";
|
||||
import { MissionEconomy } from "./MissionEconomy";
|
||||
import { UIID } from "../common/config/GameUIConfig";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@@ -53,6 +56,14 @@ export class HeroBoxComp extends CCComp {
|
||||
@property(Node)
|
||||
private noHero: Node = null;
|
||||
|
||||
/** 出售按钮(绑定英雄时激活,点击出售该英雄) */
|
||||
@property(Node)
|
||||
private sell_btn: Node = null;
|
||||
|
||||
/** 出售价格标签(按英雄等级显示当前卖价,含驻场加成) */
|
||||
@property(Label)
|
||||
private sell_price_label: Label = null;
|
||||
|
||||
/** 技能描述富文本(多行,展示当前等级触发技能,数值高亮) */
|
||||
@property(RichText)
|
||||
private skill_label: RichText = null;
|
||||
@@ -124,35 +135,12 @@ export class HeroBoxComp extends CCComp {
|
||||
|
||||
// ======================== 更多属性折叠栏 ========================
|
||||
|
||||
/** 属性栏容器(展开/收起时动态调整高度) */
|
||||
/** 次级属性栏容器(默认隐藏,长按更多按钮时显示) */
|
||||
@property(Node)
|
||||
private attr_box: Node = null;
|
||||
/** 更多按钮(点击展开/收起次级属性栏) */
|
||||
/** 更多按钮(长按显示次级属性栏,放开隐藏) */
|
||||
@property(Node)
|
||||
private more_btn: Node = null;
|
||||
/** 击晕信息栏(默认隐藏) */
|
||||
@property(Node)
|
||||
private stun_row: Node = null;
|
||||
/** 冰冻信息栏(默认隐藏) */
|
||||
@property(Node)
|
||||
private freeze_row: Node = null;
|
||||
/** 击退信息栏(默认隐藏) */
|
||||
@property(Node)
|
||||
private knockback_row: Node = null;
|
||||
/** 穿透信息栏(默认隐藏) */
|
||||
@property(Node)
|
||||
private puncture_row: Node = null;
|
||||
/** 风怒信息栏(默认隐藏) */
|
||||
@property(Node)
|
||||
private wfuny_row: Node = null;
|
||||
|
||||
/** 属性栏收起高度 */
|
||||
private attrBoxHeightCollapsed: number = 100;
|
||||
/** 属性栏展开高度 */
|
||||
private attrBoxHeightExpanded: number = 210;
|
||||
|
||||
/** 次级属性栏是否展开 */
|
||||
private moreExpanded: boolean = false;
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
@@ -168,20 +156,73 @@ export class HeroBoxComp extends CCComp {
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad() {
|
||||
// 次级属性栏默认收起
|
||||
this.setMoreRowsVisible(false);
|
||||
this.more_btn?.on(NodeEventType.TOUCH_END, this.onMoreBtnClick, this);
|
||||
// 次级属性栏默认隐藏,长按更多按钮时显示,放开隐藏
|
||||
if (this.attr_box && this.attr_box.isValid) {
|
||||
this.attr_box.active = false;
|
||||
}
|
||||
this.more_btn?.on(NodeEventType.TOUCH_START, this.onMoreBtnTouchStart, this);
|
||||
this.more_btn?.on(NodeEventType.TOUCH_END, this.onMoreBtnTouchEnd, this);
|
||||
this.more_btn?.on(NodeEventType.TOUCH_CANCEL, this.onMoreBtnTouchEnd, this);
|
||||
// 空槽位点击 → 打开英雄卡池
|
||||
this.noHero?.on(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
|
||||
// 出售按钮点击 → 出售当前绑定英雄
|
||||
this.sell_btn?.on(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (this.more_btn && this.more_btn.isValid) {
|
||||
this.more_btn.off(NodeEventType.TOUCH_END, this.onMoreBtnClick, this);
|
||||
this.more_btn.off(NodeEventType.TOUCH_START, this.onMoreBtnTouchStart, this);
|
||||
this.more_btn.off(NodeEventType.TOUCH_END, this.onMoreBtnTouchEnd, this);
|
||||
this.more_btn.off(NodeEventType.TOUCH_CANCEL, this.onMoreBtnTouchEnd, this);
|
||||
}
|
||||
if (this.noHero && this.noHero.isValid) {
|
||||
this.noHero.off(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
|
||||
}
|
||||
if (this.sell_btn && this.sell_btn.isValid) {
|
||||
this.sell_btn.off(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 出售按钮点击:弹出公共确认窗口,确认后再执行出售。
|
||||
* Why: 出售不可逆,需二次确认防止误触。
|
||||
*/
|
||||
private onSellHeroClick() {
|
||||
if (!this.eid || !this.model) return;
|
||||
oops.audio.playEffect("music/button");
|
||||
|
||||
const heroName = this.model.hero_name ?? "";
|
||||
const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
|
||||
const gold = MissionEconomy.getSellGold(heroLv);
|
||||
|
||||
// oops.gui 公共确认窗口(自定义 CommonPrompt:内容支持富文本高亮)
|
||||
oops.gui.open(UIID.Window, {
|
||||
title: "出售英雄",
|
||||
content: `确定出售 <color=#C0392B>${heroName}</color>(Lv.${heroLv})吗?\n出售后获得 <color=#1E8449>${gold}</color> 金币`,
|
||||
okWord: "出售",
|
||||
cancelWord: "取消",
|
||||
needCancel: true,
|
||||
okFunc: () => this.executeSellHero()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行出售:移除英雄实体、按等级结算金币并派发 HeroSell 事件。
|
||||
* MissionCardComp 监听该事件后会刷新槽位,本盒自动切为空状态。
|
||||
*/
|
||||
private executeSellHero() {
|
||||
// 确认窗口打开期间英雄可能已死亡/被移除,需二次校验
|
||||
if (!this.eid || !this.model) return;
|
||||
const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
|
||||
// 移除 ECS 实体失败(实体已失效)则不做后续结算
|
||||
if (!Hero.removeByEid(this.eid)) return;
|
||||
|
||||
// 统一经济管理入口:按等级计算卖价并加金币
|
||||
const gold = MissionEconomy.executeSellHero(heroLv);
|
||||
mLogger.log(this.debugMode, "HeroBoxComp", "executeSellHero", { eid: this.eid, heroLv, gold });
|
||||
|
||||
// 通知 MissionCardComp 更新场上英雄数量并刷新英雄盒槽位
|
||||
oops.message.dispatchEvent(GameEvent.HeroSell, { eid: this.eid });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,45 +263,22 @@ export class HeroBoxComp extends CCComp {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/** 更多按钮点击:切换次级属性栏显示/隐藏 */
|
||||
private onMoreBtnClick() {
|
||||
/** 更多按钮按下:显示次级属性栏 */
|
||||
private onMoreBtnTouchStart() {
|
||||
oops.audio.playEffect("music/button");
|
||||
this.moreExpanded = !this.moreExpanded;
|
||||
this.setMoreRowsVisible(this.moreExpanded);
|
||||
this.setAttrBoxVisible(true);
|
||||
}
|
||||
|
||||
/** 设置 5 个次级属性栏(击晕/冰冻/击退/穿透/风怒)的可见性,并同步调整属性栏高度 */
|
||||
private setMoreRowsVisible(visible: boolean) {
|
||||
const rows = [this.stun_row, this.freeze_row, this.knockback_row, this.puncture_row, this.wfuny_row];
|
||||
for (const row of rows) {
|
||||
if (row && row.isValid) {
|
||||
row.active = visible;
|
||||
/** 更多按钮放开/取消:隐藏次级属性栏 */
|
||||
private onMoreBtnTouchEnd() {
|
||||
this.setAttrBoxVisible(false);
|
||||
}
|
||||
}
|
||||
// 展开/收起时切换属性栏高度(默认 125 ↔ 210,可在编辑器自定义)
|
||||
|
||||
/** 切换次级属性栏容器可见性 */
|
||||
private setAttrBoxVisible(visible: boolean) {
|
||||
if (this.attr_box && this.attr_box.isValid) {
|
||||
const transform = this.attr_box.getComponent(UITransform) || this.attr_box.addComponent(UITransform);
|
||||
const height = visible ? this.attrBoxHeightExpanded : this.attrBoxHeightCollapsed;
|
||||
transform.setContentSize(transform.width, height);
|
||||
this.attr_box.active = visible;
|
||||
}
|
||||
// 切换更多按钮状态子节点:收起=n_* 激活,展开=a_* 激活
|
||||
this.setMoreBtnState(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换更多按钮的状态子节点。
|
||||
* 收起态激活 n_Bg / n_Arrow,展开态激活 a_Bg / a_Arrow。
|
||||
*/
|
||||
private setMoreBtnState(expanded: boolean) {
|
||||
if (!this.more_btn || !this.more_btn.isValid) return;
|
||||
const nBg = this.more_btn.getChildByName("n_Bg");
|
||||
const nArrow = this.more_btn.getChildByName("n_Arrow");
|
||||
const aBg = this.more_btn.getChildByName("a_Bg");
|
||||
const aArrow = this.more_btn.getChildByName("a_Arrow");
|
||||
if (nBg) nBg.active = !expanded;
|
||||
if (nArrow) nArrow.active = !expanded;
|
||||
if (aBg) aBg.active = expanded;
|
||||
if (aArrow) aArrow.active = expanded;
|
||||
}
|
||||
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
@@ -294,6 +312,9 @@ export class HeroBoxComp extends CCComp {
|
||||
if (this.noHero && this.noHero.isValid) {
|
||||
this.noHero.active = false;
|
||||
}
|
||||
if (this.sell_btn && this.sell_btn.isValid) {
|
||||
this.sell_btn.active = true;
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
@@ -308,6 +329,13 @@ export class HeroBoxComp extends CCComp {
|
||||
this.noHero.active = true;
|
||||
}
|
||||
|
||||
if (this.sell_btn && this.sell_btn.isValid) {
|
||||
this.sell_btn.active = false;
|
||||
}
|
||||
if (this.sell_price_label && this.sell_price_label.isValid) {
|
||||
this.sell_price_label.string = "";
|
||||
}
|
||||
|
||||
if (this.name_label && this.name_label.isValid) {
|
||||
this.name_label.string = "";
|
||||
}
|
||||
@@ -359,6 +387,12 @@ export class HeroBoxComp extends CCComp {
|
||||
this.level_label.color = getLvColor(lv);
|
||||
}
|
||||
|
||||
// ---- 出售价格(按等级计算,含驻场 SellGold 加成) ----
|
||||
if (this.sell_price_label && this.sell_price_label.isValid) {
|
||||
const lv = Math.max(1, Math.floor(this.model.lv ?? 1));
|
||||
this.sell_price_label.string = `${MissionEconomy.getSellGold(lv)}`;
|
||||
}
|
||||
|
||||
// ---- AP / HP ----
|
||||
const finalAp = Math.max(0, Math.floor(this.model.getFinalAp()));
|
||||
const baseAp = Math.max(0, Math.floor(this.model.base_ap ?? 0));
|
||||
@@ -401,8 +435,8 @@ export class HeroBoxComp extends CCComp {
|
||||
|
||||
// ---- 技能描述(当前等级触发技能,读运行时 model 最终配置;数值富文本高亮) ----
|
||||
if (this.skill_label && this.skill_label.isValid) {
|
||||
// 缓存未变则跳过重设,避免 refresh 降频内 RichText 重复重解析排版
|
||||
const desc = buildSkillDescRich(this.model);
|
||||
// 缓存未变则跳过重设,避免 refresh 降频内 RichText 重复重解析排版;隐藏「技能名」仅保留效果描述
|
||||
const desc = buildSkillDescRich(this.model, false);
|
||||
if (desc !== this.skillDescCache) {
|
||||
this.skillDescCache = desc;
|
||||
this.skill_label.string = desc;
|
||||
|
||||
136
design/quick-specs/hero-power-formula-2026-08-01.md
Normal file
136
design/quick-specs/hero-power-formula-2026-08-01.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Quick Design Spec: 英雄强度评分公式
|
||||
|
||||
**Type**: Addition
|
||||
**System**: 英雄/技能配置系统(`assets/script/game/common/config/heroSet.ts`)
|
||||
**GDD Reference**: 暂无项目级 GDD;本规范作为英雄强度锚定的临时设计依据。
|
||||
**Date**: 2026-08-01
|
||||
|
||||
## Change Summary
|
||||
|
||||
在 `heroSet.ts` 中新增一个纯函数 `calcHeroPower(hero, targetLv)`,将现有 `heroInfo` 配置(含普攻、触发技能、`evolve` 成长、驻场光环)转换为**无量纲强度分**。该分数用于:
|
||||
|
||||
1. 对比同 `card_lv` 英雄的相对强度;
|
||||
2. 为关卡怪物总强度提供英雄侧锚定基准;
|
||||
3. 为抽卡/经济定价提供量化参考。
|
||||
|
||||
**不改变现有英雄配置、战斗行为或技能效果。**
|
||||
|
||||
## Motivation
|
||||
|
||||
当前英雄池已从 6 名扩展至 11 名,且每名英雄拥有 6 级成长。策划仅靠基础 HP/AP 判断强度已不够用,尤其 `evolve` 带来的触发阈值变化、驻场光环、复活等机制难以横向比较。一个统一的评分函数可以让关卡难度和卡池分级有据可依。
|
||||
|
||||
## Design Delta
|
||||
|
||||
现有 `heroSet.ts` 只有静态配置,无强度量化。本规范新增 `calcHeroPower` 函数,读取 `heroInfo`、`SkillSet`、`FieldSkillSet`、`BuffList` 等配置,输出标量 power。
|
||||
|
||||
## New Rules / Values
|
||||
|
||||
### 1. 核心公式
|
||||
|
||||
```ts
|
||||
calcHeroPower(hero: heroInfo, targetLv: number = 1): number
|
||||
```
|
||||
|
||||
```text
|
||||
P = HP_EFF * W_HP
|
||||
+ DPS_EFF * W_DPS
|
||||
+ Σ triggerPower(trigger, hero, targetLv)
|
||||
```
|
||||
|
||||
| 符号 | 计算方式 |
|
||||
|---|---|
|
||||
| `HP_EFF` | `(hero.hp + sumHpBonus) * HERO_LV_MULTIPLIER^(targetLv - 1)` |
|
||||
| `AP_EFF` | `(hero.ap + sumApBonus) * HERO_LV_MULTIPLIER^(targetLv - 1)` |
|
||||
| `DPS_EFF` | `AP_EFF / hero.skills[0].cd` |
|
||||
| `sumHpBonus` | `targetLv` 之前所有 `evolve[l].hp_bonus` 之和 |
|
||||
| `sumApBonus` | `targetLv` 之前所有 `evolve[l].ap_bonus` 之和 |
|
||||
|
||||
### 2. 触发技能估值 `triggerPower`
|
||||
|
||||
对 `heroInfo` 中的每种触发字段分别求和:
|
||||
|
||||
| 触发类型 | 公式 |
|
||||
|---|---|
|
||||
| `atking` | `effectValue(skill, overrides) * (1 / t_num) * scopeMul * W_atking` |
|
||||
| `atked` | `effectValue(skill, overrides) * (1 / t_num) * scopeMul * W_atked` |
|
||||
| `fstart` | `effectValue(skill, overrides) * scopeMul * W_fstart` |
|
||||
| `fend` | `effectValue(skill, overrides) * scopeMul * W_fend` |
|
||||
| `dead` | `effectValue(skill, overrides) * scopeMul * W_dead` |
|
||||
| `field` | `Σ fieldSkillPower(uuid) * W_field_alive` |
|
||||
| `revive` | `HP_EFF * upr * r_num * W_revive` |
|
||||
|
||||
`scopeMul`:
|
||||
- `TGroup.Self` = 1
|
||||
- `TGroup.Ally` / `TGroup.Team` = `HERO_MAX_NUM`(当前为 3)
|
||||
|
||||
### 3. 技能效果估值 `effectValue`
|
||||
|
||||
基于 `SkillConfig.kind`:
|
||||
|
||||
| Kind | 估值 |
|
||||
|---|---|
|
||||
| Damage | `ap_pct / 100 * AP_EFF` |
|
||||
| Heal | `ap_pct / 100 * AP_EFF * W_heal` |
|
||||
| Shield | `shield_layers * expectedEnemyDmg * W_shield`(`shield_layers` 取 `SkillConfig.ap`) |
|
||||
| Support | 按 `buff_type` 折算:<br>• AP → `value / CD * W_DPS`<br>• HP → `value * W_HP`<br>• crit/crit_dmg → 按期望 DPS 增量折算<br>• stun 等控制 → `value * W_control` |
|
||||
| Gold | 忽略(非战斗强度) |
|
||||
|
||||
对于**计时 buff**(`timed_buff_id` 存在),先计算其永久等效值,再按覆盖率折算:
|
||||
|
||||
```text
|
||||
timedValue = permanentEquivalent * min(buff_duration / FIGHT_TIME, 1)
|
||||
```
|
||||
|
||||
其中 `FIGHT_TIME` 取 `GameSet.FiIGHT_TIME`(30 秒)。
|
||||
|
||||
### 4. 调参旋钮
|
||||
|
||||
所有旋钮以常量对象形式放在 `heroSet.ts` 顶部,方便运行时热调:
|
||||
|
||||
| 旋钮 | 默认值 | 建议范围 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `W_HP` | 1.0 | 0.5–2.0 | 每点有效 HP 的强度价值 |
|
||||
| `W_DPS` | 30.0 | 10–100 | 每秒有效伤害的强度价值 |
|
||||
| `W_atking` | 1.0 | 0.5–2.0 | 攻击触发技能频率折算 |
|
||||
| `W_atked` | 0.8 | 0.3–1.5 | 受击触发技能频率折算(低于 atking,因依赖敌方攻击频率) |
|
||||
| `W_fstart` | 1.0 | 0.5–2.0 | 每波开始一次性技能价值 |
|
||||
| `W_fend` | 1.2 | 0.5–2.0 | 每波结束一次性技能价值 |
|
||||
| `W_dead` | 0.6 | 0.2–1.0 | 死亡一次性技能价值 |
|
||||
| `W_field_alive` | 1.0 | 0.5–2.0 | 驻场光环存活期间价值 |
|
||||
| `W_revive` | 1.0 | 0.5–2.0 | 每次复活的价值系数 |
|
||||
| `W_heal` | 0.8 | 0.5–1.2 | 治疗相对直接伤害的折价 |
|
||||
| `W_shield` | 1.0 | 0.5–2.0 | 护盾相对 HP 的折价/溢价 |
|
||||
| `W_control` | 0.5 | 0.1–1.0 | 控制效果(击晕等)的强度折算 |
|
||||
| `expectedEnemyDmg` | 20 | 10–100 | 估算护盾价值时假设的敌方单次伤害 |
|
||||
|
||||
### 5. 归一化(可选)
|
||||
|
||||
函数返回原始分。如需以某个英雄为 100 分基准,可额外提供:
|
||||
|
||||
```ts
|
||||
calcHeroPowerNormalized(hero: heroInfo, targetLv = 1, baseHeroId = 5011, baseScore = 100): number
|
||||
```
|
||||
|
||||
即 `calcHeroPower(hero) / calcHeroPower(HeroInfo[baseHeroId]) * baseScore`。
|
||||
|
||||
## Affected Systems
|
||||
|
||||
| 系统 | 影响 | 所需操作 |
|
||||
|---|---|---|
|
||||
| `heroSet.ts` | 新增 `calcHeroPower` 及调参常量 | 实现时写入 |
|
||||
| `SkillSet.ts` | 读取技能配置计算触发价值 | 无需改动 |
|
||||
| `BuffSet.ts` | 读取计时 buff 默认数值 | 无需改动 |
|
||||
| 关卡/经济系统 | 获得英雄强度量化输入 | 后续消费端按需调用 |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `calcHeroPower(HeroInfo[5011], 1)` 返回可复现的正数分数。
|
||||
- [ ] 同 `card_lv` 英雄中,功能更全面的角色分数普遍更高(如 5045 不灭战魂 > 5011 小铁卫)。
|
||||
- [ ] `targetLv` 从 1 提升到 6 时,分数随 `HERO_LV_MULTIPLIER` 与 `evolve` 加成单调上升。
|
||||
- [ ] 对怪物调用时不报错,且分数趋势与基础 HP/AP 一致。
|
||||
- [ ] 不改变现有 `heroInfo`、`HeroList` 任何字段。
|
||||
- [ ] 无回归:现有战斗逻辑、UI 展示不依赖新增函数。
|
||||
|
||||
## GDD Update Required?
|
||||
|
||||
No。项目暂无 `design/gdd/` 文档,本 Quick Design Spec 作为英雄强度锚定的临时设计依据。后续若建立系统 GDD,需将本公式合并进 Formulas 与 Tuning Knobs 章节。
|
||||
Reference in New Issue
Block a user