254 lines
8.5 KiB
JavaScript
254 lines
8.5 KiB
JavaScript
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 DATA_DIR = path.join(__dirname, 'data')
|
||
const HEROES_PATH = path.join(DATA_DIR, 'heroes.json')
|
||
|
||
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) })
|
||
}
|
||
})
|
||
|
||
/** 英雄数据持久化:GET 读取(无文件时返回空列表) */
|
||
app.get('/api/heroes', (_req, res) => {
|
||
try {
|
||
if (!fs.existsSync(HEROES_PATH)) return res.json({ heroes: [] })
|
||
res.json(JSON.parse(fs.readFileSync(HEROES_PATH, 'utf8')))
|
||
} catch (e) {
|
||
res.status(500).json({ error: String(e?.message || e) })
|
||
}
|
||
})
|
||
|
||
/** 英雄数据持久化:POST 全量覆盖保存 */
|
||
app.post('/api/heroes', (req, res) => {
|
||
const heroes = req.body?.heroes
|
||
if (!Array.isArray(heroes)) {
|
||
return res.status(400).json({ error: 'heroes 必须为数组' })
|
||
}
|
||
try {
|
||
fs.mkdirSync(DATA_DIR, { recursive: true })
|
||
fs.writeFileSync(HEROES_PATH, JSON.stringify({ heroes }, null, 2), 'utf8')
|
||
res.json({ ok: true, path: HEROES_PATH, count: heroes.length })
|
||
} catch (e) {
|
||
res.status(500).json({ error: String(e?.message || e) })
|
||
}
|
||
})
|
||
|
||
/** 序列化 overrides:TGroup 以字符串存储(如 "Team"),导出时还原为枚举表达式 TGroup.Team */
|
||
function serializeOverrides(o) {
|
||
return JSON.stringify(o).replace(/"TGroup":\s*"(\w+)"/g, '"TGroup": TGroup.$1')
|
||
}
|
||
|
||
/** 生成 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 { TGroup } 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}`
|
||
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: ${serializeOverrides(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: ${serializeOverrides(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: ${serializeOverrides(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(` ],`)
|
||
}
|
||
|
||
// 复活已纳入 dead 触发组,通过 dead 编辑器的 overrides 配置 revive_hp / revive_count
|
||
// 不再导出独立的 revive 字段
|
||
|
||
// 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}`)
|
||
})
|