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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user