diff --git a/assets/script/game/common/config/英雄设计表.csv b/assets/script/game/common/config/英雄设计表.csv index 64ff2965..87ce6837 100644 --- a/assets/script/game/common/config/英雄设计表.csv +++ b/assets/script/game/common/config/英雄设计表.csv @@ -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,无 diff --git a/put/server/data/heroes.json b/put/server/data/heroes.json index 40d1f908..60fd7e2e 100644 --- a/put/server/data/heroes.json +++ b/put/server/data/heroes.json @@ -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}秒)" } diff --git a/put/server/index.js b/put/server/index.js index eb7e7684..2f9100d4 100644 --- a/put/server/index.js +++ b/put/server/index.js @@ -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 = {${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 diff --git a/put/web/src/App.vue b/put/web/src/App.vue index 9335863b..4c616ae7 100644 --- a/put/web/src/App.vue +++ b/put/web/src/App.vue @@ -7,6 +7,7 @@ 保存到服务器 导出 hexport.ts 写入 heroSet.ts + 导出英雄设计表.csv
@@ -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() diff --git a/put/web/src/api.js b/put/web/src/api.js index 46561dbb..1633ba72 100644 --- a/put/web/src/api.js +++ b/put/web/src/api.js @@ -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), } diff --git a/put/web/src/components/OverrideEditor.vue b/put/web/src/components/OverrideEditor.vue index 92eaff42..78c5648f 100644 --- a/put/web/src/components/OverrideEditor.vue +++ b/put/web/src/components/OverrideEditor.vue @@ -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 }) } diff --git a/put/web/src/constants.js b/put/web/src/constants.js index 8295ad47..49850415 100644 --- a/put/web/src/constants.js +++ b/put/web/src/constants.js @@ -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' },