From d3e7438bca8dbc5fd41d12b4f995e95a1297e41a Mon Sep 17 00:00:00 2001 From: panFD Date: Sun, 13 Sep 2026 11:43:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E7=90=86=E4=B8=8E=E4=B8=80=E9=94=AE=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E8=8B=B1=E9=9B=84=E5=90=91=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增服务端模板持久化接口,支持触发/光环预设的读写 2. 前端新增预设库管理页面,支持增删改查触发/光环预设 3. 新增英雄创建向导,支持按职业快速生成英雄并套用预设 4. 调整本地开发端口与代理地址 5. 补充职业默认配置与攻速映射等常量定义 --- put/server/data/templates.json | 132 +++++++ put/server/index.js | 41 ++ put/web/src/App.vue | 60 ++- put/web/src/api.js | 2 + put/web/src/components/HeroWizard.vue | 280 ++++++++++++++ put/web/src/components/TemplateManager.vue | 411 +++++++++++++++++++++ put/web/src/constants.js | 51 +++ put/web/vite.config.js | 5 +- 8 files changed, 973 insertions(+), 9 deletions(-) create mode 100644 put/server/data/templates.json create mode 100644 put/web/src/components/HeroWizard.vue create mode 100644 put/web/src/components/TemplateManager.vue diff --git a/put/server/data/templates.json b/put/server/data/templates.json new file mode 100644 index 00000000..4bc3acd9 --- /dev/null +++ b/put/server/data/templates.json @@ -0,0 +1,132 @@ +{ + "triggerPresets": [ + { + "id": "tp-atking-fenli", + "label": "每3次普攻·奋力一击", + "group": "atking", + "uuid": 6004, + "tags": ["伤害"], + "entry": { "lv": 4, "s_lv": 1, "name": "风暴打击", "t_num": 3, "overrides": { "ap": 300 }, "desc": "立刻打击{target},造成攻击力{ap}%伤害" } + }, + { + "id": "tp-atking-poison", + "label": "每3次普攻·蚀骨毒箭", + "group": "atking", + "uuid": 6007, + "tags": ["伤害", "持续"], + "entry": { "lv": 4, "s_lv": 1, "name": "蚀骨毒箭", "t_num": 3, "overrides": { "ap": 80, "timed_debuff_id": 7005, "debuff_value": 40, "debuff_chance": 100 }, "desc": "追加射出蚀骨毒箭,对{target}造成攻击力{ap}%伤害,必定使目标中毒" } + }, + { + "id": "tp-atking-heavy-arrow", + "label": "每3次普攻·追加重箭", + "group": "atking", + "uuid": 6008, + "tags": ["伤害", "击退"], + "entry": { "lv": 4, "s_lv": 1, "name": "", "t_num": 3, "overrides": { "ap": 120, "bck_chance": 50 }, "desc": "" } + }, + { + "id": "tp-atking-shield", + "label": "每3次普攻·附加护盾", + "group": "atking", + "uuid": 6301, + "tags": ["防御", "团队"], + "entry": { "lv": 4, "s_lv": 1, "name": "圣愈之盾", "t_num": 3, "overrides": { "ap": 30 }, "desc": "{target}获得生命最大值{ap}%的护盾" } + }, + { + "id": "tp-atked-shield-team", + "label": "受击3次·全队护盾", + "group": "atked", + "uuid": 6301, + "tags": ["防御", "团队"], + "entry": { "lv": 4, "s_lv": 1, "name": "全体圣盾", "t_num": 3, "overrides": { "ap": 10 }, "desc": "全队获得本英雄生命最大值{ap}%的护盾" } + }, + { + "id": "tp-atked-heal-self", + "label": "受击3次·自我治疗", + "group": "atked", + "uuid": 6302, + "tags": ["治疗"], + "entry": { "lv": 4, "s_lv": 1, "name": "", "t_num": 3, "overrides": { "ap": 5 }, "desc": "为自己释放治疗,恢复生命值的{ap}%" } + }, + { + "id": "tp-atked-thorns", + "label": "受击3次·荆棘反伤", + "group": "atked", + "uuid": 6516, + "tags": ["反伤", "防御"], + "entry": { "lv": 4, "s_lv": 1, "name": "荆棘回应", "t_num": 3, "overrides": { "timed_buff_id": 7028, "buff_value": 10 }, "desc": "凝聚荆棘护体,受击时反弹{buff_value}%所受伤害" } + }, + { + "id": "tp-fstart-crit-burst", + "label": "开场·团队暴击", + "group": "fstart", + "uuid": 6504, + "tags": ["增益", "团队", "暴击"], + "entry": { "lv": 4, "s_lv": 1, "name": "猎杀时刻", "t_num": 1, "overrides": { "buff_value": 30 }, "desc": "战斗开始时全体友方暴击率提升{buff_value}%" } + }, + { + "id": "tp-fstart-armor-break", + "label": "开场·先手破甲", + "group": "fstart", + "uuid": 6012, + "tags": ["减益", "破甲"], + "entry": { "lv": 4, "s_lv": 1, "name": "先手蚀甲", "t_num": 1, "overrides": { "ap": 150, "timed_debuff_id": 7008 }, "desc": "战斗开始掷出蚀甲酸瓶,必定使其破甲" } + }, + { + "id": "tp-fstart-lightning", + "label": "开场·雷击先手", + "group": "fstart", + "uuid": 6203, + "tags": ["伤害", "麻痹"], + "entry": { "lv": 4, "s_lv": 1, "name": "雷击先手", "t_num": 1, "overrides": { "ap": 150, "para": 30 }, "desc": "战斗开始释放雷击,对{target}造成{ap}%伤害,{para}%概率麻痹" } + } + ], + "fieldPresets": [ + { + "id": "fp-crit-master", + "label": "暴击双光环(暴击率+暴伤)", + "tags": ["暴击"], + "lv": 4, + "uuids": [ + { "uuid": 7004, "value": 0.12, "name": "剑刃风暴" }, + { "uuid": 7005, "value": 0.2, "name": "斩魂利刃" } + ] + }, + { + "id": "fp-atk-aura", + "label": "攻击光环 15%", + "tags": ["增伤"], + "lv": 4, + "uuids": [ + { "uuid": 7002, "value": 0.15, "name": "" } + ] + }, + { + "id": "fp-atk-speed", + "label": "攻速光环 15%", + "tags": ["攻速"], + "lv": 4, + "uuids": [ + { "uuid": 7006, "value": 0.15, "name": "" } + ] + }, + { + "id": "fp-armor-pierce", + "label": "穿透光环 10%", + "tags": ["破甲"], + "lv": 4, + "uuids": [ + { "uuid": 7009, "value": 0.1, "name": "锐矛之赐" } + ] + }, + { + "id": "fp-thorns-aura", + "label": "荆棘领域 8", + "tags": ["反伤", "防御"], + "lv": 4, + "uuids": [ + { "uuid": 7022, "value": 8, "name": "荆棘领域" } + ] + } + ] +} diff --git a/put/server/index.js b/put/server/index.js index d41fafa2..b4f3ebc1 100644 --- a/put/server/index.js +++ b/put/server/index.js @@ -103,6 +103,47 @@ app.post('/api/heroes', (req, res) => { } }) +const TEMPLATES_PATH = path.join(DATA_DIR, 'templates.json') + +/** 预设库持久化:GET 读取(无文件时返回空池) */ +app.get('/api/templates', (_req, res) => { + try { + if (!fs.existsSync(TEMPLATES_PATH)) return res.json({ triggerPresets: [], fieldPresets: [] }) + const data = JSON.parse(fs.readFileSync(TEMPLATES_PATH, 'utf8')) + // 兼容旧格式:若根上是 templates 数组,返回空池让前端走默认 + if (Array.isArray(data.templates)) return res.json({ triggerPresets: [], fieldPresets: [] }) + res.json({ + triggerPresets: Array.isArray(data.triggerPresets) ? data.triggerPresets : [], + fieldPresets: Array.isArray(data.fieldPresets) ? data.fieldPresets : [], + }) + } catch (e) { + res.status(500).json({ error: String(e?.message || e) }) + } +}) + +/** 职业模板持久化:POST 全量覆盖保存(结构:{ triggerPresets: [], fieldPresets: [] }) */ +app.post('/api/templates', (req, res) => { + const body = req.body || {} + const triggerPresets = body.triggerPresets + const fieldPresets = body.fieldPresets + if (!Array.isArray(triggerPresets) || !Array.isArray(fieldPresets)) { + return res.status(400).json({ error: 'triggerPresets 与 fieldPresets 必须为数组' }) + } + // 触发预设必须绑定合法触发组 + const groups = ['atking', 'atked', 'fstart', 'dead'] + const bad = triggerPresets.find((p) => !groups.includes(p.group)) + if (bad) { + return res.status(400).json({ error: `触发预设 ${bad.id || bad.label} 的 group 非法:${bad.group}` }) + } + try { + fs.mkdirSync(DATA_DIR, { recursive: true }) + fs.writeFileSync(TEMPLATES_PATH, JSON.stringify({ triggerPresets, fieldPresets }, null, 2), 'utf8') + res.json({ ok: true, path: TEMPLATES_PATH, triggerCount: triggerPresets.length, fieldCount: fieldPresets.length }) + } catch (e) { + res.status(500).json({ error: String(e?.message || e) }) + } +}) + /** 序列化 overrides:TGroup 以字符串存储(如 "Team"),导出时还原为枚举表达式 TGroup.Team; * buff_target 以数值存储(0-3),导出时还原为 BuffTarget.X 枚举表达式提升可读性 */ const BUFF_TARGET_NAMES = ['Hit', 'Self', 'Ally', 'Enemy'] diff --git a/put/web/src/App.vue b/put/web/src/App.vue index 91615175..1b93b228 100644 --- a/put/web/src/App.vue +++ b/put/web/src/App.vue @@ -3,6 +3,10 @@

英雄配置编辑器

+ + 英雄列表 + 预设库 + 刷新配置数据 保存到服务器 写入 heroSet.ts @@ -10,6 +14,17 @@
+ + + + + + +
@@ -199,17 +225,26 @@ import { api } from './api' import { HERO_ROLES, GROW_TYPES, H_TYPES, ROLE_ATK_SKILL, ROLE_ULT_SKILL, SUPPORT_ULT, - defaultHero, defaultSkill, + defaultSkill, } from './constants' import SkillEditor from './components/SkillEditor.vue' import TriggerGroupEditor from './components/TriggerGroupEditor.vue' import FieldEditor from './components/FieldEditor.vue' import BonusEditor from './components/BonusEditor.vue' +import HeroWizard from './components/HeroWizard.vue' +import TemplateManager from './components/TemplateManager.vue' const heroes = ref([]) +// 预设库(跨职业复用资产,与职业默认解耦) +const triggerPresets = ref([]) +const fieldPresets = ref([]) const activeIdx = ref(-1) const meta = ref(null) const loadingMeta = ref(false) +// 视图切换:heroes 英雄列表 / templates 模板管理 +const view = ref('heroes') +// 新建英雄向导可见性 +const wizardVisible = ref(false) // 编辑弹窗可见性:首页卡片点「修改/新建」打开,关闭仅收起不丢数据 const editVisible = ref(false) @@ -389,13 +424,13 @@ function openEdit(i) { activeIdx.value = i editVisible.value = true } +// 「+ 新建英雄」改为打开向导:选职业/技能底座/勾选模板预设,一键生成完整英雄 function addHero() { - const nh = defaultHero() - const used = new Set(heroes.value.map((h) => h.uuid)) - let uuid = 5001 - while (used.has(uuid)) uuid++ - nh.uuid = uuid - heroes.value.push(nh) + wizardVisible.value = true +} +// 向导创建完成:追加到列表并打开编辑弹窗微调 +function onWizardCreate(h) { + heroes.value.push(h) openEdit(heroes.value.length - 1) } function removeHero(i) { @@ -448,6 +483,16 @@ async function loadHeroes() { } } +async function loadTemplates() { + try { + const r = await api.fetchTemplates() + triggerPresets.value = Array.isArray(r.triggerPresets) ? r.triggerPresets : [] + fieldPresets.value = Array.isArray(r.fieldPresets) ? r.fieldPresets : [] + } catch (e) { + ElMessage.error('加载预设库失败: ' + (e?.response?.data?.error || e.message)) + } +} + async function doSave() { try { const r = await api.saveHeroes(heroes.value) @@ -492,5 +537,6 @@ async function doExportCsv() { onMounted(() => { loadMeta() loadHeroes() + loadTemplates() }) diff --git a/put/web/src/api.js b/put/web/src/api.js index 27dd2423..0ed89e2a 100644 --- a/put/web/src/api.js +++ b/put/web/src/api.js @@ -7,4 +7,6 @@ export const api = { saveHeroes: (heroes) => axios.post('/api/heroes', { 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), + fetchTemplates: () => axios.get('/api/templates').then((r) => r.data), + saveTemplates: (templates) => axios.post('/api/templates', { templates }).then((r) => r.data), } diff --git a/put/web/src/components/HeroWizard.vue b/put/web/src/components/HeroWizard.vue new file mode 100644 index 00000000..58e9fdf5 --- /dev/null +++ b/put/web/src/components/HeroWizard.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/put/web/src/components/TemplateManager.vue b/put/web/src/components/TemplateManager.vue new file mode 100644 index 00000000..36dd504f --- /dev/null +++ b/put/web/src/components/TemplateManager.vue @@ -0,0 +1,411 @@ + + + + + diff --git a/put/web/src/constants.js b/put/web/src/constants.js index b61f5db9..049cf101 100644 --- a/put/web/src/constants.js +++ b/put/web/src/constants.js @@ -54,6 +54,34 @@ export const ROLE_ULT_SKILL = { // 辅助类技能(任意职业可用作大招) export const SUPPORT_ULT = [6301, 6302, 6407, 6412, 6413, 6414, 6415, 6416, 6501, 6514, 6515, 6516, 6517, 6518, 6519] +// 职业默认值(新建英雄向导用):形态/攻速/稀有度/三维偏移 +// 数值取自现有英雄样本中位数,向导内可手动微调 +export const ROLE_DEFAULTS = { + Tank: { typeKey: 'Melee', atkSpeed: 'Slow', rarity: 4, statBase: { hp: 20, ap: 0, def: 10 } }, + Warrior: { typeKey: 'Melee', atkSpeed: 'Slow', rarity: 4, statBase: { hp: 5, ap: 20, def: 5 } }, + Assassin: { typeKey: 'Mid', atkSpeed: 'Fast', rarity: 4, statBase: { hp: 0, ap: 30, def: 0 } }, + Archer: { typeKey: 'Long', atkSpeed: 'Normal', rarity: 4, statBase: { hp: 0, ap: 30, def: 0 } }, + Mage: { typeKey: 'Long', atkSpeed: 'Slow', rarity: 4, statBase: { hp: -5, ap: 10, def: 0 } }, + Support: { typeKey: 'Long', atkSpeed: 'Normal', rarity: 4, statBase: { hp: 10, ap: 20, def: 0 } }, +} + +// 攻速档 → cd 数值 / cdExpr 表达式 映射(向导生成普攻用) +export const ATK_SPEED_MAP = { + VeryFast: { cd: 0.75, cdExpr: 'AtkSpeedSet[AtkSpeedLv.VeryFast].cd' }, + Fast: { cd: 0.9, cdExpr: 'AtkSpeedSet[AtkSpeedLv.Fast].cd' }, + Normal: { cd: 1.05, cdExpr: 'AtkSpeedSet[AtkSpeedLv.Normal].cd' }, + Slow: { cd: 1.2, cdExpr: 'AtkSpeedSet[AtkSpeedLv.Slow].cd' }, + VerySlow: { cd: 1.5, cdExpr: 'AtkSpeedSet[AtkSpeedLv.VerySlow].cd' }, +} + +// 触发组 key 列表(向导/模板管理共用) +export const TRIGGER_GROUPS = [ + { key: 'atking', label: 'atking 攻击后' }, + { key: 'atked', label: 'atked 受击后' }, + { key: 'fstart', label: 'fstart 战斗开始' }, + { key: 'dead', label: 'dead 死亡后' }, +] + export function defaultHero() { return { uuid: 5001, @@ -97,6 +125,29 @@ export function defaultTriggerEntry(lv = 4) { return { lv, s_lv: 1, name: '', t_num: 3, overrides: {}, desc: '' } } +// 触发预设工厂:预设是跨职业复用的触发组条目(group + 技能底座 uuid + 完整 entry) +export function defaultTriggerPreset(group = 'atking') { + return { + id: `tp-${group}-${Date.now()}`, + label: '', + group, + uuid: null, + tags: [], + entry: defaultTriggerEntry(4), + } +} + +// 光环预设工厂:一个预设 = 解锁等级 + 多个光环词条 +export function defaultFieldPreset() { + return { + id: `fp-${Date.now()}`, + label: '', + tags: [], + lv: 4, + uuids: [], + } +} + // overrides 可用键说明 export const OVERRIDE_KEYS = [ { key: 'ap', label: 'ap 攻击百分比' }, diff --git a/put/web/vite.config.js b/put/web/vite.config.js index 4fa900a6..26f90413 100644 --- a/put/web/vite.config.js +++ b/put/web/vite.config.js @@ -4,10 +4,11 @@ import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { - port: 5173, + port: 4004, + strictPort: true, proxy: { '/api': { - target: 'http://localhost:3001', + target: 'http://127.0.0.1:3001', changeOrigin: true, }, },