Compare commits
8 Commits
9fd4b2d2e3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| da0d74f841 | |||
| 7f30803862 | |||
| 2fdd535939 | |||
| 1100cbeb08 | |||
| e14f19e4ed | |||
| 1a10444136 | |||
| 4d8fa0f40f | |||
| e8ea5e249d |
38
README.md
38
README.md
@@ -44,11 +44,13 @@ git pull
|
||||
```
|
||||
slaymaple/
|
||||
├── data/ # 게임 데이터 단일 소스 (생성기가 읽어 주입). 맵은 정적 데이터 없음(절차 생성)
|
||||
│ ├── cards.json # 카드 121장(클래스·2차전직별 + 저주) + 클래스별 시작 덱
|
||||
│ ├── cards.json # 카드 166장(1~3차 전직 계열별 + 저주) + 클래스별 시작 덱
|
||||
│ ├── enemies.json # 적 18종(일반/정예/보스, 디버프 인텐트 포함)
|
||||
│ ├── potions.json # 물약 6종 + 드랍률·슬롯·상점가
|
||||
│ ├── relics.json # 유물 19종(StS 효과 × 메이플 장비) + 시작 유물 + 풀
|
||||
│ ├── cardframes.json # 커스텀 카드 프레임 3종(전사/마법사/도적 × normal/unique/legend) + 보상 등급 가중치
|
||||
│ ├── characters.json # 클래스별 초상화 RUID
|
||||
│ ├── cards.xlsx # cards.json 왕복 편집용 엑셀(excel_to_cards.bat / cards_to_excel.bat)
|
||||
│ └── camera.json # 맵별 카메라 설정값(줌·오프셋·고정 영역)
|
||||
├── Global/ # 월드 전역 설정 · 공용 모델 · 게임로직
|
||||
│ ├── common.gamelogic # SlayDeckController 부착 지점 (산출물)
|
||||
@@ -71,13 +73,13 @@ slaymaple/
|
||||
│ ├── lobby.map # 로비 허브 맵 (마을 배경, NPC 4종, 전투 없음)
|
||||
│ └── map01.map ~ map05.map # 5막 전투/맵 노드 (공식 배경 + STS풍 우측 배치)
|
||||
├── tools/ # 결정적 생성기·도구 (주체별 폴더, 단일 소스)
|
||||
│ ├── deck/ # gen-slaydeck.mjs(★게임 전체 생성: 카드/덱·전투·맵노드·상점·유물·로비·메뉴 UI + SlayDeckController + common) · gen-cardhand.mjs
|
||||
│ ├── deck/ # gen-slaydeck.mjs(★컨트롤러+common 생성 오케스트레이터) · cb/(codeblock Lua 메서드 20모듈: boot·screens·combat·hand·npc·navigation·layout·shop·reward·soul 등) · lib/(공유 상수·데이터·헬퍼) · legacy/(옛 UI emit 휴면)
|
||||
│ ├── map/ # gen-maps.mjs(맵 배경/타일) · gen-lobby-map.mjs(로비 맵+NPC) · gen-map-encounters.mjs(노드별 몬스터 그룹) · rogue-map.mjs(절차 생성 JS 미러)+test
|
||||
│ ├── camera/ # gen-camera.mjs(맵별 고정 카메라 codeblock)
|
||||
│ ├── player/ # gen-player-lock.mjs(전투맵 입력 잠금) · freeze-turn-player.mjs(모델 이동 정지) · gen-lobby-npc.mjs(LobbyNpc·LobbyMobility codeblock)
|
||||
│ ├── monster/ # gen-combat-monster.mjs(EnemyId 마커) · freeze-turn-monsters.mjs(필드 AI 정지)
|
||||
│ ├── balance/ # sim-balance.mjs(전투 밸런스 몬테카를로 시뮬) · sim-balance.test.mjs
|
||||
│ ├── verify/ # count.mjs·uimap.mjs·cbgap.mjs(산출물 카운트/UIGroup 매핑/재연결 GAP 검증 — 경로 내장)
|
||||
│ ├── verify/ # count·uimap·cbgap(카운트/UIGroup 매핑/재연결 GAP) · cardkinds(카드 kind↔효과) · cbprops(미선언 self 대입) · cbset(메서드 집합 무손실) · diffcheck(바이트동일)
|
||||
│ └── git/ # gitea-pr.mjs(UTF-8 안전 PR 생성/수정/머지 — RULES.md 참조)
|
||||
├── ui/ # UIGroup 7종 — 메이커 저작(Default/Select/Lobby/Run/Deck/Popup/Toast)
|
||||
├── docs/
|
||||
@@ -98,9 +100,9 @@ slaymaple/
|
||||
|
||||
3직업 모두 Slay the Spire 2 차용 + 메이플 IP 재해석. 카드 덱 상세 설계는 [`docs/deck-concept.md`](docs/deck-concept.md) 참조.
|
||||
|
||||
- **⚔️ 전사 (탱커, Ironclad 차용)** — **파이터**: 공격을 *연속*으로 내면 콤보가 쌓이고(방어·파워 등 비공격 카드를 쓰면 콤보 리셋) 콤보로 데미지 증가 버프 = 브루저. **페이지**: 위협 디버프로 버티며 방어도 축적 → **바디 슬램(방어 비례 피해)** 카운터. **스피어맨**: 하이퍼바디·아이언월 유지/리치형.
|
||||
- **🗡️ 도적 (단검·독, Silent 차용)** — 표창 난사 / 독 / 교활·버림. **어쌔신**(표창·크리·흡혈) / **시프**(단검 난타·독). *형 구현 완료(Silent 86장)*.
|
||||
- **🔮 법사 (약체·게이지, Defect 차용)** — **위자드(불/독)**: 독을 묻히고 *독 걸린 적에 불 카드 → 추가 데미지*(독뎀 시너지). **위자드(썬/콜)**: 오브로 썬더(다중 공격)·콜드(빙결=취약+피해), 오브 획득·다중 소모 운용. **클레릭**: 오브 없이 회복·버프 + 언데드엔 힐로 공격하는 보조 힐러.
|
||||
- **⚔️ 전사 (탱커, Ironclad 차용, HP80)** — 2차 3종. **파이터**: 공격을 *연속*으로 내면 콤보가 쌓이고(비공격 카드 시 리셋) 콤보로 데미지 증가 = 브루저(콤보 어택·버서크·라이징 어택). **페이지**: 썬더/블리자드 **속성 차지** + 파워 가드. **스피어맨**: 피어스·아이언 월·하이퍼 바디 유지/관통형.
|
||||
- **🗡️ 도적 (단검·독, Silent 차용, HP70)** — 표창 난사 / 독 / 교활·버림. **2차 어쌔신**(표창·독 압박·빠른 마무리)·**시프**(단검·드로우·연계) → **3차 헤르밋**(어쌔신 심화)·**시프 마스터**(시프 심화). 도적 계열만 132장(Silent 완역 포트 + 공식 스킬 아이콘).
|
||||
- **🔮 법사 (약체·게이지, Defect 차용, HP70)** — 2차 3종. **위자드(불·독)**: 독을 묻히고 *독 걸린 적에 불 카드 → 추가 데미지*(독뎀 시너지). **위자드(썬·콜)**: 오브로 썬더(다중 공격)·콜드(빙결=취약+피해), 오브 획득·다중 소모 운용. **클레릭**: 오브 없이 회복·버프 + 언데드엔 힐로 공격하는 보조 힐러.
|
||||
|
||||
## 게임 프레임워크 현황
|
||||
|
||||
@@ -114,13 +116,13 @@ slaymaple/
|
||||
|
||||
게임 전체는 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작합니다. **UI는 메이커 저작**(7개 UIGroup: Default/Select/Lobby/Run/Deck/Popup/Toast)이고, 컨트롤러가 엔티티 경로(`/ui/<UIGroup>/<Hud>/...`)로 내용을 런타임 주입합니다. 생성기 `tools/deck/gen-slaydeck.mjs`는 **`SlayDeckController.codeblock` + `common.gamelogic`만 생성**(`.ui` 미접근, 결정적 출력 — `RULES.md` 참조). 게임 데이터는 **`data/*.json`**, 맵 구조는 **런타임 절차 생성**(`GenerateMap` Lua ↔ `tools/map/rogue-map.mjs` JS 미러).
|
||||
|
||||
### 구현된 기능 (배포 퀄리티 P1~P15+, PR #34~#79)
|
||||
### 구현된 기능 (배포 퀄리티 P1~P15+, PR #34~#104)
|
||||
|
||||
| 영역 | 내용 |
|
||||
|---|---|
|
||||
| **로비 마을** | 전용 물리 맵 `lobby.map`(마을 배경). **NPC 4종 월드 엔티티** — 모험가(런 시작)·사서(카드 도감)·상인(영혼 상점)·안내원(게시판). 근접 시 머리 위 마크 + `↑`키 **또는 직접 클릭**으로 상호작용. **이동·공격 모션은 로비 맵에서만** 풀림(전투맵은 잠금), 카메라는 로비에서 **플레이어 추종**(전투맵은 고정) |
|
||||
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택(**초상화·직업 설명·선택 테두리 강조** 캐릭터 선택 UI), 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**2차 전직**] — 각 클래스 3종(전사→파이터/페이지/스피어맨, 법사→위자드불독/위자드썬콜/클레릭, 도적→Shiv/Poison/Trickster). 전용 카드는 해당 클래스 풀만 획득 |
|
||||
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬=위로 스윕). 카드 **121장** — kind **Attack/Skill/Power/Status**. 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
|
||||
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택(**초상화·직업 설명·선택 테두리 강조** 캐릭터 선택 UI), 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**전직**] — 전사→파이터/페이지/스피어맨, 법사→위자드(불·독)/위자드(썬·콜)/클레릭 (2차 3종씩), **도적→어쌔신·시프(2차) → 헤르밋·시프 마스터(3차)**. 전직 시 대표 카드 지급, 전용 카드는 해당 계열 풀만 획득 |
|
||||
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬/파워=위로 스윕). 카드 **166장** — kind **Attack(59)/Skill(74)/Power(31)/Status(2)**. kind↔효과 정합성 정적 검증(`cardkinds.mjs`). 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
|
||||
| **도적 카드 공용 효과** | 카드 효과를 **카드명 하드코딩 대신 `data/cards.json` 공용 필드**로 표현(재사용). **불가침**·**x-cost**(에너지 비례 피해/약화)·드로우 수 비례 데미지·**다음 스킬 반복**·**처치 보상/반복**·카드 설명 **키워드 하이라이트**·드로우 연동(`drawSkillBlock`·`drawPoison`)·독 버스트·랜덤 타깃 등. **Lua + JS 미러 양쪽 구현**. 필드 사전 [`docs/card-effect-fields.md`](docs/card-effect-fields.md) |
|
||||
| **버프/디버프** | StS 표준 — **힘**(+N 영구)·**약화**(주는 피해 −25%)·**취약**(받는 피해 +50%)·**독**(매 행동 틱). 양방향(적 디버프 인텐트 포함), 인텐트는 최종 예상치 표시 |
|
||||
| **전투 연출** | 공격 이펙트·**몬스터 데미지 팝업(자릿수 스킨)**·드래그 타깃 마커·적 개별 차례·**공격/피격/독뎀 모션**(아바타 상태 전이·몬스터 hit 클립·런지/넉백) |
|
||||
@@ -131,10 +133,10 @@ slaymaple/
|
||||
| **승천(Ascension)** | A1~A10 누적 모디파이어(적 강화·시작 HP 감소·보상 감소). UserDataStorage 유저별 영구 저장, 런 클리어 시 다음 단계 해금 |
|
||||
| **멀티 act** | **5막** 진행(보스 클리어→다음 막 텔레포트, 맵·인카운터 변경, 적 스케일 `1+(막-1)*0.45`), 5막 클리어 시 런 종료 |
|
||||
| **경제** | 화폐 표기 **메소**(코인 아이콘), 카드/유물/물약 메소 가격. 내부 식별자는 Gold 유지 |
|
||||
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트(현 84종) |
|
||||
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트(현 97종) |
|
||||
|
||||
> ⚠️ 수치(적 스탯·경제·승천 배율)는 1차 조정 상태입니다. 정밀 밸런싱은 `sim-balance.mjs`로 검증하며 진행합니다.
|
||||
> ℹ️ 도적(Silent) 카드 86장은 STS Silent 완역 포트 + **공식 스킬 아이콘 적용 완료**. 남은 작업은 카드명 메이플 재서사(어쌔신/시프)·멀티플레이어 전제 카드 싱글 정리 — [`docs/deck-concept.md`](docs/deck-concept.md) 참조.
|
||||
> ℹ️ 도적 계열 카드 132장은 STS Silent 완역 포트 + **공식 스킬 아이콘 적용 완료**, rogue 1차 + 어쌔신/시프(2차) + 헤르밋/시프 마스터(3차)로 재편. 남은 작업은 카드명 메이플 재서사·멀티플레이어 전제 카드 싱글 정리 — [`docs/deck-concept.md`](docs/deck-concept.md)·[`docs/bandit-card-audit.md`](docs/bandit-card-audit.md) 참조.
|
||||
|
||||
### 유용한 스크립트 호출
|
||||
`/common` 엔티티(또는 Play Test 컨텍스트)에서:
|
||||
@@ -144,14 +146,14 @@ local c = _EntityService:GetEntityByPath("/common").SlayDeckController
|
||||
c:OnLobbyNpcInteract("run") -- 모험가(런 시작) / "codex"(도감) / "shop"(영혼상점) / "board"(게시판)
|
||||
c:ShowLobby() -- 로비 맵 복귀 + 상태 초기화
|
||||
-- 런
|
||||
c:SelectClass("warrior") -- "warrior" / "bandit" / "magician"
|
||||
c:SelectClass("warrior") -- "warrior" / "rogue" / "magician"
|
||||
c:StartNewGame() -- 캐릭터 선택 → 런 시작(map01 텔레포트)
|
||||
c:PickNode("r1c2") -- 맵 노드 선택(절차 생성 그리드 id) / "boss"
|
||||
c:PlayCard(1) -- 손패 slot 카드 사용
|
||||
c:EndPlayerTurn() -- 턴 종료 → 적 턴 → 다음 턴
|
||||
c:PickReward(1) -- 보상 카드 1택(0=건너뛰기)
|
||||
c:BuyCard(1) / c:BuyRelic() / c:BuyPotion() -- 상점 구매(메소)
|
||||
c:SetJob("fighter") -- 전직 (보스 보상 선택 화면)
|
||||
c:SetJob("fighter") -- 전직 (보스 보상 화면) — 2차: fighter/page/spearman·firepoison/icelightning/cleric·assassin/thief, 3차: hermit/thiefmaster
|
||||
c:AdjustAscension(1) -- 메뉴에서 승천 단계 +1
|
||||
```
|
||||
|
||||
@@ -179,7 +181,7 @@ node tools/camera/gen-camera.mjs # 맵별 카메라
|
||||
node tools/player/gen-player-lock.mjs # 전투맵 입력 잠금
|
||||
node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
|
||||
```
|
||||
> 산출물 검증은 내용 출력 없이 카운트만: `node tools/verify/count.mjs <ui|cb|common> <regex>...` (자세한 가드는 [`RULES.md`](RULES.md)).
|
||||
> 산출물 검증은 내용 출력 없이 카운트만: `node tools/verify/count.mjs <ui|cb|common> <regex>...`. 정적 가드 — 카드 kind↔효과 `cardkinds.mjs` · 미선언 self 대입 `cbprops.mjs` · UI 경로 재연결 GAP `cbgap.mjs` · 리팩터 바이트동일 `diffcheck.mjs` (자세한 가드는 [`RULES.md`](RULES.md)).
|
||||
|
||||
---
|
||||
|
||||
@@ -188,6 +190,7 @@ node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
|
||||
현재 게임 전체 로직이 `SlayDeckController` 단일 codeblock에 모여 있습니다. 초기 설계의 3분할(`SlayCardCatalog`/`SlayRunState`/`SlayCombatManager`)은 **기능적으로 모두 구현**됐으나 아직 한 컴포넌트 안에 있습니다. 맵 NPC·카메라·입력 잠금 등 **맵 단위 동작은 별도 codeblock**(LobbyNpc/LobbyMobility/MapCamera/PlayerLock/CombatMonster)으로 분리해 각 맵 루트/엔티티에 부착합니다. 카드/적/맵/유물/프레임/카메라 데이터는 `data/*.json`로 외부화돼 있습니다. **2026-06-17**: UI를 단일 `DefaultGroup`에서 7개 UIGroup(Select/Lobby/Run/Deck 등)으로 분리해 **메이커 저작으로 전환** — 생성기는 더 이상 `.ui`를 만들지 않고, 컨트롤러가 새 UIGroup 경로로 재연결됨(옛 UI emit `hud/*`·`gen-cardhand`는 `tools/deck/legacy/` 휴면). 재연결 무결성은 `tools/verify/cbgap.mjs`(GAP 0)로 검증.
|
||||
|
||||
> ⚠️ **전투 규칙과 맵 생성은 Lua(gen-slaydeck 내장)와 JS 미러(sim-balance/rogue-map)로 이중 구현**입니다. 한쪽을 고치면 반드시 다른 쪽도 동기화하고 테스트하세요(`RULES.md` §6).
|
||||
> ⚠️ **카드 `kind`는 효과와 반드시 일치**해야 합니다 — 데미지=`Attack`, 방어/유틸=`Skill`, 지속효과=`Power`. 안 맞으면 런타임 에러 없이 *사용 불가/무효과 死카드*가 됩니다(2026-06-30 Defend·Rage 사고). 새 효과 필드는 `docs/card-effect-fields.md` 등록 + Lua/JS 양쪽 핸들러 구현. 정적 검증 `node tools/verify/cardkinds.mjs`(`RULES.md` §9). cb Lua 지역변수는 의미명 사용(`RULES.md` §8).
|
||||
|
||||
---
|
||||
|
||||
@@ -195,11 +198,14 @@ node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
|
||||
- [x] 전투 루프 · 런 루프 · 절차 생성 맵 · 상점/휴식/유물 방 · 유물 19종 · 물약 · 버프/디버프 · Power · 전직(전사/법사/도적 2차) · 승천+개인 저장 · 전투 모션 · 커스텀 프레임 · **반복 런·로비 맵·NPC·영혼·메소·카메라 추종 (P1~P15 완료)**
|
||||
- [x] **UI 메이커-저작 전환** — 단일 DefaultGroup → 7개 UIGroup 분리, 생성기 UI 저작 폐기(`tools/deck/legacy/`), 컨트롤러 경로 재연결(cbgap GAP 0) (2026-06-17)
|
||||
- [x] **시작 로비 직행 · 캐릭터 선택 UI · 디버그 치트 · map01 로스터 (2026-06-18)** — 게임 시작 시 MainMenu 없이 곧장 로비 진입(MainMenu는 추후 싱글/멀티/종료 메뉴로 재지정); 캐릭터 선택 화면 초상화·직업 설명·선택 테두리·Art 클리핑(MaskComponent) 배선; 디버그 단축키 Ctrl+Shift+C(카드 picker)·Ctrl+Shift+E(체력+에너지 전체 회복); map01 몬스터 18종 로스터(랜덤 행동)
|
||||
- [ ] **도적 카드명 재서사·설명 한글화** — Silent 직역 카드명을 어쌔신/시프 메이플 스킬명으로 재서사(아이콘은 적용 완료), 2차 전직 설명 한글화
|
||||
- [x] **컨트롤러 관심사별 모듈 분리 · 코드 규칙 (2026-06-26, #94)** — SlayDeckController를 `cb/*.mjs` 20모듈로 분리(런타임은 단일 codeblock 유지), 변수명 의미화, 검증 `cbset.mjs`(집합 무손실)·`cbprops.mjs`(미선언 self)
|
||||
- [x] **도적 계열 대개편 + 3차 전직 · 카드 공용 효과 (2026-06-23~30, #82~#99)** — Silent 포트를 rogue 1차 + 어쌔신/시프(2차) + 헤르밋/시프 마스터(3차)로 재편, 카드 효과를 카드명 하드코딩 대신 `cards.json` 공용 필드로(`docs/card-effect-fields.md`), 카드 **166장**
|
||||
- [x] **코드리뷰 버그수정 + kind↔효과 규칙 (2026-06-29~30, #96·#102)** — 게임버그 6·시뮬 충실도 3·설명 2 수정(Defend kind Attack→Skill·Rage Power→Attack 포함), kind↔효과 정적 검증 `cardkinds.mjs`, 카드 왕복 편집 엑셀(#93)
|
||||
- [ ] **도적 카드명 재서사·설명 한글화** — Silent 직역 카드명을 어쌔신/시프 메이플 스킬명으로 재서사(아이콘은 적용 완료), 2·3차 전직 설명 한글화
|
||||
- [ ] **런 이어하기** — 진행 중 런 직렬화 저장(UserDataStorage 확장, 메뉴 "이어하기" 활성화)
|
||||
- [ ] **카드 제거/업그레이드** — 상점 카드 제거 슬롯, 휴식 노드에서 카드 강화
|
||||
- [ ] **이벤트 노드(?)** — 랜덤 텍스트 이벤트(선택지·리스크/리워드)
|
||||
- [ ] **3차 전직** — 후반 막 보상으로 확장
|
||||
- [ ] **3차 전직 — 전사·법사 확장** (도적은 완료: 헤르밋·시프 마스터), 후반 막 보상으로
|
||||
- [ ] **궁수 등 추가 클래스** — 캐릭터 선택 슬롯 확장
|
||||
- [ ] **정밀 밸런싱** — 첫 인카운터 승률 완화·직업별 카드 효율 튜닝(`sim-balance.mjs` 리포트 기반)
|
||||
- [ ] **상점 보장 규칙** — 막당 상점 최소 1회 등장
|
||||
|
||||
9
RULES.md
9
RULES.md
@@ -94,3 +94,12 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
- cb(`tools/deck/cb/*.mjs`)의 Lua 지역변수는 **의미가 드러나는 이름**으로 작성한다(`e`→`entity`, `n`→`count`, `m`→`monster`, `lp`→`localPlayer`, `s`→`soulPoints`, `tr`→`transform`). `a`/`b`/`c` 같은 무의미 단일문자 변수는 금지.
|
||||
- 단, 순수 반복 인덱스 `i`/`j`/`r`/`c`는 관용상 허용한다.
|
||||
- 새 cb 메서드를 작성하거나 기존 메서드를 손댈 때 이 규칙을 적용한다(대규모 일괄 개명은 별도 작업으로).
|
||||
|
||||
## 9. 카드 데이터 규칙 (kind ↔ 효과 일치)
|
||||
|
||||
새 카드를 추가/수정할 때 `data/cards.json`의 `kind`는 카드의 효과·사용 메커니즘과 **반드시 일치**해야 한다. 안 맞으면 카드가 **사용 불가**거나 **재생 시 아무 효과 없는 死카드**가 된다(런타임 에러도 안 나고 sim 테스트도 못 잡음 — 정적 검증 필수).
|
||||
|
||||
- **`ResolveCardDrop` 사용 라우팅이 kind별로 다름**: `Attack`=몬스터 위에 드롭(`FindMonsterAtTouch>0` 필요)·`Skill`/`Power`=위로 스윕(`ui.y>-180`)·`Status`=unplayable. → **block·디버프·드로우 등 유틸만 있고 데미지가 없는 카드를 `Attack`으로 두면 위로 스윕으로 사용할 수 없다**(2026-06-30 아이언 바디 사고: block만 있는 방어카드가 Attack이라 전사 시작덱 4장이 먹통 → Skill로 수정).
|
||||
- **`PlayCard`의 `Power` 분기는 PlayerPowers 등록만 하고 `damage`/`aoe`를 무시**한다. → 데미지 카드=`Attack`, 방어/유틸=`Skill`, 지속효과=`Power`(단 `powerEffect` 또는 지속/온플레이 power 필드 — `turnStart*`·`dex`·`thorns`·`intangible`·`attackPoison`·`drawDamage`·`shivX`·`cardPlayed*` 등 — 이 있어야 함). Power인데 power 효과 필드가 없으면 死카드(2026-06-30 분노 사고: `damage:4/aoe`만 있어 Power 분기서 무시됨 → kind Power→Attack으로 기능화).
|
||||
- 새 효과 필드는 `docs/card-effect-fields.md` 사전에 등록하고 Lua(`tools/deck/cb/*.mjs`) + JS 미러(`tools/balance/sim-balance.mjs`) **양쪽에 핸들러 구현**(§6). 한쪽만 있으면 게임↔시뮬 드리프트.
|
||||
- **검증: `node tools/verify/cardkinds.mjs`** — kind↔효과 위반(Attack-무데미지 / Power-무효과 / 미지원 kind)을 정적 검출(이상 0 = exit 0). 카드 추가/수정 후 반드시 실행. (관련 가드: 미선언 `self.X` = `cbprops.mjs`, UI 경로 = `cbgap.mjs`, 이중구현 = `sim-balance.test.mjs`.)
|
||||
|
||||
File diff suppressed because one or more lines are too long
327
data/cards.json
327
data/cards.json
@@ -1401,9 +1401,10 @@
|
||||
"kind": "Attack",
|
||||
"class": "thief",
|
||||
"rarity": "normal",
|
||||
"desc": "피해를 4만큼 3번 줍니다.",
|
||||
"damage": 4,
|
||||
"hits": 3,
|
||||
"desc": "피해를 3만큼 2번 줍니다. 이번 턴에 사용한 공격 카드 1장당 피해가 2 증가합니다.",
|
||||
"damage": 3,
|
||||
"hits": 2,
|
||||
"damagePerAttackPlayedThisTurn": 2,
|
||||
"image": "92a5020c978c46bdabab910598118b86"
|
||||
},
|
||||
"CriticalEdge": {
|
||||
@@ -1412,9 +1413,10 @@
|
||||
"kind": "Skill",
|
||||
"class": "thief",
|
||||
"rarity": "unique",
|
||||
"desc": "카드를 1장 뽑습니다. 다음 턴에, 공격 카드의 피해량이 2배가 됩니다.",
|
||||
"desc": "카드를 1장 뽑습니다. 다음 턴에 공격 카드의 피해량이 2배가 됩니다. 보존.",
|
||||
"draw": 1,
|
||||
"nextTurnAttackMultiplier": 2,
|
||||
"retain": true,
|
||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
||||
},
|
||||
"Steal": {
|
||||
@@ -1423,8 +1425,9 @@
|
||||
"kind": "Attack",
|
||||
"class": "thief",
|
||||
"rarity": "normal",
|
||||
"desc": "피해를 5 줍니다. 에너지를 1 얻습니다.",
|
||||
"damage": 5,
|
||||
"desc": "피해를 3 줍니다. 이번 턴에 버린 카드 1장당 피해가 3 증가합니다. 에너지를 1 얻습니다.",
|
||||
"damage": 3,
|
||||
"damagePerDiscardedThisTurn": 3,
|
||||
"gainEnergy": 1,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
@@ -1434,9 +1437,11 @@
|
||||
"kind": "Skill",
|
||||
"class": "thief",
|
||||
"rarity": "normal",
|
||||
"desc": "카드를 2장 뽑습니다. 카드를 1장 버립니다.",
|
||||
"draw": 2,
|
||||
"desc": "카드를 1장 뽑습니다. 카드를 1장 버립니다. 버린 카드마다 카드를 1장 더 뽑고, 표창 1장을 손에 넣습니다.",
|
||||
"draw": 1,
|
||||
"discard": 1,
|
||||
"drawPerDiscarded": 1,
|
||||
"addShiv": 1,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
},
|
||||
"Karma": {
|
||||
@@ -1445,9 +1450,10 @@
|
||||
"kind": "Attack",
|
||||
"class": "thief",
|
||||
"rarity": "unique",
|
||||
"desc": "피해를 8 줍니다. 방어도를 무시합니다.",
|
||||
"damage": 8,
|
||||
"desc": "피해를 7 줍니다. 방어도를 무시합니다. 약화 상태의 적에게는 피해가 2배가 됩니다.",
|
||||
"damage": 7,
|
||||
"pierce": true,
|
||||
"attackDamageVsWeakMultiplier": 2,
|
||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||
},
|
||||
"DaggerMastery": {
|
||||
@@ -1456,8 +1462,9 @@
|
||||
"kind": "Power",
|
||||
"class": "thief",
|
||||
"rarity": "unique",
|
||||
"desc": "힘을 1 얻습니다.",
|
||||
"strength": 1,
|
||||
"desc": "카드를 사용할 때마다 방어도를 1 얻습니다. 매 턴 첫 카드의 피해가 3 증가합니다.",
|
||||
"cardPlayedBlock": 1,
|
||||
"firstCardDamageBonus": 3,
|
||||
"image": "49c8f279bfa64bf3954037f17da0052d"
|
||||
},
|
||||
"PhysicalTraining": {
|
||||
@@ -1466,9 +1473,10 @@
|
||||
"kind": "Skill",
|
||||
"class": "thief",
|
||||
"rarity": "normal",
|
||||
"desc": "힘을 1 얻습니다. 민첩을 1 얻습니다.",
|
||||
"desc": "힘을 1 얻습니다. 민첩을 1 얻습니다. 방어도를 4 얻습니다.",
|
||||
"strength": 1,
|
||||
"dex": 1,
|
||||
"block": 4,
|
||||
"image": "49c8f279bfa64bf3954037f17da0052d"
|
||||
},
|
||||
"ShieldMastery": {
|
||||
@@ -1477,9 +1485,9 @@
|
||||
"kind": "Skill",
|
||||
"class": "thief",
|
||||
"rarity": "normal",
|
||||
"desc": "방어도를 8 얻습니다. 다음 턴에, 방어도를 3 얻습니다.",
|
||||
"block": 8,
|
||||
"nextTurnBlock": 3,
|
||||
"desc": "방어도를 7 얻습니다. 다음 턴에 방어도가 사라지지 않습니다.",
|
||||
"block": 7,
|
||||
"nextTurnKeepBlock": true,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"ThiefAgility": {
|
||||
@@ -1488,9 +1496,10 @@
|
||||
"kind": "Skill",
|
||||
"class": "thief",
|
||||
"rarity": "unique",
|
||||
"desc": "방어도를 5 얻습니다. 민첩을 1 얻습니다.",
|
||||
"desc": "방어도를 5 얻습니다. 민첩을 1 얻습니다. 이번 턴 동안 손의 다른 스킬 카드 1장이 교활해집니다.",
|
||||
"block": 5,
|
||||
"dex": 1,
|
||||
"turnHandSlyCount": 1,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
},
|
||||
"EdgeCarnival": {
|
||||
@@ -1499,9 +1508,11 @@
|
||||
"kind": "Attack",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "피해를 3만큼 5번 줍니다.",
|
||||
"damage": 3,
|
||||
"hits": 5,
|
||||
"desc": "무작위 적에게 피해를 2만큼 4번 줍니다. 표창 1장을 손에 넣습니다.",
|
||||
"damage": 2,
|
||||
"hits": 4,
|
||||
"randomTargetEachHit": true,
|
||||
"addShiv": 1,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"MuspelHeim": {
|
||||
@@ -1510,9 +1521,10 @@
|
||||
"kind": "Attack",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "모든 적에게 피해를 8 줍니다. 약화를 1 부여합니다.",
|
||||
"desc": "모든 적에게 피해를 4 줍니다. 이번 턴에 버린 카드 1장당 피해가 2 증가합니다. 약화를 1 부여합니다.",
|
||||
"aoe": true,
|
||||
"damage": 8,
|
||||
"damage": 4,
|
||||
"damagePerDiscardedThisTurn": 2,
|
||||
"weak": 1,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
},
|
||||
@@ -1522,20 +1534,22 @@
|
||||
"kind": "Attack",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "이번 턴에 버린 카드 1장당 피해를 6 줍니다.",
|
||||
"damage": 0,
|
||||
"damagePerDiscardedThisTurn": 6,
|
||||
"desc": "피해를 2 줍니다. 이번 턴에 버린 카드 1장당 피해가 7 증가합니다. 방어도를 무시합니다.",
|
||||
"damage": 2,
|
||||
"damagePerDiscardedThisTurn": 7,
|
||||
"pierce": true,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"DarkFlare": {
|
||||
"name": "다크 플레어",
|
||||
"cost": 1,
|
||||
"cost": 2,
|
||||
"kind": "Power",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "매 턴 모든 적에게 피해를 4 줍니다.",
|
||||
"desc": "매 턴 모든 적에게 피해를 2 줍니다. 카드를 사용할 때마다 무작위 적에게 피해를 2 줍니다.",
|
||||
"cardPlayedRandomDamage": 2,
|
||||
"powerEffect": "damagePerTurn",
|
||||
"value": 4,
|
||||
"value": 2,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"PickPocket": {
|
||||
@@ -1544,9 +1558,11 @@
|
||||
"kind": "Skill",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "카드를 1장 뽑습니다. 표창을 1장 손으로 가져옵니다.",
|
||||
"desc": "카드를 1장 뽑습니다. 카드를 1장 버립니다. 버린 카드마다 표창 1장을 손에 넣고, 에너지를 1 얻습니다.",
|
||||
"draw": 1,
|
||||
"addShiv": 1,
|
||||
"discard": 1,
|
||||
"addShivPerDiscard": true,
|
||||
"gainEnergy": 1,
|
||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
||||
},
|
||||
"ShadowPartner": {
|
||||
@@ -1555,9 +1571,12 @@
|
||||
"kind": "Skill",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "legend",
|
||||
"desc": "다음 턴에, 공격 카드의 피해량이 2배가 됩니다. 카드를 1장 뽑습니다.",
|
||||
"nextTurnAttackMultiplier": 2,
|
||||
"desc": "카드를 1장 선택합니다. 다음 턴에 그 카드의 복사본 1장을 손에 넣습니다. 카드를 1장 뽑습니다. 소멸.",
|
||||
"nextTurnCopies": 1,
|
||||
"nextTurnSelectHandCard": true,
|
||||
"nextTurnSelectPrompt": "복사할 카드를 선택하세요.",
|
||||
"draw": 1,
|
||||
"exhaust": true,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"AdvancedDarkSight": {
|
||||
@@ -1566,9 +1585,9 @@
|
||||
"kind": "Skill",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "무형을 1 얻습니다. 카드를 1장 뽑습니다.",
|
||||
"desc": "무형을 1 얻습니다. 이번 턴 동안 손의 다른 스킬 카드 2장이 교활해집니다.",
|
||||
"intangible": 1,
|
||||
"draw": 1,
|
||||
"turnHandSlyCount": 2,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"IntoDarkness": {
|
||||
@@ -1577,9 +1596,10 @@
|
||||
"kind": "Skill",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "모든 적에게 약화를 2 부여합니다.",
|
||||
"desc": "모든 적에게 약화를 1 부여합니다. 이번 턴 동안 손의 다른 스킬 카드 2장이 교활해집니다.",
|
||||
"affectsAllEnemies": true,
|
||||
"weak": 2,
|
||||
"weak": 1,
|
||||
"turnHandSlyCount": 2,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"Venom": {
|
||||
@@ -1588,8 +1608,9 @@
|
||||
"kind": "Power",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "legend",
|
||||
"desc": "공격 카드가 맞히지 않은 피해를 줄 때마다 중독을 2 부여합니다.",
|
||||
"attackPoison": 2,
|
||||
"desc": "공격 카드가 막히지 않은 피해를 줄 때마다 중독을 1 부여합니다. 적 턴 시작 시 독이 한 번 더 적용됩니다.",
|
||||
"attackPoison": 1,
|
||||
"extraPoisonTicks": 1,
|
||||
"image": "19361e72087946b1888684185b40d935"
|
||||
},
|
||||
"Grid": {
|
||||
@@ -1598,9 +1619,9 @@
|
||||
"kind": "Power",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "unique",
|
||||
"desc": "매 턴 방어도를 3 얻습니다.",
|
||||
"powerEffect": "blockPerTurn",
|
||||
"value": 3,
|
||||
"desc": "가시를 3 얻습니다. 카드를 사용할 때마다 방어도를 1 얻습니다.",
|
||||
"thorns": 3,
|
||||
"cardPlayedBlock": 1,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"RadicalDarkness": {
|
||||
@@ -1609,10 +1630,232 @@
|
||||
"kind": "Skill",
|
||||
"class": "thiefmaster",
|
||||
"rarity": "legend",
|
||||
"desc": "이번 턴 동안 얻는 방어도가 2배가 됩니다. 카드를 1장 뽑습니다.",
|
||||
"desc": "카드를 1장 뽑습니다. 이번 턴 동안 얻는 방어도가 2배가 됩니다. 다음 턴에 방어도가 사라지지 않습니다.",
|
||||
"blockGainMultiplier": 2,
|
||||
"nextTurnKeepBlock": true,
|
||||
"draw": 1,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"ShurikenBurst": {
|
||||
"name": "슈리켄 버스트",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "assassin",
|
||||
"rarity": "normal",
|
||||
"desc": "무작위 적에게 피해를 3씩 4번 줍니다.",
|
||||
"damage": 3,
|
||||
"hits": 4,
|
||||
"randomTargetEachHit": true,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"WindTalisman": {
|
||||
"name": "윈드 탈리스만",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "assassin",
|
||||
"rarity": "unique",
|
||||
"desc": "카드를 1장 뽑고, 에너지를 1 얻습니다. 이번 턴 동안 스킬 카드의 비용이 1 감소합니다.",
|
||||
"draw": 1,
|
||||
"gainEnergy": 1,
|
||||
"skillCostReductionThisTurn": 1,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
},
|
||||
"MarkOfAssassin": {
|
||||
"name": "마크 오브 어쌔신",
|
||||
"cost": 1,
|
||||
"kind": "Power",
|
||||
"class": "assassin",
|
||||
"rarity": "unique",
|
||||
"desc": "약화 1을 부여합니다. 약화 상태의 적에게 주는 공격 피해가 2배가 됩니다.",
|
||||
"weak": 1,
|
||||
"attackDamageVsWeakMultiplier": 2,
|
||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
||||
},
|
||||
"ShadowRush": {
|
||||
"name": "쉐도우 러쉬",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "assassin",
|
||||
"rarity": "normal",
|
||||
"desc": "피해 7, 방어도 5를 얻습니다.",
|
||||
"damage": 7,
|
||||
"block": 5,
|
||||
"image": "92a5020c978c46bdabab910598118b86"
|
||||
},
|
||||
"ShadowLeap": {
|
||||
"name": "쉐도우 리프",
|
||||
"cost": 0,
|
||||
"kind": "Skill",
|
||||
"class": "assassin",
|
||||
"rarity": "normal",
|
||||
"desc": "방어도 4를 얻습니다. 다음 턴에 방어도 4를 얻습니다.",
|
||||
"block": 4,
|
||||
"nextTurnBlock": 4,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
},
|
||||
"ShadowBlink": {
|
||||
"name": "쉐도우 블링크",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "assassin",
|
||||
"rarity": "unique",
|
||||
"desc": "무형 1을 얻습니다. 다음 스킬 카드의 비용이 0이 됩니다. 소멸.",
|
||||
"intangible": 1,
|
||||
"nextSkillCostZero": true,
|
||||
"exhaust": true,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"JavelinMastery": {
|
||||
"name": "자벨린 마스터리",
|
||||
"cost": 1,
|
||||
"kind": "Power",
|
||||
"class": "assassin",
|
||||
"rarity": "unique",
|
||||
"desc": "표창의 피해량이 2 증가합니다.",
|
||||
"shivDamageBonus": 2,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"JavelinAcceleration": {
|
||||
"name": "자벨린 액셀레이션",
|
||||
"cost": 0,
|
||||
"kind": "Skill",
|
||||
"class": "assassin",
|
||||
"rarity": "normal",
|
||||
"desc": "카드를 2장 뽑습니다. 카드를 1장 버립니다. 표창 1장을 손에 넣습니다.",
|
||||
"draw": 2,
|
||||
"discard": 1,
|
||||
"addShiv": 1,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
},
|
||||
"CriticalThrow": {
|
||||
"name": "크리티컬 스로우",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "assassin",
|
||||
"rarity": "unique",
|
||||
"desc": "피해를 7씩 2번 줍니다. 방어도를 무시합니다. 이번 턴 첫 카드라면 피해가 더 강해집니다.",
|
||||
"damage": 7,
|
||||
"hits": 2,
|
||||
"pierce": true,
|
||||
"firstCardDamageBonus": 3,
|
||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||
},
|
||||
"AssassinPhysicalTraining": {
|
||||
"name": "피지컬 트레이닝",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "assassin",
|
||||
"rarity": "normal",
|
||||
"desc": "힘 1, 민첩 1을 얻고 카드를 1장 뽑습니다.",
|
||||
"strength": 1,
|
||||
"dex": 1,
|
||||
"draw": 1,
|
||||
"image": "49c8f279bfa64bf3954037f17da0052d"
|
||||
},
|
||||
"TripleThrow": {
|
||||
"name": "트리플 스로우",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "hermit",
|
||||
"rarity": "normal",
|
||||
"desc": "피해를 4씩 3번 줍니다.",
|
||||
"damage": 4,
|
||||
"hits": 3,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"ShurikenChallenge": {
|
||||
"name": "슈리켄 챌린지",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "hermit",
|
||||
"rarity": "unique",
|
||||
"desc": "피해를 5씩 2번 줍니다. 다음 턴에 카드를 1장 더 뽑습니다.",
|
||||
"damage": 5,
|
||||
"hits": 2,
|
||||
"nextTurnDraw": 1,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"HermitDarkFlare": {
|
||||
"name": "다크 플레어",
|
||||
"cost": 2,
|
||||
"kind": "Power",
|
||||
"class": "hermit",
|
||||
"rarity": "unique",
|
||||
"desc": "매 턴 모든 적에게 피해 3을 줍니다. 턴 시작마다 표창 1장을 손에 넣습니다.",
|
||||
"powerEffect": "damagePerTurn",
|
||||
"value": 3,
|
||||
"turnStartShiv": 1,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"HermitShadowPartner": {
|
||||
"name": "쉐도우 파트너",
|
||||
"cost": 2,
|
||||
"kind": "Skill",
|
||||
"class": "hermit",
|
||||
"rarity": "legend",
|
||||
"desc": "카드를 1장 뽑습니다. 다음 턴 공격 카드의 피해가 2배가 됩니다.",
|
||||
"draw": 1,
|
||||
"nextTurnAttackMultiplier": 2,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"SpiritJavelin": {
|
||||
"name": "스피릿 자벨린",
|
||||
"cost": 1,
|
||||
"kind": "Power",
|
||||
"class": "hermit",
|
||||
"rarity": "unique",
|
||||
"desc": "표창이 턴 종료 시 사라지지 않습니다. 매 턴 처음 사용하는 표창의 피해량이 4 증가합니다.",
|
||||
"shivRetain": true,
|
||||
"firstShivDamageBonus": 4,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"HermitRadicalDarkness": {
|
||||
"name": "래디컬 다크니스",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "hermit",
|
||||
"rarity": "unique",
|
||||
"desc": "방어도 4를 얻습니다. 이번 턴 동안 얻는 방어도가 2배가 됩니다. 소멸.",
|
||||
"block": 4,
|
||||
"blockGainMultiplier": 2,
|
||||
"exhaust": true,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"HermitVenom": {
|
||||
"name": "베놈",
|
||||
"cost": 2,
|
||||
"kind": "Power",
|
||||
"class": "hermit",
|
||||
"rarity": "legend",
|
||||
"desc": "공격 카드가 막히지 않은 피해를 줄 때마다 중독 1을 부여합니다. 전투 중 독 부여 3회마다 모든 적에게 피해 8을 줍니다.",
|
||||
"attackPoison": 1,
|
||||
"poisonApplicationBurstEvery": 3,
|
||||
"poisonApplicationBurstDamage": 8,
|
||||
"image": "19361e72087946b1888684185b40d935"
|
||||
},
|
||||
"SkilledJavelin": {
|
||||
"name": "숙련된 표창술",
|
||||
"cost": 1,
|
||||
"kind": "Power",
|
||||
"class": "hermit",
|
||||
"rarity": "unique",
|
||||
"desc": "표창의 피해량이 2 증가합니다. 매 턴 처음 사용하는 표창의 피해량이 4 증가합니다.",
|
||||
"shivDamageBonus": 2,
|
||||
"firstShivDamageBonus": 4,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"HermitAdrenaline": {
|
||||
"name": "아드레날린",
|
||||
"cost": 0,
|
||||
"kind": "Skill",
|
||||
"class": "hermit",
|
||||
"rarity": "legend",
|
||||
"desc": "에너지를 1 얻고 카드를 1장 뽑습니다. 표창 1장을 손에 넣습니다. 소멸.",
|
||||
"gainEnergy": 1,
|
||||
"draw": 1,
|
||||
"addShiv": 1,
|
||||
"exhaust": true,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
}
|
||||
},
|
||||
"starterDecks": {
|
||||
|
||||
BIN
data/cards.xlsx
BIN
data/cards.xlsx
Binary file not shown.
@@ -22,6 +22,8 @@
|
||||
- 공용으로 표현 가능한 효과는 카드 전용 분기로 만들지 않는다.
|
||||
- 같은 의미의 효과는 같은 필드 이름을 쓴다.
|
||||
- 문서는 카드별 상태표와 공용 필드 사전을 분리해서 유지한다.
|
||||
- 카드 `kind`는 효과와 맞춘다 — 데미지 카드=`Attack`, block·유틸만 있으면=`Skill`, 지속효과=`Power`(`powerEffect` 또는 power 필드 필수). 안 맞으면 사용 불가/死카드가 된다(Power 분기는 damage/aoe 무시, Attack은 몬스터 드롭 라우팅).
|
||||
- 새 효과 필드는 Lua(`cb/*.mjs`)와 JS 미러(`tools/balance/sim-balance.mjs`) 양쪽에 구현한다(한쪽만 = 게임↔시뮬 드리프트).
|
||||
|
||||
## 응답 원칙
|
||||
|
||||
@@ -29,3 +31,9 @@
|
||||
- 바뀐 점과 남은 점만 말한다.
|
||||
- 불필요한 재설명은 줄인다.
|
||||
|
||||
## 검증·통합 원칙
|
||||
|
||||
- 카드/cb 변경 후 검증 스위트를 돌린다: `node tools/verify/cardkinds.mjs`(kind↔효과)·`cbprops.mjs`(미선언 `self.X` 필드)·`cbgap.mjs`(UI 경로) + `node --test tools/balance/sim-balance.test.mjs`(이중구현 미러). 이상 0을 확인한 뒤 산출물을 갱신한다.
|
||||
- 작업 브랜치에 `main`을 머지했다가 충돌·문제가 나도 그 머지 커밋을 통째로 `git revert`하지 않는다 — main에 먼저 들어간 타인 작업이 collateral로 사라진다(2026-06-30 `#98/#99`가 `#96` 11개 수정을 이렇게 날린 사고). 소스 충돌만 해소하고 산출물(codeblock 등)은 재생성한다.
|
||||
- 하네스 규칙의 최종 권위는 `RULES.md`(§1 산출물 읽기/수정 금지·§4 git/PR·§6 이중구현 동기화·§9 카드 kind)이고, codex 전용 하드룰은 `docs/codex-working-rules.md`다. 작업 전 둘 다 따른다.
|
||||
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
3. 전직 구조를 바꿀 때는 실제 직업명만 사용한다. 임의의 내부 분류명이나 새 직업명을 사용자-facing 구조에 추가하지 않는다.
|
||||
4. 대량 치환 전에 수정 대상 파일과 범위를 먼저 확인하고, 원본 문자열이 깨진 상태면 치환 작업을 진행하지 않는다.
|
||||
5. 생성기 파일을 크게 수정할 때는 `node --check`와 생성기 실행으로 문법을 먼저 검증한 뒤 산출물을 갱신한다.
|
||||
6. 작업 브랜치에 `main`을 머지했다가 충돌·문제가 나도 **그 머지 커밋을 통째로 `git revert`하지 않는다** — main에 먼저 들어간 타인 작업이 collateral로 사라진다(2026-06-30 `#98/#99`가 `#96`의 버그수정 11개를 이렇게 전부 날림). 소스 충돌만 해소하고 산출물(codeblock 등)은 재생성한다. (RULES §4)
|
||||
7. 카드 `kind`는 효과와 일치시킨다 — 데미지 카드=`Attack`, block·유틸만 있으면=`Skill`, 지속효과=`Power`(`powerEffect` 또는 power 필드 필수). 안 맞으면 사용 불가/死카드가 된다(2026-06-30 아이언 바디=Attack인데 block만, 분노=Power인데 damage만 → 둘 다 먹통). 카드 추가/수정 후 `node tools/verify/cardkinds.mjs`로 검증(이상 0 = exit 0). (RULES §9)
|
||||
8. 카드/cb 변경 후 검증 스위트를 돌린다: `node tools/verify/cardkinds.mjs`(kind↔효과)·`cbprops.mjs`(미선언 `self.X` 필드)·`cbgap.mjs`(UI 경로) + `node --test tools/balance/sim-balance.test.mjs`(이중구현 미러). 새 효과 필드는 Lua(`cb/*.mjs`)와 JS 미러(`tools/balance/sim-balance.mjs`) **양쪽**에 구현(한쪽만 = 게임↔시뮬 드리프트). (RULES §6)
|
||||
9. 하네스 규칙의 권위는 `RULES.md`다 — 작업 전 RULES.md(§1 산출물 읽기/수정 금지·§4 git/PR·§6 이중구현 동기화·§9 카드 kind)를 읽고 따른다.
|
||||
|
||||
42
tools/verify/cardkinds.mjs
Normal file
42
tools/verify/cardkinds.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
// 카드 kind ↔ 효과 정합성 정적 검사 (협업자/codex가 카드 추가 후 실행).
|
||||
// 배경(2026-06-30): kind가 효과와 안 맞으면 카드가 사용불가/死카드가 된다.
|
||||
// - ResolveCardDrop 라우팅: Attack=몬스터 위 드롭(FindMonsterAtTouch>0 필요) / Skill·Power=위로 스윕 / Status=unplayable.
|
||||
// → block·유틸만 있고 데미지 없는 카드를 Attack으로 두면 위로 스윕으로 못 쓴다(아이언 바디 사고).
|
||||
// - PlayCard의 Power 분기는 PlayerPowers 등록만 하고 damage/aoe를 무시한다.
|
||||
// → Power인데 powerEffect도 power필드도 없으면 재생 시 아무 효과 없는 死카드(분노 사고).
|
||||
// 사용: node tools/verify/cardkinds.mjs (이상 0 → exit 0, 있으면 목록 + exit 1)
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const cards = JSON.parse(readFileSync('data/cards.json', 'utf8')).cards;
|
||||
|
||||
// Power 카드를 실제로 기능하게 하는 필드(powerEffect 지속효과 + 온플레이/지속 power 필드).
|
||||
// damage/aoe/block 같은 Attack/Skill 전용 필드는 Power 분기서 무시되므로 제외.
|
||||
const POWER_FIELDS = [
|
||||
'powerEffect', 'strength', 'dex', 'thorns', 'intangible',
|
||||
'turnStartShiv', 'turnStartDraw', 'turnStartDiscard',
|
||||
'shivDamageBonus', 'firstShivDamageBonus', 'shivRetain', 'shivAoe',
|
||||
'attackPoison', 'drawDamage', 'drawPoison', 'attackDamageVsWeakMultiplier',
|
||||
'cardPlayedBlock', 'cardPlayedDamage', 'cardPlayedRandomDamage',
|
||||
'extraPoisonTicks', 'poisonApplicationBurstEvery', 'poisonApplicationBurstDamage',
|
||||
'skillSlyOnPlay', 'endTurnDexLoss',
|
||||
];
|
||||
const VALID_KINDS = ['Attack', 'Skill', 'Power', 'Status'];
|
||||
|
||||
const issues = [];
|
||||
for (const [id, c] of Object.entries(cards)) {
|
||||
if (!VALID_KINDS.includes(c.kind)) {
|
||||
issues.push(`${id}(${c.name}): 미지원 kind="${c.kind}"`);
|
||||
continue;
|
||||
}
|
||||
if (c.kind === 'Attack' && c.damage == null && c.xDamagePerEnergy == null) {
|
||||
issues.push(`${id}(${c.name}): kind=Attack인데 damage 없음 → 몬스터 드롭 라우팅 불가(방어/유틸이면 kind=Skill)`);
|
||||
}
|
||||
if (c.kind === 'Power' && !POWER_FIELDS.some((f) => c[f] != null)) {
|
||||
issues.push(`${id}(${c.name}): kind=Power인데 power효과 없음(死카드) → damage/aoe는 Power 분기서 무시, kind 재검토`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`카드 ${Object.keys(cards).length}장 kind↔효과 정합성: 이상 ${issues.length}`);
|
||||
for (const i of issues) console.log(' ⚠️ ' + i);
|
||||
console.log(issues.length ? 'RESULT: 정합성 위반 (위 카드 kind 수정 필요)' : 'RESULT: 모든 카드 kind↔효과 일치 ✓');
|
||||
process.exit(issues.length ? 1 : 0);
|
||||
Reference in New Issue
Block a user