feat(editor): 完成英雄配置编辑器全功能开发
本提交新增完整的英雄配置可视化编辑器: 1. 初始化前后端项目结构,配置monorepo工作区 2. 实现后端API服务,支持读取游戏配置、导出hexport.ts文件 3. 开发前端Vue应用,包含完整的英雄增删改、属性配置、技能编辑功能 4. 实现多类型编辑器组件:基础属性、普攻/大招编辑、触发技能、光环、复活、属性加成等 5. 清理冗余的配置元数据文件
This commit is contained in:
229
put/server/index.js
Normal file
229
put/server/index.js
Normal 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}`)
|
||||
})
|
||||
Reference in New Issue
Block a user