feat(editor): 完成英雄配置编辑器全功能开发

本提交新增完整的英雄配置可视化编辑器:
1.  初始化前后端项目结构,配置monorepo工作区
2.  实现后端API服务,支持读取游戏配置、导出hexport.ts文件
3.  开发前端Vue应用,包含完整的英雄增删改、属性配置、技能编辑功能
4.  实现多类型编辑器组件:基础属性、普攻/大招编辑、触发技能、光环、复活、属性加成等
5.  清理冗余的配置元数据文件
This commit is contained in:
panFD
2026-09-02 23:35:40 +08:00
parent 6fed56d3c7
commit a2dc40997c
28 changed files with 4641 additions and 66 deletions

229
put/server/index.js Normal file
View File

@@ -0,0 +1,229 @@
import express from 'express'
import cors from 'cors'
import esbuild from 'esbuild'
import fs from 'node:fs'
import path from 'node:path'
import url from 'node:url'
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
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 app = express()
app.use(cors())
app.use(express.json({ limit: '5mb' }))
/** 编译配置 TS 文件为可执行 CJS,返回模块 exports */
async function loadConfigModule(file) {
const result = await esbuild.build({
entryPoints: [path.join(CONFIG_DIR, file)],
bundle: true,
write: false,
format: 'cjs',
platform: 'node',
target: 'node18',
external: ['cc'],
logLevel: 'silent',
})
const code = result.outputFiles[0].text
const module = { exports: {} }
// 屏蔽对 cc 的引用(v3 等)
const fn = new Function('module', 'exports', 'require', code)
fn(module, module.exports, () => ({ v3: () => ({}) }))
return module.exports
}
let cache = null
async function buildCache() {
const [skill, buff, hero] = await Promise.all([
loadConfigModule('SkillSet.ts'),
loadConfigModule('BuffSet.ts'),
loadConfigModule('heroSet.ts'),
])
cache = {
SkillSet: skill.SkillSet,
FieldSkillSet: skill.FieldSkillSet,
BuffList: buff.BuffList,
HERO_ROLE_BASE: hero.HERO_ROLE_BASE,
HERO_GROW_COEF: hero.HERO_GROW_COEF,
HERO_LV12_SPECIAL_ATTR: hero.HERO_LV12_SPECIAL_ATTR,
AtkSpeedSet: hero.AtkSpeedSet,
HeroDisVal: hero.HeroDisVal,
}
return cache
}
app.get('/api/meta', async (_req, res) => {
try {
if (!cache) await buildCache()
res.json(cache)
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
app.post('/api/refresh', async (_req, res) => {
try {
await buildCache()
res.json({ ok: true })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** 生成 hexport.ts 内容 */
function renderHeroTs(heroes) {
const nl = '\n'
let out = ''
out += `/**${nl}`
out += ` * 英雄配置导出文件 - 由英雄配置编辑器生成${nl}`
out += ` * 生成时间: ${new Date().toISOString()}${nl}`
out += ` * 使用方式: import { HexportHeroInfo } from "./hexport" 后合并进 HeroInfo${nl}`
out += ` */${nl}`
out += `import { v3 } from "cc"${nl}`
out += `import { FacSet } from "./GameSet"${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}`
for (const h of heroes) {
out += renderSingleHero(h)
}
out += `}${nl}`
return out
}
function renderSingleHero(h) {
const lines = []
const indent = ' '
const push = (s) => lines.push(indent + s)
push(`// ${h.uuid} ${h.name} — ${h.comment || ''}`)
push(`${h.uuid}: {`)
const roleExpr = `HERO_ROLE_BASE[HRole.${h.roleKey}]`
const stat = (k) => {
const base = h.statBase?.[k] ?? 0
if (base === 0) return `${roleExpr}.${k}`
return `${roleExpr}.${k} ${base > 0 ? '+' : '-'} ${Math.abs(base)}`
}
push(
` uuid: ${h.uuid}, name: ${JSON.stringify(h.name)}, path: ${JSON.stringify(h.path)}, fac: FacSet.HERO, rarity: ${h.rarity}, lv: 1, type: HType.${h.typeKey}, grow_type: HGrowType.${h.growKey}, role: HRole.${h.roleKey}, hp: ${stat('hp')}, ap: ${stat('ap')}, def: ${stat('def')},`
)
if (h.dis != null) push(` dis: ${h.dis},`)
if (h.speed != null) push(` speed: ${h.speed},`)
// skills
push(` skills: {`)
for (const sk of h.skills) {
const parts = [`uuid: ${sk.uuid}`]
if (sk.name) parts.push(`name: ${JSON.stringify(sk.name)}`)
parts.push(`lv: 1`)
parts.push(`cd: ${sk.cdExpr || sk.cd}`)
parts.push(`ccd: 0`)
if (sk.overrides && Object.keys(sk.overrides).length) parts.push(`overrides: ${JSON.stringify(sk.overrides)}`)
if (sk.desc) parts.push(`desc: ${JSON.stringify(sk.desc)}`)
if (sk.lv_entries?.length) {
push(` ${sk.uuid}: { ${parts.join(', ')},`)
push(` lv_entries: [`)
for (const e of sk.lv_entries) {
const eparts = [`lv: ${e.lv}`]
if (e.s_lv) eparts.push(`s_lv: SkillLv.Lv${e.s_lv}`)
if (e.overrides && Object.keys(e.overrides).length) eparts.push(`overrides: ${JSON.stringify(e.overrides)}`)
if (e.desc) eparts.push(`desc: ${JSON.stringify(e.desc)}`)
push(` { ${eparts.join(', ')} },`)
}
push(` ],`)
push(` },`)
} else {
push(` ${sk.uuid}: { ${parts.join(', ')} },`)
}
}
push(` },`)
// triggers
const triggerKeys = ['atking', 'atked', 'fstart', 'dead']
for (const tk of triggerKeys) {
const grp = h[tk]
if (!grp || !Object.keys(grp).length) continue
push(` ${tk}: {`)
for (const [sid, entries] of Object.entries(grp)) {
push(` ${sid}: [`)
for (const e of entries) {
const eparts = [`lv: ${e.lv}`]
if (e.s_lv) eparts.push(`s_lv: SkillLv.Lv${e.s_lv}`)
if (e.name) eparts.push(`name: ${JSON.stringify(e.name)}`)
eparts.push(`t_num: ${e.t_num}`)
if (e.overrides && Object.keys(e.overrides).length) eparts.push(`overrides: ${JSON.stringify(e.overrides)}`)
if (e.desc) eparts.push(`desc: ${JSON.stringify(e.desc)}`)
push(` { ${eparts.join(', ')} },`)
}
push(` ],`)
}
push(` },`)
}
// field
if (h.field?.length) {
push(` field: [`)
for (const f of h.field) {
const uuids = f.uuids.map((u) => {
const uparts = [`uuid: ${u.uuid}`, `value: ${u.value}`]
if (u.name) uparts.push(`name: ${JSON.stringify(u.name)}`)
return `{ ${uparts.join(', ')} }`
})
const desc = f.desc ? `, desc: ${JSON.stringify(f.desc)}` : ''
push(` { lv: ${f.lv}, uuids: [${uuids.join(', ')}]${desc} },`)
}
push(` ],`)
}
// revive
if (h.revive?.length) {
push(` revive: [`)
for (const r of h.revive) {
const rparts = [`lv: ${r.lv}`, `s_uuid: ${r.s_uuid}`, `r_num: ${r.r_num}`, `upr: ${r.upr}`]
if (r.name) rparts.push(`name: ${JSON.stringify(r.name)}`)
if (r.desc) rparts.push(`desc: ${JSON.stringify(r.desc)}`)
push(` { ${rparts.join(', ')} },`)
}
push(` ],`)
}
// bonuses
for (const bk of ['ap_bonus', 'hp_bonus', 'def_bonus']) {
if (!h[bk]?.length) continue
push(` ${bk}: [`)
for (const b of h[bk]) push(` { lv: ${b.lv}, value: ${b.value} },`)
push(` ],`)
}
push(` info: ${JSON.stringify(h.info || '')},`)
push(`},`)
return lines.join('\n') + '\n'
}
app.post('/api/export', (req, res) => {
try {
const heroes = req.body?.heroes
if (!Array.isArray(heroes) || !heroes.length) {
return res.status(400).json({ error: 'heroes 不能为空' })
}
const content = renderHeroTs(heroes)
fs.writeFileSync(EXPORT_PATH, content, 'utf8')
res.json({ ok: true, path: EXPORT_PATH, bytes: Buffer.byteLength(content) })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
app.get('/api/export', (_req, res) => {
if (!fs.existsSync(EXPORT_PATH)) return res.status(404).json({ error: '尚未生成' })
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.send(fs.readFileSync(EXPORT_PATH, 'utf8'))
})
const PORT = 3001
app.listen(PORT, () => {
console.log(`[hero-config-tool] server listening http://localhost:${PORT}`)
console.log(`[hero-config-tool] export path: ${EXPORT_PATH}`)
})

14
put/server/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch index.js"
},
"dependencies": {
"cors": "^2.8.5",
"esbuild": "^0.24.0",
"express": "^4.21.2"
}
}

View File

@@ -0,0 +1,21 @@
// 验证 server /api/meta 是否正常工作
import http from 'node:http'
http.get('http://localhost:3001/api/meta', (res) => {
let data = ''
res.on('data', (c) => (data += c))
res.on('end', () => {
try {
const m = JSON.parse(data)
console.log('SkillSet:', Object.keys(m.SkillSet || {}).length)
console.log('FieldSkillSet:', Object.keys(m.FieldSkillSet || {}).length)
console.log('BuffList:', Object.keys(m.BuffList || {}).length)
console.log('HERO_ROLE_BASE:', Object.keys(m.HERO_ROLE_BASE || {}).length)
console.log('AtkSpeedSet:', Object.keys(m.AtkSpeedSet || {}).length)
const s6010 = m.SkillSet?.[6010]
console.log('Skill 6010:', s6010 ? `${s6010.name} ap=${s6010.ap}` : 'missing')
} catch (e) {
console.error('parse fail:', e.message, 'raw:', data.slice(0, 200))
}
})
}).on('error', (e) => console.error('http err:', e.message))