feat(D): 카드/적 데이터 외부화 (data/*.json)
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) <noreply@anthropic.com>
This commit is contained in:
8
data/cards.json
Normal file
8
data/cards.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
14
data/enemies.json
Normal file
14
data/enemies.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"enemies": {
|
||||||
|
"slime": {
|
||||||
|
"name": "슬라임",
|
||||||
|
"maxHp": 45,
|
||||||
|
"intents": [
|
||||||
|
{ "kind": "Attack", "value": 10 },
|
||||||
|
{ "kind": "Attack", "value": 6 },
|
||||||
|
{ "kind": "Defend", "value": 8 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"activeEnemy": "slime"
|
||||||
|
}
|
||||||
@@ -1,5 +1,45 @@
|
|||||||
import { readFileSync, writeFileSync } from 'node:fs';
|
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 UI_FILE = 'ui/DefaultGroup.ui';
|
||||||
const COMMON_FILE = 'Global/common.gamelogic';
|
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 byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
|
||||||
|
|
||||||
const cards = [
|
// 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시
|
||||||
{ name: '타격', cost: '1', desc: '피해 6', tint: ATTACK },
|
const previewIds = Object.keys(CARDS.cards);
|
||||||
{ name: '타격', cost: '1', desc: '피해 6', tint: ATTACK },
|
const cards = Array.from({ length: 5 }, (_, i) => {
|
||||||
{ name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND },
|
const c = CARDS.cards[previewIds[i % previewIds.length]];
|
||||||
{ name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND },
|
return { name: c.name, cost: String(c.cost), desc: c.desc, tint: c.kind === 'Attack' ? ATTACK : DEFEND };
|
||||||
{ name: '강타', cost: '2', desc: '피해 10', tint: ATTACK },
|
});
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const card = byPath.get(`/ui/DefaultGroup/CardHand/Card${i}`);
|
const card = byPath.get(`/ui/DefaultGroup/CardHand/Card${i}`);
|
||||||
@@ -360,10 +399,10 @@ function upsertUi() {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
const enemyTexts = [
|
const enemyTexts = [
|
||||||
['EnemyName', { x: 0, y: 358 }, { x: 360, y: 44 }, '슬라임', 28, true, GOLD],
|
['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 45/45', 24, true, { r: 1, g: 1, b: 1, a: 1 }],
|
['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 }],
|
['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;
|
let cmbN = 2;
|
||||||
for (const [suffix, pos, size, value, fontSize, bold, color] of enemyTexts) {
|
for (const [suffix, pos, size, value, fontSize, bold, color] of enemyTexts) {
|
||||||
@@ -497,7 +536,7 @@ function writeCodeblocks() {
|
|||||||
prop('number', 'PlayerMaxHp', '80'),
|
prop('number', 'PlayerMaxHp', '80'),
|
||||||
prop('number', 'PlayerBlock', '0'),
|
prop('number', 'PlayerBlock', '0'),
|
||||||
prop('number', 'EnemyHp', '0'),
|
prop('number', 'EnemyHp', '0'),
|
||||||
prop('number', 'EnemyMaxHp', '45'),
|
prop('number', 'EnemyMaxHp', String(ACTIVE_ENEMY.maxHp)),
|
||||||
prop('number', 'EnemyBlock', '0'),
|
prop('number', 'EnemyBlock', '0'),
|
||||||
prop('number', 'EnemyIntentIndex', '1'),
|
prop('number', 'EnemyIntentIndex', '1'),
|
||||||
prop('boolean', 'CombatOver', 'false'),
|
prop('boolean', 'CombatOver', 'false'),
|
||||||
@@ -510,25 +549,17 @@ self.Turn = 0
|
|||||||
self.PlayerMaxHp = 80
|
self.PlayerMaxHp = 80
|
||||||
self.PlayerHp = self.PlayerMaxHp
|
self.PlayerHp = self.PlayerMaxHp
|
||||||
self.PlayerBlock = 0
|
self.PlayerBlock = 0
|
||||||
self.EnemyName = "슬라임"
|
self.EnemyName = ${luaStr(ACTIVE_ENEMY.name)}
|
||||||
self.EnemyMaxHp = 45
|
self.EnemyMaxHp = ${ACTIVE_ENEMY.maxHp}
|
||||||
self.EnemyHp = self.EnemyMaxHp
|
self.EnemyHp = self.EnemyMaxHp
|
||||||
self.EnemyBlock = 0
|
self.EnemyBlock = 0
|
||||||
self.EnemyIntents = {
|
${luaIntentsTable(ACTIVE_ENEMY.intents)}
|
||||||
{ kind = "Attack", value = 10 },
|
|
||||||
{ kind = "Attack", value = 6 },
|
|
||||||
{ kind = "Defend", value = 8 },
|
|
||||||
}
|
|
||||||
self.EnemyIntentIndex = 1
|
self.EnemyIntentIndex = 1
|
||||||
self.CombatOver = false
|
self.CombatOver = false
|
||||||
self.DiscardPile = {}
|
self.DiscardPile = {}
|
||||||
self.Hand = {}
|
self.Hand = {}
|
||||||
self.Cards = {
|
${luaCardsTable(CARDS.cards)}
|
||||||
Strike = { name = "타격", cost = 1, desc = "피해 6", kind = "Attack", damage = 6 },
|
${luaDeckTable(CARDS.starterDeck)}
|
||||||
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" }
|
|
||||||
self:Shuffle(self.DrawPile)
|
self:Shuffle(self.DrawPile)
|
||||||
self:BindButtons()
|
self:BindButtons()
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
|
|||||||
@@ -2059,9 +2059,9 @@
|
|||||||
"PreserveSprite": 0,
|
"PreserveSprite": 0,
|
||||||
"StartFrameIndex": 0,
|
"StartFrameIndex": 0,
|
||||||
"Color": {
|
"Color": {
|
||||||
"r": 0.86,
|
"r": 0.42,
|
||||||
"g": 0.42,
|
"g": 0.55,
|
||||||
"b": 0.38,
|
"b": 0.85,
|
||||||
"a": 1
|
"a": 1
|
||||||
},
|
},
|
||||||
"DropShadow": false,
|
"DropShadow": false,
|
||||||
@@ -2514,7 +2514,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "타격",
|
"Text": "방어",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -2702,7 +2702,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "피해 6",
|
"Text": "방어도 5",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -2811,9 +2811,9 @@
|
|||||||
"PreserveSprite": 0,
|
"PreserveSprite": 0,
|
||||||
"StartFrameIndex": 0,
|
"StartFrameIndex": 0,
|
||||||
"Color": {
|
"Color": {
|
||||||
"r": 0.42,
|
"r": 0.86,
|
||||||
"g": 0.55,
|
"g": 0.42,
|
||||||
"b": 0.85,
|
"b": 0.38,
|
||||||
"a": 1
|
"a": 1
|
||||||
},
|
},
|
||||||
"DropShadow": false,
|
"DropShadow": false,
|
||||||
@@ -3078,7 +3078,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "1",
|
"Text": "2",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -3266,7 +3266,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "방어",
|
"Text": "강타",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -3454,7 +3454,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "방어도 5",
|
"Text": "피해 10",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -3563,9 +3563,9 @@
|
|||||||
"PreserveSprite": 0,
|
"PreserveSprite": 0,
|
||||||
"StartFrameIndex": 0,
|
"StartFrameIndex": 0,
|
||||||
"Color": {
|
"Color": {
|
||||||
"r": 0.42,
|
"r": 0.86,
|
||||||
"g": 0.55,
|
"g": 0.42,
|
||||||
"b": 0.85,
|
"b": 0.38,
|
||||||
"a": 1
|
"a": 1
|
||||||
},
|
},
|
||||||
"DropShadow": false,
|
"DropShadow": false,
|
||||||
@@ -4018,7 +4018,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "방어",
|
"Text": "타격",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -4206,7 +4206,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "방어도 5",
|
"Text": "피해 6",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -4315,9 +4315,9 @@
|
|||||||
"PreserveSprite": 0,
|
"PreserveSprite": 0,
|
||||||
"StartFrameIndex": 0,
|
"StartFrameIndex": 0,
|
||||||
"Color": {
|
"Color": {
|
||||||
"r": 0.86,
|
"r": 0.42,
|
||||||
"g": 0.42,
|
"g": 0.55,
|
||||||
"b": 0.38,
|
"b": 0.85,
|
||||||
"a": 1
|
"a": 1
|
||||||
},
|
},
|
||||||
"DropShadow": false,
|
"DropShadow": false,
|
||||||
@@ -4582,7 +4582,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "2",
|
"Text": "1",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -4770,7 +4770,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "강타",
|
"Text": "방어",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
@@ -4958,7 +4958,7 @@
|
|||||||
"bottom": 0
|
"bottom": 0
|
||||||
},
|
},
|
||||||
"SizeFit": false,
|
"SizeFit": false,
|
||||||
"Text": "피해 10",
|
"Text": "방어도 5",
|
||||||
"UseOutLine": true,
|
"UseOutLine": true,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user