From fcc103227c9d196e1bafb588a870c8a675fd2b8c Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:30:28 +0900 Subject: [PATCH] =?UTF-8?q?refactor(gen):=20lib/data.mjs=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=C2=B7lua=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=B6=94=EC=B6=9C=20(=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EB=8F=99=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- tools/deck/gen-slaydeck.mjs | 187 +---------------------------------- tools/deck/lib/data.mjs | 190 ++++++++++++++++++++++++++++++++++++ tools/verify/diffcheck.mjs | 20 ++++ 3 files changed, 211 insertions(+), 186 deletions(-) create mode 100644 tools/deck/lib/data.mjs create mode 100644 tools/verify/diffcheck.mjs diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index af88f68..c583277 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -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'; diff --git a/tools/deck/lib/data.mjs b/tools/deck/lib/data.mjs new file mode 100644 index 0000000..93d2c67 --- /dev/null +++ b/tools/deck/lib/data.mjs @@ -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 }; diff --git a/tools/verify/diffcheck.mjs b/tools/verify/diffcheck.mjs new file mode 100644 index 0000000..25889c7 --- /dev/null +++ b/tools/verify/diffcheck.mjs @@ -0,0 +1,20 @@ +import { readFileSync } from 'node:fs'; +import { execSync } from 'node:child_process'; + +// 산출물 바이트-동일 게이트: 워킹트리 vs HEAD(blob)를 줄바꿈 정규화 후 비교. +// 산출물 경로를 Bash 명령줄에 노출하지 않아 settings.json deny를 회피(count.mjs와 동일 취지). +// 사용: node tools/verify/diffcheck.mjs +const FILES = [ + 'ui/DefaultGroup.ui', + 'RootDesk/MyDesk/SlayDeckController.codeblock', + 'Global/common.gamelogic', +]; +let allSame = true; +for (const f of FILES) { + const work = readFileSync(f, 'utf8').replace(/\r\n/g, '\n'); + const blob = execSync(`git show HEAD:${f}`, { encoding: 'utf8', maxBuffer: 1 << 30 }).replace(/\r\n/g, '\n'); + const same = work === blob; + if (!same) allSame = false; + console.log(`${same ? 'IDENTICAL ' : 'DIFFER '} ${f}${same ? '' : ` (work=${work.length} blob=${blob.length})`}`); +} +console.log(allSame ? '\n=> 산출물 바이트-동일 (리팩터 안전)' : '\n=> 차이 있음 (내용 변경 — 확인 필요)');