2 Commits

Author SHA1 Message Date
panFD
1806f4b1a5 feat(web+server): 新增枚举覆写支持与CSV导出功能
1. 新增buff_target枚举覆写配置,支持选择施法目标类型
2. 后端添加CSV导出接口,生成标准化英雄设计表
3. 前端新增CSV导出按钮,支持一键导出配置
4. 优化覆写字段渲染逻辑,支持枚举值转可读枚举名
5. 清理英雄数据中冗余的summon_count默认配置
2026-09-05 12:23:23 +08:00
panFD
071afc8fa2 feat(skill): 添加计时buff目标覆写功能
1. 新增BuffTarget枚举定义计时buff的四种施加目标类型
2. 扩展SkillOverrides接口添加buff_target配置项
3. 实现resolveBuffTargets方法解析buff实际施加目标
4. 调整普攻buff施加逻辑适配新的目标配置规则
5. 修复伤害技能误给敌方挂友方buff的问题
6. 清理英雄技能配置中多余的summon_count参数
2026-09-05 12:22:18 +08:00
11 changed files with 274 additions and 54 deletions

View File

@@ -20,6 +20,18 @@ export enum DTType {
range = 1,
}
/**
* 计时 buff(timed_buff_id)的施加目标。
* 缺省(undefined)时按技能主效果推导:伤害技能挂被命中者,辅助技能挂友方目标。
* 显式配置后打破 TGroup 默认绑定,实现"伤害技能命中后给自己回血"等跨阵营效果。
*/
export enum BuffTarget {
Hit = 0, // 被命中者(敌方技能的默认行为)
Self = 1, // 施法者自身
Ally = 2, // 全体友方(含施法者)
Enemy = 3, // 全体敌方(辅助技能给敌人挂 debuff 用)
}
export enum SkillKind {
Damage = 0,
Heal = 1,
@@ -178,6 +190,8 @@ export interface SkillOverrides {
buff_value?: number;
buff_duration?: number;
buff_chance?: number;
/** 计时 buff 施加目标覆写:缺省按技能主效果推导(伤害→被命中者,辅助→友方) */
buff_target?: BuffTarget;
call_hero?: number;
/** 单次施法召唤的技能实体个数(覆盖 prefab 上 SkillView.summon_count;>1 视为召唤类,不再参与风怒等额外触发) */
summon_count?: number;

View File

@@ -599,10 +599,10 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 2, s_lv: SkillLv.Lv2, overrides: {"stun":10}, desc: "挥斧对{target}造成攻击力{ap}%伤害,{stun}%概率击晕" },
],
},
6104: { uuid: 6104, name: "开山斧", lv: 1, cd: 8, ccd: 0, overrides: {"ap":300,"stun":100,"summon_count":1}, desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害,必定击晕",
6104: { uuid: 6104, name: "开山斧", lv: 1, cd: 8, ccd: 0, overrides: {"ap":300,"stun":100}, desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害,必定击晕",
lv_entries: [
{ lv: 8, s_lv: SkillLv.Lv2, overrides: {"ap":450,"stun":100,"summon_count":1} },
{ lv: 16, s_lv: SkillLv.Lv3, overrides: {"ap":450,"stun":100,"summon_count":2} },
{ lv: 8, s_lv: SkillLv.Lv2, overrides: {"ap":450,"stun":100} },
{ lv: 16, s_lv: SkillLv.Lv3, overrides: {"ap":450,"stun":100} },
],
},
},
@@ -620,10 +620,10 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 2, s_lv: SkillLv.Lv2, overrides: {"crt":10}, desc: "挥刀对{target}造成攻击力{ap}%伤害,{crt}%概率暴击" },
],
},
6100: { uuid: 6100, name: "光闪", lv: 1, cd: 8, ccd: 0, overrides: {"ap":150,"summon_count":1}, desc: "斩出光闪,对{target}造成攻击力{ap}%伤害",
6100: { uuid: 6100, name: "光闪", lv: 1, cd: 8, ccd: 0, overrides: {"ap":150}, desc: "斩出光闪,对{target}造成攻击力{ap}%伤害",
lv_entries: [
{ lv: 8, s_lv: SkillLv.Lv2, overrides: {"ap":250,"summon_count":1} },
{ lv: 16, s_lv: SkillLv.Lv3, overrides: {"ap":350,"summon_count":2} },
{ lv: 8, s_lv: SkillLv.Lv2, overrides: {"ap":250} },
{ lv: 16, s_lv: SkillLv.Lv3, overrides: {"ap":350} },
],
},
},
@@ -641,10 +641,10 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 2, s_lv: SkillLv.Lv2, overrides: {"timed_buff_id":7007,"buff_value":30,"buff_chance":30}, desc: "挥刀对{target}造成攻击力{ap}%伤害,{buff_chance}%概率使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)" },
],
},
6103: { uuid: 6103, name: "闪电斩", lv: 1, cd: 8, ccd: 0, overrides: {"ap":150,"summon_count":1}, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害",
6103: { uuid: 6103, name: "闪电斩", lv: 1, cd: 8, ccd: 0, overrides: {"ap":150}, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害",
lv_entries: [
{ lv: 8, s_lv: SkillLv.Lv2, overrides: {"ap":250,"timed_buff_id":7007,"summon_count":1}, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)" },
{ lv: 16, s_lv: SkillLv.Lv3, overrides: {"ap":350,"para":50,"timed_buff_id":7007,"buff_value":60,"summon_count":2}, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害,{para}%概率麻痹,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)" },
{ lv: 8, s_lv: SkillLv.Lv2, overrides: {"ap":250,"timed_buff_id":7007}, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)" },
{ lv: 16, s_lv: SkillLv.Lv3, overrides: {"ap":350,"para":50,"timed_buff_id":7007,"buff_value":60}, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害,{para}%概率麻痹,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)" },
],
},
},

View File

@@ -1,19 +1,19 @@
字段,5005,5007,5008,5002,5003,5010,5001,5006
英雄名称,叠甲骑士,易伤剑仙,眩晕暴击哥,刀刀暴击,出血贼,冻箭哥,击退火女,团疗奶爸
简化说明,自身护盾+全体护盾,击退压场+易伤放大;复刻酒仙,高爆发+击晕收割;复刻齐天大圣,暴击/暴伤双光环乘区;复刻剑圣,入场先手+受击格挡;复刻剑豪,多重单体箭,带冰冻,群体击退/麻痹压场;复刻深渊女王,全体治疗续航+自身护盾
rarity,1,1,1,1,1,1,1,1
类型,tank,warrior,warrior,assassin,assassin,archer,mage,support
形态,近战,近战,近战,中程,中程,远程,远程,远程
ap,tank+0,warrior+20,warrior+20,assassin+30,assassin+30,archer+30,mage+10,support+20
hp,tank+20,warrior+5,warrior+5,assassin+0,assassin+0,archer,mage-5,support+10
def,tank+10,warrior+5,warrior+5,assassin+0,assassin+0,archer,mage+0,support
skill[0]_lv1,6004 攻击(ap100;慢速1.20s),6004 攻击(ap100;慢速1.20s),6004 攻击(ap100;慢速1.20s),6004 攻击(ap100;快速;,6004 攻击(ap100;快速,6006 射击(ap100;快速,6010 紫魔球(ap100;慢速1.20s),6004 攻击(ap100;慢速1.20s)
skill[0]_lv2,ap120,ap120,stun10,crt10,附加30%出血7007buff_chance30,bck10,bck10,bck10
skill[1]_lv1,6301 魔法盾(ap:30;summon1):获得生命最大值30%的护盾,6101 气斩(ap150;summon1附加易伤7002),6104 开山斧(ap300;stun100 击晕),6100 光闪(ap150;,6103 闪电斩ap150,6150 万箭齐发冰(ap150%;frz30),6200 火焰喷射(ap250;bck100),6302 治疗(团队恢复ap300%
skill[1]_lv8,ap:60;summon1:获得生命最大值60%的护盾,ap150;summon1;附加易伤7002buff_value20,ap450;stun100 击晕;,ap250,ap250;附加出血7007,ap200%;frz50,6200 火焰喷射(ap350;bck100),团队回复ap400%
skill[1]_lv16,ap:120;summon1:获得生命最大值120%的护盾,ap180;bck60;summon2;易伤buff_value40,ap450;stun100 击晕;;summon2,ap350,ap350;para50;summon2;出血强化7007 buff_value2,ap250%;frz70,6200 火焰喷射(ap450;bck100),团队回复ap450%
atking[0]_lv4,无,6004 奋力一击(t_num3;ap300;慢速1.20s):每三次攻击后立即打击敌人,对敌人造成300%伤害,无,无,无,无,无,6301 魔法盾(ap:30;summon1):获得生命最大值30%的护盾
atked[0]_lv4,6301 魔法盾(ap:10;team,summon1):全队获得本英雄生命最大值10%的护盾,无,无,无,无,无,无,无
fstart[0]_lv4,无,无,无,无,无,无,无,无
dead[0]_lv4,无,无,无,无,无,无,无,无
field[0]_lv4,无,无,7005 暴伤光环0.25,7004 暴击光环0.12+7005 暴伤光环0.20,7018 攻击强化value1(连击光环),7006 攻速光环0.15,7002 攻击光环0.15,无
字段,5001,5002,5003,5004,5005,5006,5007,5008
英雄名称,叠甲骑士,易伤剑仙,眩晕暴击哥,刀刀暴击,出血贼,冻箭哥,击退火女,团疗奶爸
简化说明,CSV导入:自身护盾+全体护盾,CSV导入:击退压场+易伤放大;复刻酒仙,CSV导入:高爆发+击晕收割;复刻齐天大圣,CSV导入:暴击/暴伤双光环乘区;复刻剑圣,CSV导入:入场先手+受击格挡;复刻剑豪,CSV导入:多重单体箭,带冰冻,CSV导入:群体击退/麻痹压场;复刻深渊女王,CSV导入:全体治疗续航+自身护盾
rarity,1,1,1,1,1,1,1,1
类型,tank,warrior,warrior,assassin,assassin,archer,mage,support
形态,近战,近战,近战,中程,中程,远程,远程,远程
ap,tank,warrior+20,warrior+20,assassin+30,assassin+30,archer+30,mage+10,support+20
hp,tank+20,warrior+5,warrior+5,assassin,assassin,archer,mage-5,support+10
def,tank+10,warrior+5,warrior+5,assassin,assassin,archer,mage,support
skill[0]_lv1,6004 强化普攻(ap100;慢速1.20s),6004 强化普攻(ap100;慢速1.20s),6004 强化普攻(ap100;慢速1.20s),6002 强化普攻(ap100;快速0.90s),6002 强化普攻(ap100;快速0.90s),6006 强化普攻(ap100;中速1.05s),6010 强化普攻(ap100;慢速1.20s;bck30),6011 强化普攻(ap100;中速1.05s)
skill[0]_lv2,ap120,ap120,stun10,crt10,附加30出血7007buff_chance30,bck10,bck40,bck10
skill[1]_lv1,6301 魔法盾(ap30),6101 气斩(ap150;附加易伤7002),6104 开山斧(ap300;stun100),6100 光闪(ap150),6103 闪电斩(ap150),6150 万箭齐发·冰(ap150;frz30),6200 火焰喷射(ap250;bck100),6302 治疗(ap300)
skill[1]_lv8,ap60,,ap450;stun100,ap250,ap250;附加出血7007,ap200;frz50,ap350;bck100,ap400
skill[1]_lv16,ap120,ap180;bck60,ap450;stun100;summon2,ap350;summon2,ap350;para50;summon2;附加60出血7007,ap250;frz70,ap450;bck100;summon2,ap450;summon2
atking[0]_lv4,无,6004 风暴打击(t_num3;ap300):立刻打击{target},造成攻击力{ap}%伤害,无,无,无,无,无,6301 圣愈之盾(t_num3;ap30):{target}获得生命最大值{ap}%的护盾
atked[0]_lv4,6301 全体圣盾(t_num3;ap10):全队获得本英雄生命最大值{ap}%的护盾,无,无,无,无,无,无,无
fstart[0]_lv4,无,无,无,无,无,无,无,无
dead[0]_lv4,无,无,无,无,无,无,无,无
field[0]_lv4,无,无,7005 大圣神威0.25,7004 剑刃风暴0.12+7005 斩魂利刃0.2,7018 连击光环1,7006 攻速光环0.15,7002 深渊威压0.15,无
1 字段 5001 5002 5003 5004 5005 5010 5006 5007 5008
2 英雄名称 击退火女 叠甲骑士 刀刀暴击 易伤剑仙 出血贼 眩晕暴击哥 刀刀暴击 叠甲骑士 出血贼 冻箭哥 团疗奶爸 冻箭哥 易伤剑仙 击退火女 眩晕暴击哥 团疗奶爸
3 简化说明 群体击退/麻痹压场;复刻深渊女王 CSV导入:自身护盾+全体护盾 暴击/暴伤双光环乘区;复刻剑圣 CSV导入:击退压场+易伤放大;复刻酒仙 入场先手+受击格挡;复刻剑豪 CSV导入:高爆发+击晕收割;复刻齐天大圣 CSV导入:暴击/暴伤双光环乘区;复刻剑圣 自身护盾+全体护盾 CSV导入:入场先手+受击格挡;复刻剑豪 多重单体箭,带冰冻 全体治疗续航+自身护盾 CSV导入:多重单体箭,带冰冻 击退压场+易伤放大;复刻酒仙 CSV导入:群体击退/麻痹压场;复刻深渊女王 高爆发+击晕收割;复刻齐天大圣 CSV导入:全体治疗续航+自身护盾
4 rarity 1 1 1 1 1 1 1 1 1
5 类型 mage tank assassin warrior assassin warrior assassin tank assassin archer support archer warrior mage warrior support
6 形态 远程 近战 中程 近战 中程 近战 中程 近战 中程 远程 远程 近战 远程 近战 远程
7 ap mage+10 tank assassin+30 warrior+20 assassin+30 warrior+20 assassin+30 tank+0 assassin+30 archer+30 support+20 archer+30 warrior+20 mage+10 warrior+20 support+20
8 hp mage-5 tank+20 assassin+0 warrior+5 assassin+0 warrior+5 assassin tank+20 assassin archer support+10 archer warrior+5 mage-5 warrior+5 support+10
9 def mage+0 tank+10 assassin+0 warrior+5 assassin+0 warrior+5 assassin tank+10 assassin archer support archer warrior+5 mage warrior+5 support
10 skill[0]_lv1 6010 紫魔球(ap100;慢速1.20s) 6004 强化普攻(ap100;慢速1.20s) 6004 攻击(ap100;快速; 6004 强化普攻(ap100;慢速1.20s) 6004 攻击(ap100;快速 6004 强化普攻(ap100;慢速1.20s) 6002 强化普攻(ap100;快速0.90s) 6004 攻击(ap100;慢速1.20s) 6002 强化普攻(ap100;快速0.90s) 6006 射击(ap100;快速 6004 攻击(ap100;慢速1.20s) 6006 强化普攻(ap100;中速1.05s) 6004 攻击(ap100;慢速1.20s) 6010 强化普攻(ap100;慢速1.20s;bck30) 6004 攻击(ap100;慢速1.20s) 6011 强化普攻(ap100;中速1.05s)
11 skill[0]_lv2 bck10 ap120 crt10 ap120 附加30%出血7007buff_chance30 stun10 crt10 ap120 附加30出血7007buff_chance30 bck10 bck10 ap120 bck40 stun10 bck10
12 skill[1]_lv1 6200 火焰喷射(ap250;bck100) 6301 魔法盾(ap30) 6100 光闪(ap150; 6101 气斩(ap150;附加易伤7002) 6103 闪电斩ap150 6104 开山斧(ap300;stun100) 6100 光闪(ap150) 6301 魔法盾(ap:30;summon1):获得生命最大值30%的护盾 6103 闪电斩(ap150) 6150 万箭齐发冰(ap150%;frz30) 6302 治疗(团队恢复ap300% 6150 万箭齐发·冰(ap150;frz30) 6101 气斩(ap150;summon1附加易伤7002) 6200 火焰喷射(ap250;bck100) 6104 开山斧(ap300;stun100 击晕) 6302 治疗(ap300)
13 skill[1]_lv8 6200 火焰喷射(ap350;bck100) ap60 ap250 ap250;附加出血7007 ap450;stun100 ap250 ap:60;summon1:获得生命最大值60%的护盾 ap250;附加出血7007 ap200%;frz50 团队回复ap400% ap200;frz50 ap150;summon1;附加易伤7002buff_value20 ap350;bck100 ap450;stun100 击晕; ap400
14 skill[1]_lv16 6200 火焰喷射(ap450;bck100) ap120 ap350 ap180;bck60 ap350;para50;summon2;出血强化7007 buff_value2 ap450;stun100;summon2 ap350;summon2 ap:120;summon1:获得生命最大值120%的护盾 ap350;para50;summon2;附加60出血7007 ap250%;frz70 团队回复ap450% ap250;frz70 ap180;bck60;summon2;易伤buff_value40 ap450;bck100;summon2 ap450;stun100 击晕;;summon2 ap450;summon2
15 atking[0]_lv4 无 无 6004 风暴打击(t_num3;ap300):立刻打击{target},造成攻击力{ap}%伤害 无 无 无 无 6301 魔法盾(ap:30;summon1):获得生命最大值30%的护盾 无 6004 奋力一击(t_num3;ap300;慢速1.20s):每三次攻击后立即打击敌人,对敌人造成300%伤害 无 无 6301 圣愈之盾(t_num3;ap30):{target}获得生命最大值{ap}%的护盾
16 atked[0]_lv4 无 6301 全体圣盾(t_num3;ap10):全队获得本英雄生命最大值{ap}%的护盾 无 无 无 6301 魔法盾(ap:10;team,summon1):全队获得本英雄生命最大值10%的护盾 无 无 无 无 无
17 fstart[0]_lv4 无 无 无 无 无 无 无 无 无
18 dead[0]_lv4 无 无 无 无 无 无 无 无 无
19 field[0]_lv4 7002 攻击光环0.15 无 7004 暴击光环0.12+7005 暴伤光环0.20 无 7018 攻击强化value1(连击光环) 7005 大圣神威0.25 7004 剑刃风暴0.12+7005 斩魂利刃0.2 无 7018 连击光环1 7006 攻速光环0.15 无 7006 攻速光环0.15 无 7002 深渊威压0.15 7005 暴伤光环0.25 无

View File

@@ -11,7 +11,7 @@ import { HeroInfo } from "../common/config/heroSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { SkillTriggerType } from "../common/config/heroSet";
import { FieldSkillType } from "../common/config/SkillSet";
import { FieldSkillType, BuffTarget } from "../common/config/SkillSet";
import { mLogger } from "../common/Logger";
import { SkillTriggerHelper } from "./SkillTriggerHelper";
@@ -257,7 +257,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
}
}
// 命中附带 debuff(如灼烧 DoT):仅普攻路径,复用友方 timed_buff_id 机制;
// 命中附带 buff:仅普攻路径;buff_target 缺省挂被命中者,显式配置可改为挂施法者/全体友方/全体敌方
// 施法者 ap/完整属性快照取自伤害事件,供 BuffSystem tick 缩放与暴击/冰冻/击晕判定
if (!isDot && skillConf.timed_buff_id) {
const buffChance = skillConf.buff_chance ?? 100;
@@ -265,7 +265,9 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
const buffOverrides: BuffApplyOverrides = { source_attrs: damageEvent.Attrs };
if (skillConf.buff_value !== undefined) buffOverrides.value = skillConf.buff_value;
if (skillConf.buff_duration !== undefined) buffOverrides.duration = skillConf.buff_duration;
buffManager.applyBuff(target, skillConf.timed_buff_id, casterEid, damageEvent.Attrs[Attrs.ap] || 0, buffOverrides);
for (const buffTarget of this.resolveBuffTargets(target, casterEid, TAttrsComp.fac, skillConf.buff_target)) {
buffManager.applyBuff(buffTarget, skillConf.timed_buff_id, casterEid, damageEvent.Attrs[Attrs.ap] || 0, buffOverrides);
}
}
}
@@ -527,6 +529,38 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
return { remainingDamage: damage - absorbed, absorbedDamage: absorbed };
}
/**
* 解析 timed_buff_id 的施加目标实体列表。
* 缺省(undefined / BuffTarget.Hit)返回被命中者;显式配置可切换到施法者自身、全体友方或全体敌方。
* @param hitTarget 被命中实体(伤害事件的 target)
* @param casterEid 施法者 eid
* @param targetFac 被命中者阵营(用于推导敌我网格)
* @param buffTarget 技能配置上的目标覆写
*/
private resolveBuffTargets(hitTarget: ecs.Entity, casterEid: number, targetFac: number, buffTarget?: BuffTarget): ecs.Entity[] {
if (buffTarget === undefined || buffTarget === BuffTarget.Hit) {
return [hitTarget];
}
if (buffTarget === BuffTarget.Self) {
const caster = ecs.getEntityByEid(casterEid);
return caster ? [caster] : [];
}
// 全体友方/敌方:以施法者阵营为基准(命中者必为敌方,施法方网格即友方)
const casterFac = targetFac === FacSet.HERO ? FacSet.MON : FacSet.HERO;
const grid = buffTarget === BuffTarget.Ally
? (casterFac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid)
: (casterFac === FacSet.HERO ? smc.mission.monGrid : smc.mission.heroGrid);
const targets: ecs.Entity[] = [];
for (const eid of grid) {
if (eid < 0) continue;
const ent = ecs.getEntityByEid(eid);
if (!ent) continue;
const attrs = ent.get(HeroAttrsComp);
if (attrs && !attrs.is_dead && !attrs.is_reviving) targets.push(ent);
}
return targets;
}
/**
* 被攻击时触发的事件
*

View File

@@ -2,7 +2,7 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
import { Vec3, Prefab, instantiate, tween, Node } from "cc";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { DTType, RType, SkillConfig, SkillKind, SkillSet, TGroup, SkillOverrides, mergeSkillParams } from "../common/config/SkillSet";
import { DTType, RType, SkillConfig, SkillKind, SkillSet, TGroup, SkillOverrides, mergeSkillParams, BuffTarget } from "../common/config/SkillSet";
import { Skill } from "../skill/Skill";
import { SkillView } from "../skill/SkillView";
import { smc } from "../common/SingletonModuleComp";
@@ -545,7 +545,8 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
if (config.buff_type !== undefined) {
// 计时性 buff:配置了 timed_buff_id 时走 buffManager 限时路径,
// 同一技能不应既永久加成又限时加成,故提前 return 跳过下方永久 add_* 逻辑
if (config.timed_buff_id) {
// buff_target=Enemy 时 timed_buff_id 已在 applyFriendlySkillEffects 入口分流到敌方网格,此处跳过避免误挂友方
if (config.timed_buff_id && config.buff_target !== BuffTarget.Enemy) {
// 汇总 buff 覆写参数:value 覆写修饰数值,duration 覆写持续时间
const buffOverrides: { value?: number; duration?: number } = {};
if (config.buff_value !== undefined) buffOverrides.value = config.buff_value;

View File

@@ -231,8 +231,7 @@
"cdExpr": null,
"overrides": {
"ap": 300,
"stun": 100,
"summon_count": 1
"stun": 100
},
"desc": "劈出开山斧,对{target}造成攻击力{ap}%伤害,必定击晕",
"lv_entries": [
@@ -241,8 +240,7 @@
"s_lv": 2,
"overrides": {
"ap": 450,
"stun": 100,
"summon_count": 1
"stun": 100
},
"desc": ""
},
@@ -251,8 +249,7 @@
"s_lv": 3,
"overrides": {
"ap": 450,
"stun": 100,
"summon_count": 2
"stun": 100
},
"desc": ""
}
@@ -325,8 +322,7 @@
"cd": 8,
"cdExpr": null,
"overrides": {
"ap": 150,
"summon_count": 1
"ap": 150
},
"desc": "斩出光闪,对{target}造成攻击力{ap}%伤害",
"lv_entries": [
@@ -334,8 +330,7 @@
"lv": 8,
"s_lv": 2,
"overrides": {
"ap": 250,
"summon_count": 1
"ap": 250
},
"desc": ""
},
@@ -343,8 +338,7 @@
"lv": 16,
"s_lv": 3,
"overrides": {
"ap": 350,
"summon_count": 2
"ap": 350
},
"desc": ""
}
@@ -424,8 +418,7 @@
"cd": 8,
"cdExpr": null,
"overrides": {
"ap": 150,
"summon_count": 1
"ap": 150
},
"desc": "召唤落雷斩击{target},造成攻击力{ap}%伤害",
"lv_entries": [
@@ -434,8 +427,7 @@
"s_lv": 2,
"overrides": {
"ap": 250,
"timed_buff_id": 7007,
"summon_count": 1
"timed_buff_id": 7007
},
"desc": "召唤落雷斩击{target},造成攻击力{ap}%伤害,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)"
},
@@ -446,8 +438,7 @@
"ap": 350,
"para": 50,
"timed_buff_id": 7007,
"buff_value": 60,
"summon_count": 2
"buff_value": 60
},
"desc": "召唤落雷斩击{target},造成攻击力{ap}%伤害,{para}%概率麻痹,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)"
}

View File

@@ -10,6 +10,7 @@ const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
const CONFIG_DIR = path.join(PROJECT_ROOT, 'assets/script/game/common/config')
const EXPORT_PATH = path.join(CONFIG_DIR, 'hexport.ts')
const HEROSET_PATH = path.join(CONFIG_DIR, 'heroSet.ts')
const CSV_PATH = path.join(CONFIG_DIR, '英雄设计表.csv')
// heroSet.ts 中英雄条目区域标记(怪物配置在 end 标记之后,不受替换影响)
const HEROES_BEGIN = '// @hero-setup:heroes-begin'
const HEROES_END = '// @hero-setup:heroes-end'
@@ -104,9 +105,13 @@ app.post('/api/heroes', (req, res) => {
}
})
/** 序列化 overrides:TGroup 以字符串存储(如 "Team"),导出时还原为枚举表达式 TGroup.Team */
/** 序列化 overrides:TGroup 以字符串存储(如 "Team"),导出时还原为枚举表达式 TGroup.Team;
* buff_target 以数值存储(0-3),导出时还原为 BuffTarget.X 枚举表达式提升可读性 */
const BUFF_TARGET_NAMES = ['Hit', 'Self', 'Ally', 'Enemy']
function serializeOverrides(o) {
return JSON.stringify(o).replace(/"TGroup":\s*"(\w+)"/g, '"TGroup": TGroup.$1')
let s = JSON.stringify(o).replace(/"TGroup":\s*"(\w+)"/g, '"TGroup": TGroup.$1')
s = s.replace(/"buff_target":\s*([0-3])\b/g, (_, n) => `"buff_target": BuffTarget.${BUFF_TARGET_NAMES[+n]}`)
return s
}
/** 生成 hexport.ts 内容 */
@@ -120,7 +125,7 @@ function renderHeroTs(heroes) {
out += ` */${nl}`
out += `import { v3 } from "cc"${nl}`
out += `import { FacSet } from "./GameSet"${nl}`
out += `import { TGroup } from "./SkillSet"${nl}`
out += `import { TGroup, BuffTarget } from "./SkillSet"${nl}`
out += `import { SkillLv, HType, HGrowType, HRole, HERO_ROLE_BASE, AtkSpeedSet, AtkSpeedLv, heroInfo } from "./heroSet"${nl}`
out += nl
out += `export const HexportHeroInfo: Record<number, heroInfo> = {${nl}`
@@ -254,6 +259,150 @@ app.get('/api/export', (_req, res) => {
* 直接写回 heroSet.ts:用编辑器数据替换英雄条目区域(HEROES_BEGIN/END 标记之间),
* 怪物条目与区域外的其它配置保持不变。
*/
const ROLE_CSV_KEY = { Tank: 'tank', Warrior: 'warrior', Assassin: 'assassin', Archer: 'archer', Mage: 'mage', Support: 'support' }
const TYPE_CSV_LABEL = { Melee: '近战', Mid: '中程', Long: '远程' }
// 攻速 cd 值 → CSV 档位描述
const CD_SPEED_LABELS = [[0.75, '极速0.75s'], [0.9, '快速0.90s'], [1.05, '中速1.05s'], [1.2, '慢速1.20s'], [1.5, '极慢1.50s']]
function cdSpeedLabel(cd) {
const hit = CD_SPEED_LABELS.find(([v]) => Math.abs(v - cd) < 0.001)
return hit ? hit[1] : `攻速${cd}s`
}
// 字段值转 CSV 单元格(含逗号/换行时加引号)
function csvCell(v) {
const s = String(v ?? '')
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
}
/** 覆写值转 CSV 简写:ap120 / stun10 / crt10 / bck10 / frz30 / para50 / summon2 / 附加易伤7002 */
function overrideShort(ov, { includeAp = true } = {}) {
if (!ov) return ''
const parts = []
if (includeAp && ov.ap != null) parts.push(`ap${ov.ap}`)
if (ov.crt != null) parts.push(`crt${ov.crt}`)
if (ov.stun != null) parts.push(`stun${ov.stun}`)
if (ov.para != null) parts.push(`para${ov.para}`)
if (ov.frz != null) parts.push(`frz${ov.frz}`)
if (ov.bck != null) parts.push(`bck${ov.bck}`)
if (ov.summon_count != null && ov.summon_count > 1) parts.push(`summon${ov.summon_count}`)
if (ov.timed_buff_id != null) {
const buffName = cache?.BuffList?.[ov.timed_buff_id]?.name || ''
const val = ov.buff_value != null ? `${ov.buff_value}` : ''
const chance = ov.buff_chance != null ? `buff_chance${ov.buff_chance}` : ''
// buff_target 非默认(被命中者)时标注目标,便于识别"挂自己/挂敌方"等跨阵营 buff
const tgtLabel = ov.buff_target != null && ov.buff_target !== 0 ? `→${BUFF_TARGET_NAMES[ov.buff_target]}` : ''
parts.push(`附加${val}${buffName}${ov.timed_buff_id}${chance}${tgtLabel}`)
}
return parts.join(';')
}
/** 技能 lv1 描述行:6004 攻击(ap100;慢速1.20s) */
function csvSkillLv1(sk, isUlt) {
if (!sk) return ''
const base = cache?.SkillSet?.[sk.uuid]
const name = sk.name || base?.name || ''
const ov = sk.overrides || {}
const parts = []
if (ov.ap != null) parts.push(`ap${ov.ap}`)
if (!isUlt) parts.push(cdSpeedLabel(sk.cd ?? 1.05))
const extra = overrideShort(ov, { includeAp: false })
if (extra) parts.push(extra)
return `${sk.uuid} ${name}(${parts.join(';')})`
}
/** 技能升档描述行:ap120 / stun10 / bck10 / 附加30%出血7007buff_chance30 */
function csvSkillLvUp(sk, lv) {
if (!sk) return ''
const entry = (sk.lv_entries || []).find((e) => e.lv === lv)
if (!entry) return ''
return overrideShort(entry.overrides || {})
}
/** 触发技能描述行:6004 奋力一击(t_num3;ap300;慢速1.20s):每三次攻击后立即打击敌人,对敌人造成300%伤害 */
function csvTrigger(grp) {
if (!grp) return '无'
for (const [sid, entries] of Object.entries(grp)) {
const e = entries?.[0]
if (!e) continue
const base = cache?.SkillSet?.[sid]
const name = e.name || base?.name || ''
const ov = e.overrides || {}
const parts = [`t_num${e.t_num}`]
if (ov.ap != null) parts.push(`ap${ov.ap}`)
const desc = e.desc || ''
return `${sid} ${name}(${parts.join(';')}):${desc}`
}
return '无'
}
/** 光环描述行:7005 暴伤光环0.25 */
function csvField(field) {
if (!field?.length) return '无'
const f = field[0]
const parts = f.uuids.map((u) => {
const base = cache?.FieldSkillSet?.[u.uuid]
const name = u.name || base?.name || ''
return `${u.uuid} ${name}${u.value}`
})
return parts.join('+')
}
/** 英雄基础三维行:tank+20 / warrior+20 / archer */
function csvStatLine(h, key) {
const role = ROLE_CSV_KEY[h.roleKey] || h.roleKey || ''
const base = h.statBase?.[key] ?? 0
if (base === 0) return role
return `${role}${base > 0 ? '+' : ''}${base}`
}
/** 生成英雄设计表 CSV 内容 */
function renderHeroCsv(heroes) {
const sorted = [...heroes].sort((a, b) => a.uuid - b.uuid)
const rows = []
rows.push(['字段', ...sorted.map((h) => h.uuid)])
rows.push(['英雄名称', ...sorted.map((h) => h.name || '')])
rows.push(['简化说明', ...sorted.map((h) => h.comment || '')])
rows.push(['rarity', ...sorted.map((h) => h.rarity ?? 1)])
rows.push(['类型', ...sorted.map((h) => ROLE_CSV_KEY[h.roleKey] || h.roleKey || '')])
rows.push(['形态', ...sorted.map((h) => TYPE_CSV_LABEL[h.typeKey] || h.typeKey || '')])
rows.push(['ap', ...sorted.map((h) => csvStatLine(h, 'ap'))])
rows.push(['hp', ...sorted.map((h) => csvStatLine(h, 'hp'))])
rows.push(['def', ...sorted.map((h) => csvStatLine(h, 'def'))])
rows.push(['skill[0]_lv1', ...sorted.map((h) => csvSkillLv1(h.skills?.[0], false))])
rows.push(['skill[0]_lv2', ...sorted.map((h) => csvSkillLvUp(h.skills?.[0], 2))])
rows.push(['skill[1]_lv1', ...sorted.map((h) => csvSkillLv1(h.skills?.[1], true))])
rows.push(['skill[1]_lv8', ...sorted.map((h) => csvSkillLvUp(h.skills?.[1], 8))])
rows.push(['skill[1]_lv16', ...sorted.map((h) => csvSkillLvUp(h.skills?.[1], 16))])
rows.push(['atking[0]_lv4', ...sorted.map((h) => csvTrigger(h.atking))])
rows.push(['atked[0]_lv4', ...sorted.map((h) => csvTrigger(h.atked))])
rows.push(['fstart[0]_lv4', ...sorted.map((h) => csvTrigger(h.fstart))])
rows.push(['dead[0]_lv4', ...sorted.map((h) => csvTrigger(h.dead))])
rows.push(['field[0]_lv4', ...sorted.map((h) => csvField(h.field))])
return rows.map((r) => r.map(csvCell).join(',')).join('\n') + '\n'
}
app.post('/api/export-csv', async (req, res) => {
try {
const heroes = req.body?.heroes
if (!Array.isArray(heroes) || !heroes.length) {
return res.status(400).json({ error: 'heroes 不能为空' })
}
if (!cache) await buildCache()
const content = renderHeroCsv(heroes)
fs.writeFileSync(CSV_PATH, content, 'utf8')
res.json({ ok: true, path: CSV_PATH, count: heroes.length, bytes: Buffer.byteLength(content) })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
app.get('/api/export-csv', (_req, res) => {
if (!fs.existsSync(CSV_PATH)) return res.status(404).json({ error: '尚未生成' })
res.setHeader('Content-Type', 'text/csv; charset=utf-8')
res.send(fs.readFileSync(CSV_PATH, 'utf8'))
})
app.post('/api/apply', (req, res) => {
try {
const heroes = req.body?.heroes

View File

@@ -7,6 +7,7 @@
<el-button size="small" type="success" @click="doSave" :disabled="!heroes.length">保存到服务器</el-button>
<el-button size="small" type="warning" @click="doExport" :disabled="!heroes.length">导出 hexport.ts</el-button>
<el-button size="small" type="primary" @click="doApply" :disabled="!heroes.length">写入 heroSet.ts</el-button>
<el-button size="small" type="danger" @click="doExportCsv" :disabled="!heroes.length">导出英雄设计表.csv</el-button>
</div>
</header>
<div class="hero-main">
@@ -406,6 +407,22 @@ async function doApply() {
}
}
async function doExportCsv() {
try {
await ElMessageBox.confirm(
`将用 ${heroes.value.length} 个英雄覆盖写入 assets/script/game/common/config/英雄设计表.csv`,
'导出确认',
{ confirmButtonText: '覆盖写入', cancelButtonText: '取消', type: 'warning' }
)
} catch { return }
try {
const r = await api.exportCsv(heroes.value)
ElMessage.success(`已写入 ${r.count} 个英雄 → ${r.path}`)
} catch (e) {
ElMessage.error('导出失败: ' + (e?.response?.data?.error || e.message))
}
}
onMounted(() => {
loadMeta()
loadHeroes()

View File

@@ -7,5 +7,6 @@ export const api = {
saveHeroes: (heroes) => axios.post('/api/heroes', { heroes }).then((r) => r.data),
exportHeroes: (heroes) => axios.post('/api/export', { heroes }).then((r) => r.data),
applyHeroes: (heroes) => axios.post('/api/apply', { heroes }).then((r) => r.data),
exportCsv: (heroes) => axios.post('/api/export-csv', { heroes }).then((r) => r.data),
fetchExport: () => axios.get('/api/export', { responseType: 'text' }).then((r) => r.data),
}

View File

@@ -61,6 +61,12 @@ function isStringKey(key) {
return OVERRIDE_KEYS.find((k) => k.key === key)?.type === 'string'
}
// 该覆写字段为枚举型时返回候选列表(如 buff_target),否则返回 null 走数值输入
function enumOptions(key) {
const def = OVERRIDE_KEYS.find((k) => k.key === key)
return def?.type === 'enum' ? def.options : null
}
function add() {
rows.value.push({ key: '', value: 0 })
}

View File

@@ -112,6 +112,13 @@ export const OVERRIDE_KEYS = [
{ key: 'buff_value', label: 'buff_value 修饰数值' },
{ key: 'buff_duration', label: 'buff_duration 持续时间' },
{ key: 'buff_chance', label: 'buff_chance 附加概率' },
// 枚举型覆写:计时 buff 施加目标(缺省跟随技能主效果目标,见 SkillSet.BuffTarget)
{ key: 'buff_target', label: 'buff_target buff施加目标', type: 'enum', options: [
{ value: 0, label: '0 被命中者(Hit/默认)' },
{ value: 1, label: '1 施法者自身(Self)' },
{ value: 2, label: '2 全体友方(Ally)' },
{ value: 3, label: '3 全体敌方(Enemy)' },
]},
{ key: 'summon_count', label: 'summon_count 召唤数' },
{ key: 'num', label: 'num 额外释放次数' },
{ key: 'call_hero', label: 'call_hero 召唤英雄id' },