7 Commits

Author SHA1 Message Date
panFD
53fa0d7dc0 feat(ui): 新增通用确认弹窗并重构英雄出售逻辑
1. 新增CommonPrompt通用确认弹窗组件,支持富文本内容、自定义按钮与弹窗动画
2. 在UIID配置中新增Window弹窗枚举与对应UI配置
3. 重构HeroBoxComp的出售逻辑,添加二次确认防止误触出售英雄
2026-08-01 23:05:54 +08:00
panFD
11ae66fe1d feat(hero config): add hero power score calculation system
新增英雄强度评分系统,实现了基于属性、技能、buff的量化战力计算,支持归一化相对评分。同时添加了npx tsc命令到允许的bash操作列表中。
2026-08-01 21:38:01 +08:00
panFD
ad38e9904e feat(map,hero): add hero sell function to hero box
1. 新增出售按钮与价格标签UI到HeroBoxComp
2. 实现英雄出售逻辑:移除实体、结算金币并派发事件
3. 统一使用MissionEconomy管理出售金币计算
4. 调整窗口prefab布局适配新UI元素
2026-08-01 21:36:02 +08:00
panFD
4532cb5c61 refactor(herobox): 重构英雄框更多按钮交互逻辑
1. 将点击切换改为长按显示、松手隐藏的交互方式
2. 移除冗余的属性栏高度计算和状态按钮切换逻辑
3. 简化次级属性栏的显示隐藏实现
4. 更新相关注释和变量命名
2026-08-01 20:35:44 +08:00
panFD
096cae527b refactor(heroBox,skillDesc): 优化英雄面板技能描述显示
1. 新增showName参数控制技能描述是否显示技能名
2. 英雄盒面板隐藏技能名仅保留效果描述
3. 新增英雄强度评分公式设计文档
2026-08-01 19:55:14 +08:00
panFD
74860c6bf9 refactor(hero/config): 优化英雄技能描述格式与内容
fix(game/line): 调整游戏地平线Y轴偏移量
style(gui&map): 修正UI和地图的位置偏移参数

统一替换英雄技能描述为带颜色标记的简洁格式,移除冗余的等级描述数组;调整游戏地面线高度、角色控制器和地图物件的位置参数,适配新的布局需求。
2026-08-01 16:53:55 +08:00
panFD
175b93c7fa refactor(hero): 优化英雄描述文本展示逻辑与UI布局
1.  重构英雄组件富文本生成逻辑,直接使用配置表手写文案替代动态生成
2.  更新英雄配置表中的技能描述文本,优化表述更贴合实际效果
3.  调整任务面板和英雄预制体的UI位置与尺寸,适配新的展示内容
4.  移除冗余的空引用属性,新增刷新石数量显示节点
2026-07-31 23:16:59 +08:00
16 changed files with 12874 additions and 12946 deletions

View File

@@ -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

View File

@@ -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": {

View File

@@ -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

View File

@@ -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
}
},

View File

@@ -0,0 +1,226 @@
/**
* @file CommonPrompt.ts
* @description 自定义公共确认窗口组件UI 视图层)
*
* 职责:
* 1. 替换框架插件 CommonPromptextensions/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 的绑定约定:
* - 根节点按钮事件 handleronTouchEndbtn_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);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8c571795-0ff9-4765-ad7e-1de7f1cdcb3c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -13,7 +13,7 @@ export enum BoxSet {
LETF_END = -360,
RIGHT_END = 360,
//游戏地平线
GAME_LINE = 80,
GAME_LINE = 160,
/** 技能/装备各自独立的上限 */
BOX_MAX = 6,
}

View File

@@ -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" },

View File

@@ -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----
@@ -322,11 +325,12 @@ export function buildSkillDesc(source: ISkillDescSource): string {
/**
* 富文本描述RichText 用BBCode技能名用 <color=#2E86C1> 蓝色、数值用 <color=#27AE60> 亮绿色高亮。
* @param source 触发技能数据源(同 buildSkillDesc
* @returns 多行富文本串,技能名片段包裹 <color=#2E86C1>、数值片段包裹 <color=#27AE60>,用 \n 分隔
* @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

View File

@@ -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→66级起每受击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,
@@ -259,7 +253,7 @@ export const HeroInfo: Record<number, heroInfo> = {
2: { atked: [{ s_uuid: 6302, t_num: 3, overrides: { TGroup: TGroup.Self, ap: 300 } }] },
3: { hp_bonus: 150 },
4: { atked: [{ s_uuid: 6302, t_num: 3, overrides: { TGroup: TGroup.Self, ap: 350 } }] },
5: { atked: [{ s_uuid: 6302, t_num: 2, overrides: { TGroup: TGroup.Self, ap: 350 } }] },
5: { atked: [{ s_uuid: 6302, t_num: 2, overrides: { TGroup: TGroup.Self, ap: 350 } }] },
6: {
atked: [
{ s_uuid: 6302, t_num: 2, overrides: { TGroup: TGroup.Self, ap: 350 } },
@@ -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→125级起每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秒且攻击+155级暴击触发阈值降至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→125级起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/10s6级起全队暴击率+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;
}

View File

@@ -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;
}

View File

@@ -12,15 +12,18 @@
* - HeroInfoheroSet—— 英雄静态配置
*/
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;
}
}
// 展开/收起时切换属性栏高度(默认 125 ↔ 210可在编辑器自定义
/** 更多按钮放开/取消:隐藏次级属性栏 */
private onMoreBtnTouchEnd() {
this.setAttrBoxVisible(false);
}
/** 切换次级属性栏容器可见性 */
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;

View 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.52.0 | 每点有效 HP 的强度价值 |
| `W_DPS` | 30.0 | 10100 | 每秒有效伤害的强度价值 |
| `W_atking` | 1.0 | 0.52.0 | 攻击触发技能频率折算 |
| `W_atked` | 0.8 | 0.31.5 | 受击触发技能频率折算(低于 atking因依赖敌方攻击频率 |
| `W_fstart` | 1.0 | 0.52.0 | 每波开始一次性技能价值 |
| `W_fend` | 1.2 | 0.52.0 | 每波结束一次性技能价值 |
| `W_dead` | 0.6 | 0.21.0 | 死亡一次性技能价值 |
| `W_field_alive` | 1.0 | 0.52.0 | 驻场光环存活期间价值 |
| `W_revive` | 1.0 | 0.52.0 | 每次复活的价值系数 |
| `W_heal` | 0.8 | 0.51.2 | 治疗相对直接伤害的折价 |
| `W_shield` | 1.0 | 0.52.0 | 护盾相对 HP 的折价/溢价 |
| `W_control` | 0.5 | 0.11.0 | 控制效果(击晕等)的强度折算 |
| `expectedEnemyDmg` | 20 | 10100 | 估算护盾价值时假设的敌方单次伤害 |
### 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 章节。