Merge pull request 'feat(D): 카드/적 데이터 외부화 (data/*.json)' (#11) from feature/data-externalization into main

Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
2026-06-09 01:29:03 +09:00
6 changed files with 529 additions and 46 deletions

8
data/cards.json Normal file
View 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
View 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"
}

View File

@@ -0,0 +1,341 @@
# 카드/적 데이터 외부화 (TODO D) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 카드·적 데이터를 `data/cards.json`·`data/enemies.json`로 분리하고, `gen-slaydeck.mjs`가 읽어 codeblock·UI에 주입한다(데이터만 바꿔 재생성하면 반영).
**Architecture:** 신규 JSON 2개가 데이터 단일 소스. 생성기는 상단에서 JSON을 로드·검증하고, Lua 직렬화 헬퍼로 `self.Cards`/`self.DrawPile`/적 상태를 만들어 `StartCombat`에 주입한다. DeckHud 카드 미리보기·CombatHud 초기 텍스트도 동일 데이터에서 파생.
**Tech Stack:** Node.js ESM 생성기, JSON 데이터, MSW Lua codeblock/UI JSON. 검증은 `node --check`+재생성+sha1 결정성+데이터변경 반영 확인+메이커 Play.
---
## File Structure
- Create: `data/cards.json` — 카드 정의(`cards`) + 시작 덱(`starterDeck`).
- Create: `data/enemies.json` — 적 정의(`enemies`) + 활성 적(`activeEnemy`).
- Modify: `tools/gen-slaydeck.mjs` — JSON 로드·검증·Lua 직렬화 헬퍼, `StartCombat`/`upsertUi`/속성 데이터화.
검증 한계: MSW Lua 단위 테스트 러너 없음 → 자동 검증은 생성기 문법·재생성·결정성·데이터 반영·JSON 유효성. 실제 동작은 메이커 Play(사용자).
---
### Task 1: 데이터 파일 생성
**Files:**
- Create: `data/cards.json`
- Create: `data/enemies.json`
- [ ] **Step 1: `data/cards.json` 작성**
```json
{
"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"]
}
```
- [ ] **Step 2: `data/enemies.json` 작성**
```json
{
"enemies": {
"slime": {
"name": "슬라임",
"maxHp": 45,
"intents": [
{ "kind": "Attack", "value": 10 },
{ "kind": "Attack", "value": 6 },
{ "kind": "Defend", "value": 8 }
]
}
},
"activeEnemy": "slime"
}
```
- [ ] **Step 3: JSON 유효성 확인**
Run: `node -e "JSON.parse(require('fs').readFileSync('data/cards.json','utf8')); JSON.parse(require('fs').readFileSync('data/enemies.json','utf8')); console.log('JSON OK')"`
Expected: `JSON OK`
- [ ] **Step 4: 커밋**
```bash
git add data/cards.json data/enemies.json
git commit -m "data(D): 카드/적 데이터 JSON 외부화 파일 추가"
```
---
### Task 2: 생성기에 JSON 로드·검증·Lua 직렬화 헬퍼 추가
**Files:**
- Modify: `tools/gen-slaydeck.mjs` (상단 import 직후)
- [ ] **Step 1: 파일 상단 `import { readFileSync, writeFileSync } from 'node:fs';` 바로 다음에 추가**
```js
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 '';
}
```
- [ ] **Step 2: 문법 검사**
Run: `node --check tools/gen-slaydeck.mjs`
Expected: 오류 없음
- [ ] **Step 3: 커밋**
```bash
git add tools/gen-slaydeck.mjs
git commit -m "gen-slaydeck(D): JSON 로드·검증·Lua 직렬화 헬퍼 추가"
```
---
### Task 3: StartCombat·EnemyMaxHp 속성을 데이터에서 생성
**Files:**
- Modify: `tools/gen-slaydeck.mjs` (`prop('number', 'EnemyMaxHp', ...)`, `method('StartCombat', ...)`)
- [ ] **Step 1: EnemyMaxHp 속성 기본값을 데이터로**
`prop('number', 'EnemyMaxHp', '45'),` 를 아래로 교체:
```js
prop('number', 'EnemyMaxHp', String(ACTIVE_ENEMY.maxHp)),
```
- [ ] **Step 2: `StartCombat` 메서드 본문을 데이터 주입형으로 교체**
기존 `method('StartCombat', \`...\`)` 호출 전체(아래 "현재" 블록)를 "신규"로 교체.
현재(교체 대상):
```
self.MaxEnergy = 3
self.Turn = 0
self.PlayerMaxHp = 80
self.PlayerHp = self.PlayerMaxHp
self.PlayerBlock = 0
self.EnemyName = "슬라임"
self.EnemyMaxHp = 45
self.EnemyHp = self.EnemyMaxHp
self.EnemyBlock = 0
self.EnemyIntents = {
{ kind = "Attack", value = 10 },
{ kind = "Attack", value = 6 },
{ kind = "Defend", value = 8 },
}
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" }
self:Shuffle(self.DrawPile)
self:BindButtons()
self:RenderCombat()
self:StartPlayerTurn()
```
신규 — `method('StartCombat', ...)`의 코드 인자를 템플릿으로 생성:
```js
method('StartCombat', `self.MaxEnergy = 3
self.Turn = 0
self.PlayerMaxHp = 80
self.PlayerHp = self.PlayerMaxHp
self.PlayerBlock = 0
self.EnemyName = ${luaStr(ACTIVE_ENEMY.name)}
self.EnemyMaxHp = ${ACTIVE_ENEMY.maxHp}
self.EnemyHp = self.EnemyMaxHp
self.EnemyBlock = 0
${luaIntentsTable(ACTIVE_ENEMY.intents)}
self.EnemyIntentIndex = 1
self.CombatOver = false
self.DiscardPile = {}
self.Hand = {}
${luaCardsTable(CARDS.cards)}
${luaDeckTable(CARDS.starterDeck)}
self:Shuffle(self.DrawPile)
self:BindButtons()
self:RenderCombat()
self:StartPlayerTurn()`),
```
- [ ] **Step 3: 문법 검사**
Run: `node --check tools/gen-slaydeck.mjs`
Expected: 오류 없음
- [ ] **Step 4: 커밋**
```bash
git add tools/gen-slaydeck.mjs
git commit -m "gen-slaydeck(D): StartCombat·EnemyMaxHp를 데이터에서 생성"
```
---
### Task 4: DeckHud 카드 미리보기·CombatHud 초기 텍스트를 데이터에서 파생
**Files:**
- Modify: `tools/gen-slaydeck.mjs` (`upsertUi`의 `cards` 배열, `enemyTexts` 초기값)
- [ ] **Step 1: `upsertUi`의 카드 미리보기 배열을 데이터 파생으로 교체**
기존:
```js
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 },
];
```
교체:
```js
const cards = CARDS.starterDeck.slice(0, 5).map((id) => {
const c = CARDS.cards[id];
return { name: c.name, cost: String(c.cost), desc: c.desc, tint: c.kind === 'Attack' ? ATTACK : DEFEND };
});
```
- [ ] **Step 2: CombatHud `enemyTexts` 초기값을 데이터에서 파생**
기존:
```js
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 }],
['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 }],
];
```
교체:
```js
const enemyTexts = [
['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 }, intentText(ACTIVE_ENEMY.intents[0]), 22, true, { r: 1, g: 0.72, b: 0.5, a: 1 }],
];
```
- [ ] **Step 3: 문법 검사**
Run: `node --check tools/gen-slaydeck.mjs`
Expected: 오류 없음
- [ ] **Step 4: 커밋**
```bash
git add tools/gen-slaydeck.mjs
git commit -m "gen-slaydeck(D): 카드 미리보기·CombatHud 초기 텍스트 데이터 파생"
```
---
### Task 5: 재생성 + 검증
**Files:** 생성물 3종 (생성기 실행 결과)
- [ ] **Step 1: 생성기 실행**
Run: `node tools/gen-slaydeck.mjs`
Expected: `Slay deck UI and combat codeblocks generated.`
- [ ] **Step 2: 생성물이 B와 동치인지 — codeblock에 데이터 값이 반영됐는지 확인**
Run: `node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); const sc=j.ContentProto.Json.Methods.find(m=>m.Name==='StartCombat').Code; console.log(/Strike = { name = \"타격\".*damage = 6/.test(sc) && /슬라임/.test(sc) && /value = 10/.test(sc) ? 'DATA INJECTED OK' : 'MISMATCH')"`
Expected: `DATA INJECTED OK`
- [ ] **Step 3: 결정성 확인**
Run: `node tools/gen-slaydeck.mjs >/dev/null && sha1sum ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock > /tmp/a.sha && node tools/gen-slaydeck.mjs >/dev/null && sha1sum ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock > /tmp/b.sha && diff /tmp/a.sha /tmp/b.sha && echo DETERMINISTIC`
Expected: `DETERMINISTIC`
- [ ] **Step 4: 데이터 변경이 반영되는지 확인 (D의 핵심 검증)**
Run: `node -e "const fs=require('fs'); const f='data/cards.json'; const o=JSON.parse(fs.readFileSync(f,'utf8')); o.cards.Strike.damage=9; o.cards.Strike.desc='피해 9'; fs.writeFileSync(f, JSON.stringify(o,null,2));" && node tools/gen-slaydeck.mjs >/dev/null && node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); const sc=j.ContentProto.Json.Methods.find(m=>m.Name==='StartCombat').Code; console.log(/Strike = { name = \"타격\", cost = 1, desc = \"피해 9\", kind = \"Attack\", damage = 9/.test(sc) ? 'CHANGE REFLECTED' : 'NOT REFLECTED')"`
Expected: `CHANGE REFLECTED`
- [ ] **Step 5: 변경 되돌리고 재생성 (원복)**
Run: `git checkout -- data/cards.json && node tools/gen-slaydeck.mjs >/dev/null && echo reverted`
Expected: `reverted`
- [ ] **Step 6: 잘못된 데이터 fail-fast 확인**
Run: `node -e "const fs=require('fs'); const o=JSON.parse(fs.readFileSync('data/enemies.json','utf8')); o.activeEnemy='nope'; fs.writeFileSync('/tmp/bad-enemies.json', JSON.stringify(o));" && cp data/enemies.json /tmp/enemies.bak && cp /tmp/bad-enemies.json data/enemies.json; node tools/gen-slaydeck.mjs; echo "exit=$?"; cp /tmp/enemies.bak data/enemies.json`
Expected: 에러 메시지 `activeEnemy가 enemies에 없음: nope` + `exit=1`, 이후 원복
- [ ] **Step 7: 최종 재생성 + git status 확인**
Run: `node tools/gen-slaydeck.mjs >/dev/null; git checkout -- Global/common.gamelogic 2>/dev/null; git status --short`
Expected: `data/*.json`, `tools/gen-slaydeck.mjs`, `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock`만 변경(내용 동일한 common 제외).
- [ ] **Step 8: 생성물 커밋**
```bash
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
git commit -m "재생성(D): 데이터 기반 카드/적 주입 반영"
```
- [ ] **Step 9: 메이커 Play 수동 검증 (사용자)**
메이커 reload→Play: 기존 B 동작과 동일(데이터 동치라 회귀 없음). 적 슬라임 HP 45·의도 공격10, 카드 3종 효과 정상.
---
## Self-Review
- **Spec coverage:** cards.json/enemies.json 생성(Task1), 로드·검증·직렬화(Task2), StartCombat·속성 데이터화(Task3), UI 파생(Task4), 검증·데이터변경 반영(Task5). 스펙 전 항목 매핑됨.
- **Placeholder scan:** 모든 단계 실제 코드/명령 포함. "TODO(E)"류 미래 훅은 본 작업 범위 아님.
- **Type consistency:** `luaStr`/`luaCardsTable`/`luaDeckTable`/`luaIntentsTable`/`intentText`/`ACTIVE_ENEMY`/`CARDS`/`ENEMIES` 명칭이 정의부(Task2)와 사용부(Task3·4)에서 일치. 카드 필드(`name/cost/kind/damage/block/desc`)가 데이터(Task1)·직렬화(Task2)·검증(Task5)에서 일치.

View File

@@ -0,0 +1,89 @@
# 카드/적 데이터 외부화 (TODO 항목 D) — 설계
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO.md 항목 D + gen-slaydeck.mjs 분석.
> 선행: B(전투 통합)·A(문서 정합) 완료. F(밸런스 시뮬레이터)의 선행 조건.
## 문제
카드 정의(`self.Cards`)·시작 덱·적 정의(이름/HP/의도)가 `gen-slaydeck.mjs``StartCombat`
Lua 문자열에 하드코딩돼 있다. 카드/적 추가·밸런싱이 생성기 코드 수정을 요구한다.
## 목표
카드·적 데이터를 외부 JSON으로 분리하고, 생성기가 읽어 codeblock·UI에 주입한다.
데이터만 바꿔 재생성하면 게임에 반영(코드 수정 없이).
## 향후 방향 (참고)
추후 카드·적 공격은 **메이플스토리 IP**에 맞춰 디벨롭 예정. 본 스키마는 명시적 `desc`
키 기반 확장으로 이를 수용한다(새 카드/적은 JSON 항목 추가로 확장). 본 작업은 현 3종+적1
기준 **최소 스키마**까지만 — 새 효과 필드(상태이상/드로우 등)는 추가하지 않는다(YAGNI).
## 단일 소스 원칙
생성물(`SlayDeckController.codeblock` · `ui/DefaultGroup.ui` · `common.gamelogic`)은
`gen-slaydeck.mjs`가 생성한다. D 이후 **데이터의 단일 소스는 `data/*.json`**, 생성 로직의
단일 소스는 `gen-slaydeck.mjs`. 결정적 출력 유지.
## 설계
### 신규 파일
**`data/cards.json`**
```json
{
"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"]
}
```
**`data/enemies.json`**
```json
{
"enemies": {
"slime": {
"name": "슬라임", "maxHp": 45,
"intents": [
{ "kind": "Attack", "value": 10 },
{ "kind": "Attack", "value": 6 },
{ "kind": "Defend", "value": 8 }
]
}
},
"activeEnemy": "slime"
}
```
- `desc`는 명시적(작성자 작성). `kind``"Attack"` 또는 `"Skill"`. 효과는 `damage`/`block`.
- `activeEnemy`로 현재 단일 전투의 적을 데이터에서 지정. 향후 E(맵 노드)에서 노드별 선택으로 확장.
### 생성기(`gen-slaydeck.mjs`) 변경
1. 상단에서 `readFileSync`+`JSON.parse``data/cards.json`·`data/enemies.json` 로드.
2. **검증(fail-fast)**: `starterDeck`의 모든 id가 `cards`에 존재해야 함; `activeEnemy`
`enemies`에 존재해야 함. 아니면 명확한 에러로 `process.exit(1)`(또는 throw).
3. `writeCodeblocks()``StartCombat`에서:
- `self.Cards = {...}``cards`에서 생성(Lua 테이블 직렬화 헬퍼).
- `self.DrawPile = {...}``starterDeck`에서 생성.
- `self.EnemyName`/`EnemyMaxHp`/`EnemyIntents`/`EnemyIntentIndex``enemies[activeEnemy]`에서 생성.
- codeblock 속성 `EnemyMaxHp` DefaultValue도 데이터 값으로.
4. `upsertUi()`의 DeckHud 카드 미리보기 배열·CombatHud 초기 텍스트(적 이름·`HP n/n`·첫 의도)를
동일 데이터에서 파생.
5. Lua 문자열 직렬화 시 한글/따옴표 이스케이프 주의(데이터 값은 따옴표·역슬래시 없는 단순 문자열 가정,
필요 시 escape 헬퍼).
### 데이터 흐름
`data/*.json``gen-slaydeck.mjs`(로드·검증·직렬화) → `SlayDeckController.codeblock`(Lua 테이블)
+ `ui/DefaultGroup.ui`(초기 텍스트) → 메이커 런타임.
## 검증
- `node tools/gen-slaydeck.mjs` 정상; JSON 유효; 2회 실행 결정적.
- `data/cards.json`에서 카드 1장 수치만 변경 → 재생성 → codeblock의 해당 카드 수치 변경
(생성기/codeblock 직접 수정 없이).
- 잘못된 데이터(starterDeck에 없는 id, 잘못된 activeEnemy) → 생성기가 명확히 실패.
- 메이커 Play: 기존 B 동작과 동일(데이터 동치이므로 회귀 없음).
## 범위 밖 (금지)
- 새 효과 필드(상태이상·드로우·복합효과) 추가. 새 카드 종류 대량 추가. F(시뮬레이터)·E(메타).

View File

@@ -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()

View File

@@ -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
}