From 8789330a4ecd6b9864e24701043e75b7e282fd9f Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 9 Jun 2026 01:25:25 +0900 Subject: [PATCH] =?UTF-8?q?feat(D):=20=EC=B9=B4=EB=93=9C/=EC=A0=81=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=99=B8=EB=B6=80=ED=99=94=20(da?= =?UTF-8?q?ta/*.json)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cards·시작덱·적 정의를 data/cards.json·data/enemies.json으로 분리, gen-slaydeck가 로드·검증·주입. - data/cards.json: 카드 정의(name/cost/kind/damage|block/desc) + starterDeck - data/enemies.json: 적 정의(name/maxHp/intents) + activeEnemy - 생성기: JSON 로드 + fail-fast 검증(미존재 카드/적 id) + Lua 직렬화 헬퍼 - StartCombat·EnemyMaxHp·카드 미리보기·CombatHud 초기텍스트를 데이터에서 생성 - codeblock 출력은 기존과 동일(순수 리팩터), ui 미리보기는 카드 종류 순환 표시 - 검증: 데이터 1장 수치 변경→재생성 반영 확인, 결정성, fail-fast(exit1), 메이커 Play 정상 - 수치는 임시 placeholder, 추후 메이플 IP대로 카드/적 확장 예정 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/cards.json | 8 +++++ data/enemies.json | 14 ++++++++ tools/gen-slaydeck.mjs | 79 +++++++++++++++++++++++++++++------------- ui/DefaultGroup.ui | 44 +++++++++++------------ 4 files changed, 99 insertions(+), 46 deletions(-) create mode 100644 data/cards.json create mode 100644 data/enemies.json diff --git a/data/cards.json b/data/cards.json new file mode 100644 index 0000000..2f7b99b --- /dev/null +++ b/data/cards.json @@ -0,0 +1,8 @@ +{ + "cards": { + "Strike": { "name": "타격", "cost": 1, "kind": "Attack", "damage": 6, "desc": "피해 6" }, + "Defend": { "name": "방어", "cost": 1, "kind": "Skill", "block": 5, "desc": "방어도 5" }, + "Bash": { "name": "강타", "cost": 2, "kind": "Attack", "damage": 10, "desc": "피해 10" } + }, + "starterDeck": ["Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash"] +} diff --git a/data/enemies.json b/data/enemies.json new file mode 100644 index 0000000..a63a95d --- /dev/null +++ b/data/enemies.json @@ -0,0 +1,14 @@ +{ + "enemies": { + "slime": { + "name": "슬라임", + "maxHp": 45, + "intents": [ + { "kind": "Attack", "value": 10 }, + { "kind": "Attack", "value": 6 }, + { "kind": "Defend", "value": 8 } + ] + } + }, + "activeEnemy": "slime" +} diff --git a/tools/gen-slaydeck.mjs b/tools/gen-slaydeck.mjs index 1d246c1..12ba8ce 100644 --- a/tools/gen-slaydeck.mjs +++ b/tools/gen-slaydeck.mjs @@ -1,5 +1,45 @@ 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): 잘못된 데이터면 생성 중단 +for (const id of CARDS.starterDeck) { + if (!CARDS.cards[id]) { + throw new Error(`[gen-slaydeck] starterDeck에 없는 카드 id 참조: ${id}`); + } +} +if (!ENEMIES.enemies[ENEMIES.activeEnemy]) { + throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`); +} +const ACTIVE_ENEMY = ENEMIES.enemies[ENEMIES.activeEnemy]; + +// Lua 직렬화 헬퍼 +function luaStr(s) { + return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; +} +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}`); + return `\t${id} = { ${fields.join(', ')} },`; + }); + return `self.Cards = {\n${lines.join('\n')}\n}`; +} +function luaDeckTable(deck) { + return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`; +} +function luaIntentsTable(intents) { + const lines = intents.map((it) => `\t{ kind = ${luaStr(it.kind)}, value = ${it.value} },`); + return `self.EnemyIntents = {\n${lines.join('\n')}\n}`; +} +function intentText(it) { + if (it.kind === 'Attack') return `의도: 공격 ${it.value}`; + if (it.kind === 'Defend') return `의도: 방어 ${it.value}`; + return ''; +} + const UI_FILE = 'ui/DefaultGroup.ui'; const COMMON_FILE = 'Global/common.gamelogic'; @@ -176,13 +216,12 @@ function upsertUi() { const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e])); - const cards = [ - { name: '타격', cost: '1', desc: '피해 6', tint: ATTACK }, - { name: '타격', cost: '1', desc: '피해 6', tint: ATTACK }, - { name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND }, - { name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND }, - { name: '강타', cost: '2', desc: '피해 10', tint: ATTACK }, - ]; + // 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시 + const previewIds = Object.keys(CARDS.cards); + const cards = Array.from({ length: 5 }, (_, i) => { + const c = CARDS.cards[previewIds[i % previewIds.length]]; + return { name: c.name, cost: String(c.cost), desc: c.desc, tint: c.kind === 'Attack' ? ATTACK : DEFEND }; + }); for (let i = 1; i <= 5; i++) { const card = byPath.get(`/ui/DefaultGroup/CardHand/Card${i}`); @@ -360,10 +399,10 @@ function upsertUi() { ], })); const enemyTexts = [ - ['EnemyName', { x: 0, y: 358 }, { x: 360, y: 44 }, '슬라임', 28, true, GOLD], - ['EnemyHp', { x: 0, y: 316 }, { x: 360, y: 40 }, 'HP 45/45', 24, true, { r: 1, g: 1, b: 1, a: 1 }], + ['EnemyName', { x: 0, y: 358 }, { x: 360, y: 44 }, ACTIVE_ENEMY.name, 28, true, GOLD], + ['EnemyHp', { x: 0, y: 316 }, { x: 360, y: 40 }, `HP ${ACTIVE_ENEMY.maxHp}/${ACTIVE_ENEMY.maxHp}`, 24, true, { r: 1, g: 1, b: 1, a: 1 }], ['EnemyBlock', { x: 0, y: 280 }, { x: 360, y: 36 }, '방어 0', 20, false, { r: 0.6, g: 0.8, b: 1, a: 1 }], - ['EnemyIntent', { x: 0, y: 244 }, { x: 360, y: 38 }, '의도: 공격 10', 22, true, { r: 1, g: 0.72, b: 0.5, a: 1 }], + ['EnemyIntent', { x: 0, y: 244 }, { x: 360, y: 38 }, intentText(ACTIVE_ENEMY.intents[0]), 22, true, { r: 1, g: 0.72, b: 0.5, a: 1 }], ]; let cmbN = 2; for (const [suffix, pos, size, value, fontSize, bold, color] of enemyTexts) { @@ -497,7 +536,7 @@ function writeCodeblocks() { prop('number', 'PlayerMaxHp', '80'), prop('number', 'PlayerBlock', '0'), prop('number', 'EnemyHp', '0'), - prop('number', 'EnemyMaxHp', '45'), + prop('number', 'EnemyMaxHp', String(ACTIVE_ENEMY.maxHp)), prop('number', 'EnemyBlock', '0'), prop('number', 'EnemyIntentIndex', '1'), prop('boolean', 'CombatOver', 'false'), @@ -510,25 +549,17 @@ self.Turn = 0 self.PlayerMaxHp = 80 self.PlayerHp = self.PlayerMaxHp self.PlayerBlock = 0 -self.EnemyName = "슬라임" -self.EnemyMaxHp = 45 +self.EnemyName = ${luaStr(ACTIVE_ENEMY.name)} +self.EnemyMaxHp = ${ACTIVE_ENEMY.maxHp} self.EnemyHp = self.EnemyMaxHp self.EnemyBlock = 0 -self.EnemyIntents = { - { kind = "Attack", value = 10 }, - { kind = "Attack", value = 6 }, - { kind = "Defend", value = 8 }, -} +${luaIntentsTable(ACTIVE_ENEMY.intents)} self.EnemyIntentIndex = 1 self.CombatOver = false self.DiscardPile = {} self.Hand = {} -self.Cards = { - Strike = { name = "타격", cost = 1, desc = "피해 6", kind = "Attack", damage = 6 }, - Defend = { name = "방어", cost = 1, desc = "방어도 5", kind = "Skill", block = 5 }, - Bash = { name = "강타", cost = 2, desc = "피해 10", kind = "Attack", damage = 10 }, -} -self.DrawPile = { "Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash" } +${luaCardsTable(CARDS.cards)} +${luaDeckTable(CARDS.starterDeck)} self:Shuffle(self.DrawPile) self:BindButtons() self:RenderCombat() diff --git a/ui/DefaultGroup.ui b/ui/DefaultGroup.ui index edc6e83..23e4a05 100644 --- a/ui/DefaultGroup.ui +++ b/ui/DefaultGroup.ui @@ -2059,9 +2059,9 @@ "PreserveSprite": 0, "StartFrameIndex": 0, "Color": { - "r": 0.86, - "g": 0.42, - "b": 0.38, + "r": 0.42, + "g": 0.55, + "b": 0.85, "a": 1 }, "DropShadow": false, @@ -2514,7 +2514,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "타격", + "Text": "방어", "UseOutLine": true, "Enable": true } @@ -2702,7 +2702,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "피해 6", + "Text": "방어도 5", "UseOutLine": true, "Enable": true } @@ -2811,9 +2811,9 @@ "PreserveSprite": 0, "StartFrameIndex": 0, "Color": { - "r": 0.42, - "g": 0.55, - "b": 0.85, + "r": 0.86, + "g": 0.42, + "b": 0.38, "a": 1 }, "DropShadow": false, @@ -3078,7 +3078,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "1", + "Text": "2", "UseOutLine": true, "Enable": true } @@ -3266,7 +3266,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "방어", + "Text": "강타", "UseOutLine": true, "Enable": true } @@ -3454,7 +3454,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "방어도 5", + "Text": "피해 10", "UseOutLine": true, "Enable": true } @@ -3563,9 +3563,9 @@ "PreserveSprite": 0, "StartFrameIndex": 0, "Color": { - "r": 0.42, - "g": 0.55, - "b": 0.85, + "r": 0.86, + "g": 0.42, + "b": 0.38, "a": 1 }, "DropShadow": false, @@ -4018,7 +4018,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "방어", + "Text": "타격", "UseOutLine": true, "Enable": true } @@ -4206,7 +4206,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "방어도 5", + "Text": "피해 6", "UseOutLine": true, "Enable": true } @@ -4315,9 +4315,9 @@ "PreserveSprite": 0, "StartFrameIndex": 0, "Color": { - "r": 0.86, - "g": 0.42, - "b": 0.38, + "r": 0.42, + "g": 0.55, + "b": 0.85, "a": 1 }, "DropShadow": false, @@ -4582,7 +4582,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "2", + "Text": "1", "UseOutLine": true, "Enable": true } @@ -4770,7 +4770,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "강타", + "Text": "방어", "UseOutLine": true, "Enable": true } @@ -4958,7 +4958,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "피해 10", + "Text": "방어도 5", "UseOutLine": true, "Enable": true }