refactor(gen): lib/data.mjs로 데이터·lua 테이블 추출 (출력 바이트 동일)

gen-slaydeck.mjs의 데이터 로드·검증·luaXxxTable·게임상수(라인 3~188)를
tools/deck/lib/data.mjs로 이동, import로 연결. 산출물 무변경(diffcheck로 검증).
+ tools/verify/diffcheck.mjs: 워킹트리 vs HEAD 줄바꿈 정규화 비교(deny 회피) 게이트.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 02:30:28 +09:00
parent 44878bab9e
commit fcc103227c
3 changed files with 211 additions and 186 deletions

View File

@@ -1,191 +1,6 @@
import { readFileSync, writeFileSync } from 'node:fs';
const CARDS = JSON.parse(readFileSync('data/cards.json', 'utf8'));
const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
// 검증 (fail-fast): 잘못된 데이터면 생성 중단
const CLASSES = {
warrior: { label: '전사', maxHp: 80 },
bandit: { label: '도적', maxHp: 70 },
magician: { label: '마법사', maxHp: 70 },
};
for (const cls of Object.keys(CLASSES)) {
if (!CARDS.starterDecks?.[cls]) throw new Error(`[gen-slaydeck] starterDecks.${cls} 없음`);
for (const id of CARDS.starterDecks[cls]) {
if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`);
}
}
// 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드)
const JOBS = {
warrior: [
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' },
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' },
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' },
],
magician: [
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' },
{ id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' },
{ id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' },
],
bandit: [
{ id: 'shiv', name: 'Shiv', desc: 'Many small attacks\nBlade Dance\nAccuracy · After Image', starter: 'BladeDance' },
{ id: 'poisoner', name: 'Poison', desc: 'Poison scaling\nDeadly Poison\nCatalyst · Noxious Fumes', starter: 'DeadlyPoison' },
{ id: 'trickster', name: 'Trickster', desc: 'Draw and tempo\nAcrobatics\nAdrenaline · Tools', starter: 'Acrobatics' },
],
};
for (const [cls, jobs] of Object.entries(JOBS)) {
for (const j of jobs) {
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
}
}
// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점
const SOUL_UNLOCKS = [
{ key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 },
{ key: 'hp', name: '단련된 육체', desc: '시작 최대 HP +15', cost: 4 },
{ key: 'trim', name: '덱 정제', desc: '시작 덱에서 기본 카드 1장 제거', cost: 5 },
{ key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 },
];
function luaSoulShopTable(unlocks) {
const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${u.cost} },`).join('\n');
return `self.SoulShopDef = {\n${items}\n}`;
}
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`);
}
// 카드 프레임 (사용자 제작 이미지 — 로컬 임포트 .sprite RUID, 직업 3종 × 등급 3종)
const CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json', 'utf8'));
const RARITIES = ['normal', 'unique', 'legend'];
for (const [fid, fr] of Object.entries(CARDFRAMES.frames)) {
for (const r of RARITIES) {
if (!fr[r]) throw new Error(`[gen-slaydeck] cardframes.frames.${fid}.${r} RUID 없음`);
}
}
for (const [id, c] of Object.entries(CARDS.cards)) {
if (!RARITIES.includes(c.rarity)) throw new Error(`[gen-slaydeck] 카드 ${id} rarity 누락/오류: ${c.rarity}`);
const fc = CARDFRAMES.classToFrame[c.class];
if (!fc || !CARDFRAMES.frames[fc]) throw new Error(`[gen-slaydeck] 카드 ${id} class ${c.class} → 프레임 매핑 없음`);
}
function frameRuid(card) {
return CARDFRAMES.frames[CARDFRAMES.classToFrame[card.class]][card.rarity];
}
function luaFramesTable() {
const frames = Object.entries(CARDFRAMES.frames).map(([fid, fr]) =>
`\t${fid} = { normal = ${luaStr(fr.normal)}, unique = ${luaStr(fr.unique)}, legend = ${luaStr(fr.legend)} },`).join('\n');
const cls = Object.entries(CARDFRAMES.classToFrame).map(([c, f]) => `\t${c} = ${luaStr(f)},`).join('\n');
return `self.CardFrames = {\n${frames}\n}\nself.ClassToFrame = {\n${cls}\n}`;
}
function luaNodeIconsTable() {
const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n');
return `self.NodeIcons = {\n${rows}\n}`;
}
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7)
const MAP_COLS = 4;
// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별)
const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71';
const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759';
// 노드 맵 아이콘/배경 (공식 maplestory RUID, data/nodeicons.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8'));
for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) {
if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`);
}
if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류');
// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
for (const c of ['warrior', 'magician', 'bandit']) {
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
}
// 전투 카메라 고정값(StS2: 플레이어 좌·몬스터 우). KickCombatCamera가 StartCombat에서 재confine에 사용.
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
for (const id of RELICS.relicPool) {
if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`);
}
function luaRelicsTable(relics) {
const lines = Object.entries(relics).map(([id, r]) =>
`\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`);
return `self.Relics = {\n${lines.join('\n')}\n}`;
}
const POTIONS = JSON.parse(readFileSync('data/potions.json', 'utf8'));
for (const [pid, p] of Object.entries(POTIONS.potions)) {
if (!p.name || !p.effect || p.value == null) throw new Error(`[gen-slaydeck] potion 필드 누락: ${pid}`);
}
function luaPotionsTable(potions) {
const lines = Object.entries(potions).map(([id, p]) =>
`\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`);
return `self.Potions = {\n${lines.join('\n')}\n}`;
}
function luaIntentsArray(intents) {
return '{ ' + intents.map((it) => {
const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value != null ? it.value : 0}`];
if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`);
if (it.card != null) fields.push(`card = ${luaStr(it.card)}`);
if (it.count != null) fields.push(`count = ${it.count}`);
return `{ ${fields.join(', ')} }`;
}).join(', ') + ' }';
}
function luaEnemiesTable(enemies) {
const lines = Object.entries(enemies).map(([id, e]) =>
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
return `self.Enemies = {\n${lines.join('\n')}\n}`;
}
// Lua 직렬화 헬퍼
function luaStr(s) {
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
}
function luaJobsTable(jobs) {
const cls = Object.entries(jobs).map(([clsId, list]) => {
const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n');
return `\t${clsId} = {\n${items}\n\t},`;
}).join('\n');
return `self.Jobs = {\n${cls}\n}`;
}
function luaCardsTable(cards) {
const lines = Object.entries(cards).map(([id, c]) => {
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
if (c.damage != null) fields.push(`damage = ${c.damage}`);
if (c.block != null) fields.push(`block = ${c.block}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`);
if (c.weak != null) fields.push(`weak = ${c.weak}`);
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
if (c.value != null) fields.push(`value = ${c.value}`);
if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`);
fields.push(`class = ${luaStr(c.class)}`);
fields.push(`rarity = ${luaStr(c.rarity)}`);
if (c.hits != null) fields.push(`hits = ${c.hits}`);
if (c.pierce === true) fields.push('pierce = true');
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
if (c.draw != null) fields.push(`draw = ${c.draw}`);
if (c.heal != null) fields.push(`heal = ${c.heal}`);
if (c.poison != null) fields.push(`poison = ${c.poison}`);
if (c.discard != null) fields.push(`discard = ${c.discard}`);
if (c.discardAll === true) fields.push('discardAll = true');
if (c.sly === true) fields.push('sly = true');
if (c.retain === true) fields.push('retain = true');
if (c.aoe === true) fields.push('aoe = true');
if (c.unplayable === true) fields.push('unplayable = true');
if (c.curse === true) fields.push('curse = true');
if (c.endTurnDamage != null) fields.push(`endTurnDamage = ${c.endTurnDamage}`);
if (c.fx != null) fields.push(`fx = ${luaStr(c.fx)}`);
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
return `\t${id} = { ${fields.join(', ')} },`;
});
return `self.Cards = {\n${lines.join('\n')}\n}`;
}
function luaDeckTable(deck) {
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
}
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs';
const UI_FILE = 'ui/DefaultGroup.ui';
const COMMON_FILE = 'Global/common.gamelogic';

190
tools/deck/lib/data.mjs Normal file
View File

@@ -0,0 +1,190 @@
import { readFileSync } from 'node:fs';
const CARDS = JSON.parse(readFileSync('data/cards.json', 'utf8'));
const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
// 검증 (fail-fast): 잘못된 데이터면 생성 중단
const CLASSES = {
warrior: { label: '전사', maxHp: 80 },
bandit: { label: '도적', maxHp: 70 },
magician: { label: '마법사', maxHp: 70 },
};
for (const cls of Object.keys(CLASSES)) {
if (!CARDS.starterDecks?.[cls]) throw new Error(`[gen-slaydeck] starterDecks.${cls} 없음`);
for (const id of CARDS.starterDecks[cls]) {
if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`);
}
}
// 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드)
const JOBS = {
warrior: [
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' },
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' },
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' },
],
magician: [
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' },
{ id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' },
{ id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' },
],
bandit: [
{ id: 'shiv', name: 'Shiv', desc: 'Many small attacks\nBlade Dance\nAccuracy · After Image', starter: 'BladeDance' },
{ id: 'poisoner', name: 'Poison', desc: 'Poison scaling\nDeadly Poison\nCatalyst · Noxious Fumes', starter: 'DeadlyPoison' },
{ id: 'trickster', name: 'Trickster', desc: 'Draw and tempo\nAcrobatics\nAdrenaline · Tools', starter: 'Acrobatics' },
],
};
for (const [cls, jobs] of Object.entries(JOBS)) {
for (const j of jobs) {
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
}
}
// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점
const SOUL_UNLOCKS = [
{ key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 },
{ key: 'hp', name: '단련된 육체', desc: '시작 최대 HP +15', cost: 4 },
{ key: 'trim', name: '덱 정제', desc: '시작 덱에서 기본 카드 1장 제거', cost: 5 },
{ key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 },
];
function luaSoulShopTable(unlocks) {
const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${u.cost} },`).join('\n');
return `self.SoulShopDef = {\n${items}\n}`;
}
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`);
}
// 카드 프레임 (사용자 제작 이미지 — 로컬 임포트 .sprite RUID, 직업 3종 × 등급 3종)
const CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json', 'utf8'));
const RARITIES = ['normal', 'unique', 'legend'];
for (const [fid, fr] of Object.entries(CARDFRAMES.frames)) {
for (const r of RARITIES) {
if (!fr[r]) throw new Error(`[gen-slaydeck] cardframes.frames.${fid}.${r} RUID 없음`);
}
}
for (const [id, c] of Object.entries(CARDS.cards)) {
if (!RARITIES.includes(c.rarity)) throw new Error(`[gen-slaydeck] 카드 ${id} rarity 누락/오류: ${c.rarity}`);
const fc = CARDFRAMES.classToFrame[c.class];
if (!fc || !CARDFRAMES.frames[fc]) throw new Error(`[gen-slaydeck] 카드 ${id} class ${c.class} → 프레임 매핑 없음`);
}
function frameRuid(card) {
return CARDFRAMES.frames[CARDFRAMES.classToFrame[card.class]][card.rarity];
}
function luaFramesTable() {
const frames = Object.entries(CARDFRAMES.frames).map(([fid, fr]) =>
`\t${fid} = { normal = ${luaStr(fr.normal)}, unique = ${luaStr(fr.unique)}, legend = ${luaStr(fr.legend)} },`).join('\n');
const cls = Object.entries(CARDFRAMES.classToFrame).map(([c, f]) => `\t${c} = ${luaStr(f)},`).join('\n');
return `self.CardFrames = {\n${frames}\n}\nself.ClassToFrame = {\n${cls}\n}`;
}
function luaNodeIconsTable() {
const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n');
return `self.NodeIcons = {\n${rows}\n}`;
}
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7)
const MAP_COLS = 4;
// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별)
const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71';
const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759';
// 노드 맵 아이콘/배경 (공식 maplestory RUID, data/nodeicons.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8'));
for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) {
if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`);
}
if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류');
// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
for (const c of ['warrior', 'magician', 'bandit']) {
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
}
// 전투 카메라 고정값(StS2: 플레이어 좌·몬스터 우). KickCombatCamera가 StartCombat에서 재confine에 사용.
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
for (const id of RELICS.relicPool) {
if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`);
}
function luaRelicsTable(relics) {
const lines = Object.entries(relics).map(([id, r]) =>
`\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`);
return `self.Relics = {\n${lines.join('\n')}\n}`;
}
const POTIONS = JSON.parse(readFileSync('data/potions.json', 'utf8'));
for (const [pid, p] of Object.entries(POTIONS.potions)) {
if (!p.name || !p.effect || p.value == null) throw new Error(`[gen-slaydeck] potion 필드 누락: ${pid}`);
}
function luaPotionsTable(potions) {
const lines = Object.entries(potions).map(([id, p]) =>
`\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`);
return `self.Potions = {\n${lines.join('\n')}\n}`;
}
function luaIntentsArray(intents) {
return '{ ' + intents.map((it) => {
const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value != null ? it.value : 0}`];
if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`);
if (it.card != null) fields.push(`card = ${luaStr(it.card)}`);
if (it.count != null) fields.push(`count = ${it.count}`);
return `{ ${fields.join(', ')} }`;
}).join(', ') + ' }';
}
function luaEnemiesTable(enemies) {
const lines = Object.entries(enemies).map(([id, e]) =>
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
return `self.Enemies = {\n${lines.join('\n')}\n}`;
}
// Lua 직렬화 헬퍼
function luaStr(s) {
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
}
function luaJobsTable(jobs) {
const cls = Object.entries(jobs).map(([clsId, list]) => {
const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n');
return `\t${clsId} = {\n${items}\n\t},`;
}).join('\n');
return `self.Jobs = {\n${cls}\n}`;
}
function luaCardsTable(cards) {
const lines = Object.entries(cards).map(([id, c]) => {
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
if (c.damage != null) fields.push(`damage = ${c.damage}`);
if (c.block != null) fields.push(`block = ${c.block}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`);
if (c.weak != null) fields.push(`weak = ${c.weak}`);
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
if (c.value != null) fields.push(`value = ${c.value}`);
if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`);
fields.push(`class = ${luaStr(c.class)}`);
fields.push(`rarity = ${luaStr(c.rarity)}`);
if (c.hits != null) fields.push(`hits = ${c.hits}`);
if (c.pierce === true) fields.push('pierce = true');
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
if (c.draw != null) fields.push(`draw = ${c.draw}`);
if (c.heal != null) fields.push(`heal = ${c.heal}`);
if (c.poison != null) fields.push(`poison = ${c.poison}`);
if (c.discard != null) fields.push(`discard = ${c.discard}`);
if (c.discardAll === true) fields.push('discardAll = true');
if (c.sly === true) fields.push('sly = true');
if (c.retain === true) fields.push('retain = true');
if (c.aoe === true) fields.push('aoe = true');
if (c.unplayable === true) fields.push('unplayable = true');
if (c.curse === true) fields.push('curse = true');
if (c.endTurnDamage != null) fields.push(`endTurnDamage = ${c.endTurnDamage}`);
if (c.fx != null) fields.push(`fx = ${luaStr(c.fx)}`);
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
return `\t${id} = { ${fields.join(', ')} },`;
});
return `self.Cards = {\n${lines.join('\n')}\n}`;
}
function luaDeckTable(deck) {
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
}
export { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable };