Merge main into feature/slay-deck-controller — B~E6a 카드시스템 통합 + 메인 메뉴 이식
main의 35커밋(B 전투통합~E6a 멀티act)을 브랜치에 통합. 충돌 해결: - tools/gen-slaydeck.mjs: main(B~E6a) 생성기 채택 + 브랜치 메인 메뉴(MainMenu UI·ShowMainMenu/BindMenuButtons/StartNewGame/SetEntityEnabled·OnBeginPlay→메뉴) 이식 - ui/DefaultGroup.ui·SlayDeckController.codeblock: 통합 생성기로 재생성 - map10·모델: main 채택 후 freeze 도구(freeze-turn-monsters/player) 재적용 정적 검증: 문법·JSON 유효·생성기 결정적·메뉴/B~E6a 양쪽 유지·freeze 적용. ⚠️ Maker 연결 해제로 메뉴→게임 런타임 검증은 미수행(사용자 메이커 확인 필요). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
README.md
78
README.md
@@ -43,14 +43,15 @@ git pull
|
||||
```
|
||||
slaymaple/
|
||||
├── Global/ # 월드 전역 설정 · 공용 모델 · 게임로직
|
||||
│ ├── common.gamelogic # SlayCardCatalog / SlayRunState / SlayCombatManager 부착 지점
|
||||
│ ├── common.gamelogic # SlayDeckController 부착 지점 (카드 UI 전투)
|
||||
│ ├── Player.model # 플레이어 모델
|
||||
│ ├── *.model # 몬스터 등 공용 모델
|
||||
│ ├── WorldConfig.config # 월드 설정
|
||||
│ └── ...
|
||||
├── RootDesk/
|
||||
│ └── MyDesk/ # 작업용 책상 — codeblock(스크립트)·모델·타일셋
|
||||
│ ├── Monster.codeblock
|
||||
│ ├── SlayDeckController.codeblock # 카드 UI 전투 컨트롤러 (생성물)
|
||||
│ ├── Monster.codeblock # 필드 액션 몬스터 (HP·피격·리스폰, 카드 전투와 별개)
|
||||
│ ├── MonsterAttack.codeblock
|
||||
│ ├── PlayerAttack.codeblock
|
||||
│ ├── PlayerHit.codeblock
|
||||
@@ -58,7 +59,11 @@ slaymaple/
|
||||
│ ├── UIToast.codeblock
|
||||
│ └── RectTileData_Henesys.tileset
|
||||
├── map/
|
||||
│ └── map01.map # 메인 맵
|
||||
│ ├── map01.map ~ map11.map # 맵 11종 (공식 배경 + STS풍 우측 배치)
|
||||
├── tools/ # 결정적 생성기 (단일 소스)
|
||||
│ ├── gen-slaydeck.mjs # 카드/덱 UI · SlayDeckController · common 생성
|
||||
│ ├── gen-cardhand.mjs # 손패 카드 엔티티 생성
|
||||
│ └── gen-maps.mjs # 맵 생성
|
||||
├── ui/ # UI 그룹 (Default / Popup / Toast)
|
||||
├── docs/
|
||||
│ └── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서
|
||||
@@ -71,42 +76,61 @@ slaymaple/
|
||||
|
||||
## 게임 프레임워크 현황
|
||||
|
||||
`Global/common.gamelogic`의 `/common` 엔티티에 부착된 세 컴포넌트가 전투의 핵심입니다.
|
||||
현재 전투는 `Global/common.gamelogic`의 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작합니다. 모든 카드/덱/전투 관련 산출물(`ui/DefaultGroup.ui` · `RootDesk/MyDesk/SlayDeckController.codeblock` · `common.gamelogic`)은 **`tools/gen-slaydeck.mjs` 단일 소스에서 생성**됩니다(직접 편집 금지, 결정적 출력).
|
||||
|
||||
| 컴포넌트 | 역할 |
|
||||
|---|---|
|
||||
| `SlayCardCatalog` | 카드 데이터, 시작 덱 구성, 보상 풀, 카드 복제 정의 |
|
||||
| `SlayRunState` | HP·골드·층수·덱·유물·카드 보상 등 런(run) 영속 데이터 관리 |
|
||||
| `SlayCombatManager` | 턴 진행, 드로우/버림/소멸 더미, 에너지, 적 의도, 방어도, 데미지, 승패 처리 |
|
||||
| 컴포넌트 | 상태 | 역할 |
|
||||
|---|---|---|
|
||||
| `SlayDeckController` | ✅ 구현됨 | 카드 손패 UI 전투 — 드로우/버림/재셔플, 에너지, 카드 효과(데미지/방어), 적 HP·방어·의도, 턴 진행, 승패 |
|
||||
| `Monster.codeblock` | ✅ 구현됨 | 필드 액션 몬스터(HP·피격·리스폰) — 카드 전투와는 **별개** 시스템 |
|
||||
|
||||
### 프로토타입 흐름
|
||||
1. `SlayRunState`가 HP 80 · 10장 시작 덱으로 새 런 시작
|
||||
2. `SlayCombatManager`가 데모 전투 자동 시작
|
||||
3. 매 플레이어 턴: 에너지 3 회복, 방어도 초기화, 적 의도 갱신, 5장 드로우
|
||||
4. 카드 사용 시 에너지 소모 → 데미지/방어/드로우/에너지/상태이상 적용 → 버림 또는 소멸
|
||||
5. 턴 종료 시 손패 버림, 적 의도 실행, 상태이상 처리, 다음 턴 시작
|
||||
6. 전투 승리 시 잔여 HP 저장, 골드 15 지급, 카드 보상 3종 생성
|
||||
### 구현된 카드 전투 (단일 전투 루프)
|
||||
- **카드 손패 UI**: 에너지 3, 매 턴 5장 드로우, 버림 더미·재셔플, 카드 클릭 사용, 종류별 색상.
|
||||
- **카드 3종**: 타격(피해 6) · 방어(방어도 5) · 강타(피해 10). 각 카드에 `damage`/`block` 수치 필드. 시작 덱 10장.
|
||||
- **전투 상태**: 플레이어 `HP`/`Block`, 적 `HP`/`Block`/`Intent(의도)`. 적 의도는 **결정적 사이클**(공격10 → 공격6 → 방어8)로 다음 행동을 미리 표시.
|
||||
- **규칙**: 데미지는 방어도 먼저 차감 후 잔여만 HP에 적용. 플레이어 Block은 턴 시작 시 리셋.
|
||||
- **턴 흐름**: 카드 사용(`Attack`→적 HP↓, `Skill`→플레이어 Block↑) → 턴 종료 → 적 턴(의도 실행) → 다음 플레이어 턴.
|
||||
- **승패**: 적 HP 0 → 승리, 플레이어 HP 0 → 패배. 승패 시 입력 잠금 + 결과 표시(전투 보상 훅 자리 = E 예정).
|
||||
- **UI(CombatHud)**: 적 패널(이름·HP·방어·의도)·플레이어 패널(HP·방어)·승패 결과 텍스트.
|
||||
|
||||
> ⚠️ 플레이어 HP(80)·적 HP(45)·의도 수치(10·6·8)는 **루프 검증용 임시 placeholder**입니다.
|
||||
> 향후 캐릭터 특성별/몬스터별 데이터로 분리할 예정입니다(아래 D 참조).
|
||||
|
||||
### 유용한 스크립트 호출
|
||||
`/common` 엔티티에 붙은 스크립트에서:
|
||||
`/common` 엔티티(또는 Play Test 컨텍스트)에서:
|
||||
```lua
|
||||
self.Entity.SlayCombatManager:PlayCard(1, 1)
|
||||
self.Entity.SlayCombatManager:EndPlayerTurn()
|
||||
self.Entity.SlayCombatManager:DebugPlayFirstPlayable()
|
||||
self.Entity.SlayRunState:PickReward(1)
|
||||
self.Entity.SlayCombatManager:StartCombat("elite")
|
||||
local c = _EntityService:GetEntityByPath("/common").SlayDeckController
|
||||
c:PlayCard(1) -- 손패 slot 카드 사용
|
||||
c:EndPlayerTurn() -- 턴 종료 → 적 턴 → 다음 턴
|
||||
c:StartCombat() -- 전투 재시작(상태 초기화)
|
||||
```
|
||||
|
||||
상세 설계는 [`docs/slaymaple_basic_framework.md`](docs/slaymaple_basic_framework.md) 참조.
|
||||
|
||||
---
|
||||
|
||||
## 향후 설계 (미구현 — 목표 아키텍처)
|
||||
|
||||
아래는 로그라이크 메타까지 확장했을 때의 **목표 컴포넌트 구조**로, 현재는 미구현입니다. (현재는 `SlayDeckController` 하나가 카드 전투만 담당)
|
||||
|
||||
| 컴포넌트 (계획) | 역할 |
|
||||
|---|---|
|
||||
| `SlayCardCatalog` | 카드 데이터, 시작 덱 구성, 보상 풀, 카드 복제 정의 |
|
||||
| `SlayRunState` | HP·골드·층수·덱·유물·카드 보상 등 런(run) 영속 데이터 관리 |
|
||||
| `SlayCombatManager` | 턴 진행, 드로우/버림/소멸 더미, 에너지, 적 의도, 방어도, 데미지, 승패 처리 |
|
||||
|
||||
> 위 구조로 가더라도 카드/적 데이터는 `tools/`의 결정적 생성기를 단일 소스로 유지하는 방향을 권장합니다.
|
||||
|
||||
---
|
||||
|
||||
## 다음 구현 단계
|
||||
- [ ] HP·방어도·에너지·적 의도·손패 5버튼을 렌더링하는 전투 UI
|
||||
- [ ] 전투/엘리트/상점/휴식/이벤트/보스 노드를 가진 맵 노드 UI
|
||||
- [ ] `OnCombatStart` / `OnCardPlayed` / `OnTurnStart` / `OnCombatReward` 훅을 가진 유물 시스템
|
||||
- [ ] 적 행동 패턴을 데이터로 정의 (현재 단순 의도 패턴 → 무브셋)
|
||||
- [ ] 런 루프 완성 후 저장/불러오기
|
||||
- [x] HP·방어도·에너지·적 의도·손패 카드를 렌더링하는 전투 UI **(완료 — `SlayDeckController` + CombatHud)**
|
||||
- [x] 카드 사용이 실제 데미지/방어/적 의도/승패에 반영되는 단일 전투 루프 **(완료)**
|
||||
- [ ] 카드/적 데이터를 `data/cards.json` · `data/enemies.json`로 외부화 (D)
|
||||
- [ ] 전투를 N회 자동 시뮬레이션하는 밸런스 검증 도구 `tools/sim-balance.mjs` (F, D 선행)
|
||||
- [ ] 전투/엘리트/상점/휴식/이벤트/보스 노드를 가진 맵 노드 UI (E)
|
||||
- [ ] `OnCombatStart` / `OnCardPlayed` / `OnTurnStart` / `OnCombatReward` 훅을 가진 유물 시스템 (E)
|
||||
- [ ] 적 행동 패턴을 데이터로 정의 (현재 단순 결정적 의도 사이클 → 무브셋)
|
||||
- [ ] 런 영속(HP/골드/층/덱/유물) + 저장/불러오기 (E, 루프 end-to-end 후)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -91,6 +91,209 @@
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "NewGameHandler"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "Cards"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "PlayerHp"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "80",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "PlayerMaxHp"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "PlayerBlock"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyHp"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "45",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyMaxHp"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyBlock"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "1",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyIntentIndex"
|
||||
},
|
||||
{
|
||||
"Type": "boolean",
|
||||
"DefaultValue": "false",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "CombatOver"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyIntents"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyName"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RunDeck"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "Gold"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "Floor"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "3",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RunLength"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RewardChoices"
|
||||
},
|
||||
{
|
||||
"Type": "boolean",
|
||||
"DefaultValue": "false",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RunActive"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "Enemies"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "MapNodes"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "MapStart"
|
||||
},
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "CurrentNodeId"
|
||||
},
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "CurrentEnemyId"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "ShopChoices"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "ShopBought"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "Relics"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RunRelics"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RelicPool"
|
||||
},
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "ShopRelic"
|
||||
},
|
||||
{
|
||||
"Type": "boolean",
|
||||
"DefaultValue": "false",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "ShopRelicBought"
|
||||
}
|
||||
],
|
||||
"Methods": [
|
||||
@@ -118,7 +321,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:SetEntityEnabled(\"/ui/DefaultGroup/MainMenu\", true)\nself:SetEntityEnabled(\"/ui/DefaultGroup/CardHand\", false)\nself:SetEntityEnabled(\"/ui/DefaultGroup/DeckHud\", false)\nself:BindMenuButtons()",
|
||||
"Code": "self:SetEntityEnabled(\"/ui/DefaultGroup/MainMenu\", true)\nself:BindMenuButtons()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -133,7 +336,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local buttonEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MainMenu/NewGameButton\")\nif buttonEntity == nil or buttonEntity.ButtonComponent == nil then\n\treturn\nend\nif self.NewGameHandler ~= nil then\n\tbuttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)\n\tself.NewGameHandler = nil\nend\nself.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, self.StartNewGame)",
|
||||
"Code": "local buttonEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MainMenu/NewGameButton\")\nif buttonEntity == nil or buttonEntity.ButtonComponent == nil then\n\treturn\nend\nif self.NewGameHandler ~= nil then\n\tbuttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)\n\tself.NewGameHandler = nil\nend\nself.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -148,12 +351,42 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:SetEntityEnabled(\"/ui/DefaultGroup/MainMenu\", false)\nself:SetEntityEnabled(\"/ui/DefaultGroup/CardHand\", true)\nself:SetEntityEnabled(\"/ui/DefaultGroup/DeckHud\", true)\nself:ConfigureTurnBasedMonsters()\nself:ConfigureTurnBasedPlayer()\nself:StartCombat()",
|
||||
"Code": "self:SetEntityEnabled(\"/ui/DefaultGroup/MainMenu\", false)\nself:StartRun()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "StartNewGame"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "path"
|
||||
},
|
||||
{
|
||||
"Type": "boolean",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "enabled"
|
||||
}
|
||||
],
|
||||
"Code": "local e = _EntityService:GetEntityByPath(path)\nif e ~= nil then\n\te.Enable = enabled\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "SetEntityEnabled"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
@@ -163,65 +396,27 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.MaxEnergy = 3\nself.Turn = 0\nself.DiscardPile = {}\nself.Hand = {}\nself.DrawPile = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself:Shuffle(self.DrawPile)\nself:BindButtons()\nself:StartPlayerTurn()",
|
||||
"Code": "self.PlayerMaxHp = 80\nself.PlayerHp = self.PlayerMaxHp\nself.Gold = 0\nself.Floor = 1\nself.RunLength = 3\nself.RunDeck = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself.RunActive = true\nself.RunRelics = {}\nself.Relics = {\n\tironHeart = { name = \"강철 심장\", desc = \"전투 시작 시 방어도 +6\", hook = \"combatStart\", effect = \"block\", value = 6 },\n\tenergyCore = { name = \"에너지 코어\", desc = \"턴 시작 시 에너지 +1\", hook = \"turnStart\", effect = \"energy\", value = 1 },\n\tvampire = { name = \"흡혈 송곳니\", desc = \"공격 카드 사용 시 HP +1\", hook = \"cardPlayed\", effect = \"healOnAttack\", value = 1 },\n\tgoldIdol = { name = \"황금 우상\", desc = \"전투 승리 시 골드 +10\", hook = \"combatReward\", effect = \"gold\", value = 10 },\n}\nself.RelicPool = { \"energyCore\", \"vampire\", \"goldIdol\" }\nself.Enemies = {\n\tslime = { name = \"슬라임\", maxHp = 45, intents = { { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 6 }, { kind = \"Defend\", value = 8 } } },\n\tslime_elite = { name = \"정예 슬라임\", maxHp = 70, intents = { { kind = \"Attack\", value = 14 }, { kind = \"Attack\", value = 8 }, { kind = \"Defend\", value = 10 } } },\n\tslime_boss = { name = \"슬라임 킹\", maxHp = 120, intents = { { kind = \"Attack\", value = 18 }, { kind = \"Defend\", value = 12 }, { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 22 } } },\n}\nself.MapNodes = {\n\tA = { type = \"combat\", enemy = \"slime\", row = 1, col = -1, next = { \"C\", \"D\" } },\n\tB = { type = \"combat\", enemy = \"slime\", row = 1, col = 1, next = { \"C\", \"D\" } },\n\tC = { type = \"rest\", row = 2, col = -1, next = { \"E\", \"F\" } },\n\tD = { type = \"shop\", row = 2, col = 1, next = { \"E\", \"F\" } },\n\tE = { type = \"elite\", enemy = \"slime_elite\", row = 3, col = -1, next = { \"BOSS\" } },\n\tF = { type = \"combat\", enemy = \"slime\", row = 3, col = 1, next = { \"BOSS\" } },\n\tBOSS = { type = \"boss\", enemy = \"slime_boss\", row = 4, col = 0, next = { } },\n}\nself.MapStart = { \"A\", \"B\" }\nself.CurrentNodeId = \"\"\nself.CurrentEnemyId = \"\"\nself:BindButtons()\nself:AddRelic(\"ironHeart\")\nself:ShowMap()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "StartRun"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.MaxEnergy = 3\nself.Turn = 0\nlocal enemy = self.Enemies[self.CurrentEnemyId]\nlocal mult = 1 + (self.Floor - 1) * 0.6\nself.PlayerBlock = 0\nself.EnemyName = enemy.name\nself.EnemyMaxHp = math.floor(enemy.maxHp * mult)\nself.EnemyHp = self.EnemyMaxHp\nself.EnemyBlock = 0\nself.EnemyIntents = {}\nfor i = 1, #enemy.intents do\n\tself.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) }\nend\nself.EnemyIntentIndex = 1\nself.CombatOver = false\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"타격\", cost = 1, desc = \"피해 6\", kind = \"Attack\", damage = 6 },\n\tDefend = { name = \"방어\", cost = 1, desc = \"방어도 5\", kind = \"Skill\", block = 5 },\n\tBash = { name = \"강타\", cost = 2, desc = \"피해 10\", kind = \"Attack\", damage = 10 },\n}\nself.DrawPile = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "StartCombat"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "for mapIndex = 1, 11 do\n\tlocal mapName = \"map\" .. string.format(\"%02d\", mapIndex)\n\tfor i = 1, 6 do\n\t\tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath(\"/maps/\" .. mapName .. \"/Monster\" .. tostring(i)))\n\tend\n\tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath(\"/maps/\" .. mapName .. \"/StaticMonsterTemplate\"))\n\tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath(\"/maps/\" .. mapName .. \"/MoveMonsterTemplate\"))\n\tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath(\"/maps/\" .. mapName .. \"/ChaseMonsterTemplate\"))\n\tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath(\"/maps/\" .. mapName .. \"/monster-43\"))\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ConfigureTurnBasedMonsters"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "Entity",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "monster"
|
||||
}
|
||||
],
|
||||
"Code": "if monster == nil then\n\treturn\nend\nif monster.AIWanderComponent ~= nil then\n\tmonster.AIWanderComponent.Enable = false\nend\nif monster.AIChaseComponent ~= nil then\n\tmonster.AIChaseComponent.Enable = false\nend\nif monster.MovementComponent ~= nil then\n\tmonster.MovementComponent.Enable = false\nend\nif monster.RigidbodyComponent ~= nil then\n\tmonster.RigidbodyComponent.MoveVelocity = Vector2.zero\n\tmonster.RigidbodyComponent.RealMoveVelocity = Vector2.zero\nend\nif monster.TransformComponent ~= nil then\n\tlocal scale = monster.TransformComponent.Scale\n\tmonster.TransformComponent.Scale = Vector3(math.abs(scale.x), math.abs(scale.y), scale.z)\nend\nif monster.StateAnimationComponent ~= nil and monster.SpriteRendererComponent ~= nil then\n\tlocal stand = monster.StateAnimationComponent.ActionSheet[\"stand\"]\n\tif stand ~= nil and stand ~= \"\" then\n\t\tmonster.SpriteRendererComponent.SpriteRUID = stand\n\tend\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ConfigureMonsterForTurnCombat"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local player = nil\npcall(function()\n\tif _UserService ~= nil and _UserService.LocalPlayer ~= nil then\n\t\tplayer = _UserService.LocalPlayer\n\tend\nend)\npcall(function()\n\tif player == nil and _UserService ~= nil and _UserService.LocalPlayerEntity ~= nil then\n\t\tplayer = _UserService.LocalPlayerEntity\n\tend\nend)\npcall(function()\n\tif player == nil and _UserService ~= nil and _UserService.GetLocalPlayer ~= nil then\n\t\tplayer = _UserService:GetLocalPlayer()\n\tend\nend)\nif player ~= nil and player.Entity ~= nil then\n\tplayer = player.Entity\nend\nif player == nil then\n\treturn\nend\nif player.PlayerControllerComponent ~= nil then\n\tplayer.PlayerControllerComponent.Enable = false\n\tpcall(function() player.PlayerControllerComponent.LookDirectionX = 1 end)\nend\nif player.MovementComponent ~= nil then\n\tplayer.MovementComponent.Enable = false\n\tpcall(function() player.MovementComponent.InputSpeed = 0 end)\n\tpcall(function() player.MovementComponent.JumpForce = 0 end)\nend\nif player.RigidbodyComponent ~= nil then\n\tplayer.RigidbodyComponent.MoveVelocity = Vector2.zero\n\tplayer.RigidbodyComponent.RealMoveVelocity = Vector2.zero\nend\nif player.TransformComponent ~= nil then\n\tlocal scale = player.TransformComponent.Scale\n\tplayer.TransformComponent.Scale = Vector3(math.abs(scale.x), math.abs(scale.y), scale.z)\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ConfigureTurnBasedPlayer"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
@@ -254,7 +449,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local buttonEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/EndTurnButton\")\nif buttonEntity == nil or buttonEntity.ButtonComponent == nil then\n\treturn\nend\nif self.EndTurnHandler ~= nil then\n\tbuttonEntity:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)\n\tself.EndTurnHandler = nil\nend\nself.EndTurnHandler = buttonEntity:ConnectEvent(ButtonClickEvent, self.EndPlayerTurn)",
|
||||
"Code": "local endTurn = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/EndTurnButton\")\nif endTurn ~= nil and endTurn.ButtonComponent ~= nil then\n\tif self.EndTurnHandler ~= nil then\n\t\tendTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)\n\t\tself.EndTurnHandler = nil\n\tend\n\tself.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)\nend\nfor i = 1, 5 do\n\tlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(i))\n\tif cardEntity ~= nil and cardEntity.ButtonComponent ~= nil then\n\t\tcardEntity:ConnectEvent(ButtonClickEvent, function() self:PlayCard(i) end)\n\tend\nend\nfor i = 1, 3 do\n\tlocal rc = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud/Reward\" .. tostring(i))\n\tif rc ~= nil and rc.ButtonComponent ~= nil then\n\t\trc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)\n\tend\nend\nlocal skip = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud/Skip\")\nif skip ~= nil and skip.ButtonComponent ~= nil then\n\tskip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)\nend\nlocal mapNodeIds = { \"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"BOSS\" }\nfor i = 1, #mapNodeIds do\n\tlocal nid = mapNodeIds[i]\n\tlocal mn = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MapHud/Node_\" .. nid)\n\tif mn ~= nil and mn.ButtonComponent ~= nil then\n\t\tmn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)\n\tend\nend\nfor i = 1, 3 do\n\tlocal sc = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Card\" .. tostring(i))\n\tif sc ~= nil and sc.ButtonComponent ~= nil then\n\t\tsc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)\n\tend\nend\nlocal shopLeave = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Leave\")\nif shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then\n\tshopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)\nend\nlocal shopRelic = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Relic\")\nif shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then\n\tshopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)\nend\nlocal restLeave = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RestHud/Leave\")\nif restLeave ~= nil and restLeave.ButtonComponent ~= nil then\n\trestLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -269,7 +464,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.Turn = self.Turn + 1\nself.Energy = self.MaxEnergy\nself:DrawCards(5)\nself:RenderHand(true)",
|
||||
"Code": "self.Turn = self.Turn + 1\nself.Energy = self.MaxEnergy\nself:ApplyRelics(\"turnStart\")\nself.PlayerBlock = 0\nself:DrawCards(5)\nself:RenderHand(true)\nself:RenderCombat()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -284,7 +479,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "for i = 1, #self.Hand do\n\ttable.insert(self.DiscardPile, self.Hand[i])\nend\nself.Hand = {}\nself:RenderHand(false)\nself:RenderPiles()\n_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)",
|
||||
"Code": "if self.CombatOver == true then\n\treturn\nend\nfor i = 1, #self.Hand do\n\ttable.insert(self.DiscardPile, self.Hand[i])\nend\nself.Hand = {}\nself:RenderHand(false)\nself:RenderPiles()\nself:EnemyTurn()\nself:CheckCombatEnd()\nif self.CombatOver == true then\n\treturn\nend\n_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -337,7 +532,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:SetText(\"/ui/DefaultGroup/DeckHud/DrawPile/Count\", tostring(#self.DrawPile))\nself:SetText(\"/ui/DefaultGroup/DeckHud/DiscardPile/Count\", tostring(#self.DiscardPile))\nself:SetText(\"/ui/DefaultGroup/DeckHud/Energy\", \"에너지 \" .. tostring(self.Energy) .. \"/\" .. tostring(self.MaxEnergy))",
|
||||
"Code": "self:SetText(\"/ui/DefaultGroup/DeckHud/DrawPile/Count\", tostring(#self.DrawPile))\nself:SetText(\"/ui/DefaultGroup/DeckHud/DiscardPile/Count\", tostring(#self.DiscardPile))\nself:SetText(\"/ui/DefaultGroup/DeckHud/Energy\", \"에너지 \" .. string.format(\"%d\", self.Energy) .. \"/\" .. string.format(\"%d\", self.MaxEnergy))",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -390,7 +585,7 @@
|
||||
"Name": "cardId"
|
||||
}
|
||||
],
|
||||
"Code": "local name = cardId\nlocal cost = 0\nlocal desc = \"\"\nlocal kind = \"Skill\"\nif cardId == \"Strike\" then\n\tname = \"타격\"\n\tcost = 1\n\tdesc = \"피해 6\"\n\tkind = \"Attack\"\nelseif cardId == \"Defend\" then\n\tname = \"방어\"\n\tcost = 1\n\tdesc = \"방어도 5\"\n\tkind = \"Skill\"\nelseif cardId == \"Bash\" then\n\tname = \"강타\"\n\tcost = 2\n\tdesc = \"피해 10\"\n\tkind = \"Attack\"\nend\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Cost\", tostring(cost))\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Name\", name)\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Desc\", desc)\nlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot))\nif cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then\n\tlocal ok = false\n\tlocal color = nil\n\tif kind == \"Attack\" then\n\t\tok, color = pcall(function() return Color(0.86, 0.42, 0.38, 1) end)\n\telseif kind == \"Skill\" then\n\t\tok, color = pcall(function() return Color(0.42, 0.55, 0.85, 1) end)\n\tend\n\tif ok == true and color ~= nil then\n\t\tcardEntity.SpriteGUIRendererComponent.Color = color\n\tend\nend",
|
||||
"Code": "local c = self.Cards[cardId]\nif c == nil then\n\tc = { name = cardId, cost = 0, desc = \"\", kind = \"Skill\" }\nend\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Cost\", tostring(c.cost))\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Name\", c.name)\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Desc\", c.desc)\nlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot))\nif cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then\n\tif c.kind == \"Attack\" then\n\t\tcardEntity.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\telseif c.kind == \"Skill\" then\n\t\tcardEntity.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\telse\n\t\tcardEntity.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\tend\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -426,36 +621,6 @@
|
||||
"Attributes": [],
|
||||
"Name": "SetText"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "path"
|
||||
},
|
||||
{
|
||||
"Type": "boolean",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "enabled"
|
||||
}
|
||||
],
|
||||
"Code": "local entity = _EntityService:GetEntityByPath(path)\nif entity ~= nil then\n\tentity.Enable = enabled\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "SetEntityEnabled"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
@@ -499,6 +664,484 @@
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "AnimateCardFrom"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "slot"
|
||||
}
|
||||
],
|
||||
"Code": "if self.CombatOver == true then\n\treturn\nend\nif self.Hand == nil then\n\treturn\nend\nlocal cardId = self.Hand[slot]\nif cardId == nil then\n\treturn\nend\nlocal c = self.Cards[cardId]\nif c == nil then\n\treturn\nend\nif self.Energy < c.cost then\n\tself:Toast(\"에너지가 부족합니다\")\n\treturn\nend\nself.Energy = self.Energy - c.cost\nif c.kind == \"Attack\" then\n\tif c.damage ~= nil then\n\t\tself:DealDamageToEnemy(c.damage)\n\tend\n\tself:ApplyRelics(\"cardPlayed\")\nelseif c.kind == \"Skill\" then\n\tif c.block ~= nil then\n\t\tself.PlayerBlock = self.PlayerBlock + c.block\n\tend\nend\ntable.remove(self.Hand, slot)\ntable.insert(self.DiscardPile, cardId)\nself:RenderHand(false)\nself:RenderPiles()\nself:RenderCombat()\nself:CheckCombatEnd()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "PlayCard"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "message"
|
||||
}
|
||||
],
|
||||
"Code": "log(message)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "Toast"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "amount"
|
||||
}
|
||||
],
|
||||
"Code": "local dmg = amount\nif self.EnemyBlock > 0 then\n\tlocal absorbed = math.min(self.EnemyBlock, dmg)\n\tself.EnemyBlock = self.EnemyBlock - absorbed\n\tdmg = dmg - absorbed\nend\nself.EnemyHp = self.EnemyHp - dmg\nif self.EnemyHp < 0 then\n\tself.EnemyHp = 0\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "DealDamageToEnemy"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "amount"
|
||||
}
|
||||
],
|
||||
"Code": "local dmg = amount\nif self.PlayerBlock > 0 then\n\tlocal absorbed = math.min(self.PlayerBlock, dmg)\n\tself.PlayerBlock = self.PlayerBlock - absorbed\n\tdmg = dmg - absorbed\nend\nself.PlayerHp = self.PlayerHp - dmg\nif self.PlayerHp < 0 then\n\tself.PlayerHp = 0\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "DealDamageToPlayer"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.EnemyBlock = 0\nlocal intent = self.EnemyIntents[self.EnemyIntentIndex]\nif intent ~= nil then\n\tif intent.kind == \"Attack\" then\n\t\tself:DealDamageToPlayer(intent.value)\n\telseif intent.kind == \"Defend\" then\n\t\tself.EnemyBlock = self.EnemyBlock + intent.value\n\tend\nend\nself.EnemyIntentIndex = self.EnemyIntentIndex + 1\nif self.EnemyIntentIndex > #self.EnemyIntents then\n\tself.EnemyIntentIndex = 1\nend\nself:RenderCombat()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyTurn"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "if self.EnemyHp <= 0 then\n\tself.CombatOver = true\n\tself.Gold = self.Gold + 15\n\tself:ApplyRelics(\"combatReward\")\n\tself:RenderRun()\n\tlocal node = self.MapNodes[self.CurrentNodeId]\n\tif node ~= nil and node.type == \"elite\" then\n\t\tself:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)])\n\tend\n\tif node ~= nil and node.type == \"boss\" then\n\t\tif self.Floor < self.RunLength then\n\t\t\tself.Floor = self.Floor + 1\n\t\t\tself.CurrentNodeId = \"\"\n\t\t\tself.CurrentEnemyId = \"\"\n\t\t\tself:RenderRun()\n\t\t\tself:ShowMap()\n\t\telse\n\t\t\tself:ShowResult(\"런 클리어!\")\n\t\t\tself.RunActive = false\n\t\tend\n\telse\n\t\tself:OfferReward()\n\tend\nelseif self.PlayerHp <= 0 then\n\tself.CombatOver = true\n\tself:ShowResult(\"패배...\")\n\tself.RunActive = false\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "CheckCombatEnd"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "text"
|
||||
}
|
||||
],
|
||||
"Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/Result\", text)\nlocal entity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CombatHud/Result\")\nif entity ~= nil then\n\tentity.Enable = true\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ShowResult"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/EnemyName\", self.EnemyName)\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyHp\", \"HP \" .. string.format(\"%d\", self.EnemyHp) .. \"/\" .. string.format(\"%d\", self.EnemyMaxHp))\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyBlock\", \"방어 \" .. string.format(\"%d\", self.EnemyBlock))\nlocal intent = self.EnemyIntents[self.EnemyIntentIndex]\nlocal intentText = \"\"\nif intent ~= nil then\n\tif intent.kind == \"Attack\" then\n\t\tintentText = \"의도: 공격 \" .. tostring(intent.value)\n\telseif intent.kind == \"Defend\" then\n\t\tintentText = \"의도: 방어 \" .. tostring(intent.value)\n\tend\nend\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyIntent\", intentText)\nself:SetText(\"/ui/DefaultGroup/CombatHud/PlayerHp\", \"HP \" .. string.format(\"%d\", self.PlayerHp) .. \"/\" .. string.format(\"%d\", self.PlayerMaxHp))\nself:SetText(\"/ui/DefaultGroup/CombatHud/PlayerBlock\", \"방어 \" .. string.format(\"%d\", self.PlayerBlock))\nself:RenderRun()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "RenderCombat"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/Floor\", \"막 \" .. string.format(\"%d\", self.Floor) .. \"/\" .. string.format(\"%d\", self.RunLength))\nself:SetText(\"/ui/DefaultGroup/CombatHud/Gold\", \"골드 \" .. string.format(\"%d\", self.Gold))",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "RenderRun"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local pool = {}\nfor id, _ in pairs(self.Cards) do\n\ttable.insert(pool, id)\nend\nself.RewardChoices = {}\nfor i = 1, 3 do\n\tself.RewardChoices[i] = pool[math.random(1, #pool)]\n\tself:ApplyRewardVisual(i, self.RewardChoices[i])\nend\nlocal hud = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud\")\nif hud ~= nil then\n\thud.Enable = true\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "OfferReward"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "slot"
|
||||
},
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "cardId"
|
||||
}
|
||||
],
|
||||
"Code": "local c = self.Cards[cardId]\nif c == nil then\n\treturn\nend\nlocal base = \"/ui/DefaultGroup/RewardHud/Reward\" .. tostring(slot)\nself:SetText(base .. \"/Name\", c.name)\nself:SetText(base .. \"/Cost\", tostring(c.cost))\nself:SetText(base .. \"/Desc\", c.desc)\nlocal e = _EntityService:GetEntityByPath(base)\nif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\tif c.kind == \"Attack\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\telseif c.kind == \"Skill\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\telse\n\t\te.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\tend\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ApplyRewardVisual"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "slot"
|
||||
}
|
||||
],
|
||||
"Code": "if self.CombatOver ~= true or self.RunActive ~= true then\n\treturn\nend\nif slot ~= 0 and self.RewardChoices ~= nil then\n\tlocal id = self.RewardChoices[slot]\n\tif id ~= nil then\n\t\ttable.insert(self.RunDeck, id)\n\tend\nend\nlocal hud = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud\")\nif hud ~= nil then\n\thud.Enable = false\nend\nself:ShowMap()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "PickReward"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "hook"
|
||||
}
|
||||
],
|
||||
"Code": "if self.RunRelics == nil then\n\treturn\nend\nfor i = 1, #self.RunRelics do\n\tlocal r = self.Relics[self.RunRelics[i]]\n\tif r ~= nil and r.hook == hook then\n\t\tif r.effect == \"block\" then\n\t\t\tself.PlayerBlock = self.PlayerBlock + r.value\n\t\telseif r.effect == \"energy\" then\n\t\t\tself.Energy = self.Energy + r.value\n\t\telseif r.effect == \"healOnAttack\" then\n\t\t\tself.PlayerHp = self.PlayerHp + r.value\n\t\t\tif self.PlayerHp > self.PlayerMaxHp then\n\t\t\t\tself.PlayerHp = self.PlayerMaxHp\n\t\t\tend\n\t\telseif r.effect == \"gold\" then\n\t\t\tself.Gold = self.Gold + r.value\n\t\tend\n\tend\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ApplyRelics"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "id"
|
||||
}
|
||||
],
|
||||
"Code": "if self.RunRelics == nil then\n\tself.RunRelics = {}\nend\ntable.insert(self.RunRelics, id)\nself:RenderRelics()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "AddRelic"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local names = \"\"\nif self.RunRelics ~= nil then\n\tfor i = 1, #self.RunRelics do\n\t\tlocal r = self.Relics[self.RunRelics[i]]\n\t\tif r ~= nil then\n\t\t\tif names == \"\" then\n\t\t\t\tnames = r.name\n\t\t\telse\n\t\t\t\tnames = names .. \", \" .. r.name\n\t\t\tend\n\t\tend\n\tend\nend\nif names == \"\" then\n\tnames = \"없음\"\nend\nself:SetText(\"/ui/DefaultGroup/CombatHud/Relics\", \"유물: \" .. names)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "RenderRelics"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:RenderMap()\nlocal hud = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MapHud\")\nif hud ~= nil then\n\thud.Enable = true\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ShowMap"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "boolean",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "id"
|
||||
}
|
||||
],
|
||||
"Code": "local list\nif self.CurrentNodeId == \"\" then\n\tlist = self.MapStart\nelse\n\tlocal node = self.MapNodes[self.CurrentNodeId]\n\tif node == nil then\n\t\treturn false\n\tend\n\tlist = node.next\nend\nfor i = 1, #list do\n\tif list[i] == id then\n\t\treturn true\n\tend\nend\nreturn false",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "IsReachable"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "for id, node in pairs(self.MapNodes) do\n\tlocal e = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MapHud/Node_\" .. id)\n\tif e ~= nil then\n\t\tlocal reachable = self:IsReachable(id)\n\t\tif e.SpriteGUIRendererComponent ~= nil then\n\t\t\tif reachable then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.3, 0.55, 0.85, 1)\n\t\t\telse\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)\n\t\t\tend\n\t\tend\n\t\tif e.ButtonComponent ~= nil then\n\t\t\te.ButtonComponent.Enable = reachable\n\t\tend\n\tend\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "RenderMap"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "id"
|
||||
}
|
||||
],
|
||||
"Code": "if self.RunActive ~= true then\n\treturn\nend\nif self:IsReachable(id) ~= true then\n\treturn\nend\nself.CurrentNodeId = id\nlocal hud = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MapHud\")\nif hud ~= nil then\n\thud.Enable = false\nend\nlocal node = self.MapNodes[id]\nif node.type == \"shop\" then\n\tself:ShowShop()\nelseif node.type == \"rest\" then\n\tself:ShowRest()\nelse\n\tself.CurrentEnemyId = node.enemy\n\tself:StartCombat()\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "PickNode"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local pool = {}\nfor cid, _ in pairs(self.Cards) do\n\ttable.insert(pool, cid)\nend\nself.ShopChoices = {}\nself.ShopBought = { false, false, false }\nfor i = 1, 3 do\n\tself.ShopChoices[i] = pool[math.random(1, #pool)]\nend\nself.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)]\nself.ShopRelicBought = false\nself:RenderShop()\nlocal hud = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud\")\nif hud ~= nil then\n\thud.Enable = true\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ShowShop"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:SetText(\"/ui/DefaultGroup/ShopHud/Gold\", \"골드 \" .. string.format(\"%d\", self.Gold))\nfor i = 1, 3 do\n\tlocal cid = self.ShopChoices[i]\n\tlocal c = self.Cards[cid]\n\tlocal base = \"/ui/DefaultGroup/ShopHud/Card\" .. tostring(i)\n\tif c ~= nil then\n\t\tself:SetText(base .. \"/Name\", c.name)\n\t\tself:SetText(base .. \"/Cost\", tostring(c.cost))\n\t\tself:SetText(base .. \"/Desc\", c.desc)\n\t\tself:SetText(base .. \"/Price\", string.format(\"%d\", 30) .. \" 골드\")\n\t\tlocal e = _EntityService:GetEntityByPath(base)\n\t\tif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\t\t\tif self.ShopBought[i] == true then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)\n\t\t\telseif c.kind == \"Attack\" then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\t\t\telseif c.kind == \"Skill\" then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\t\t\telse\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\t\t\tend\n\t\tend\n\tend\nend\nlocal rr = self.Relics[self.ShopRelic]\nif rr ~= nil then\n\tself:SetText(\"/ui/DefaultGroup/ShopHud/Relic/Label\", rr.name .. \" — \" .. rr.desc)\n\tself:SetText(\"/ui/DefaultGroup/ShopHud/Relic/Price\", string.format(\"%d\", 60) .. \" 골드\")\n\tlocal re = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Relic\")\n\tif re ~= nil and re.SpriteGUIRendererComponent ~= nil then\n\t\tif self.ShopRelicBought == true then\n\t\t\tre.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)\n\t\telse\n\t\t\tre.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)\n\t\tend\n\tend\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "RenderShop"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "if self.ShopRelicBought == true then\n\treturn\nend\nif self.Gold < 60 then\n\treturn\nend\nself.Gold = self.Gold - 60\nself:AddRelic(self.ShopRelic)\nself.ShopRelicBought = true\nself:RenderShop()\nself:RenderRun()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "BuyRelic"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "slot"
|
||||
}
|
||||
],
|
||||
"Code": "if self.ShopBought == nil or self.ShopBought[slot] == true then\n\treturn\nend\nif self.Gold < 30 then\n\treturn\nend\nself.Gold = self.Gold - 30\ntable.insert(self.RunDeck, self.ShopChoices[slot])\nself.ShopBought[slot] = true\nself:RenderShop()\nself:RenderRun()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "BuyCard"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local old = self.PlayerHp\nself.PlayerHp = self.PlayerHp + 30\nif self.PlayerHp > self.PlayerMaxHp then\n\tself.PlayerHp = self.PlayerMaxHp\nend\nlocal healed = self.PlayerHp - old\nself:SetText(\"/ui/DefaultGroup/RestHud/Info\", \"HP \" .. string.format(\"%d\", old) .. \" → \" .. string.format(\"%d\", self.PlayerHp) .. \" (+\" .. string.format(\"%d\", healed) .. \")\")\nself:RenderCombat()\nlocal hud = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RestHud\")\nif hud ~= nil then\n\thud.Enable = true\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ShowRest"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local s = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud\")\nif s ~= nil then\n\ts.Enable = false\nend\nlocal r = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RestHud\")\nif r ~= nil then\n\tr.Enable = false\nend\nself:ShowMap()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "LeaveNode"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://eab37efa7f0d400f94259a2df836eb8a",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/eab37efa7f0d400f94259a2df836eb8a/639163110552472416",
|
||||
"upload_hash": "AAD8C7C4E500FF8E001E85EAB181F3B19605BA9D8C8368DB28919B419515003D",
|
||||
"name": "invincible belief",
|
||||
"resource_guid": "eab37efa7f0d400f94259a2df836eb8a",
|
||||
"resource_version": "6a238b0f1a7908d59b5d8fe4"
|
||||
}
|
||||
}
|
||||
}
|
||||
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"]
|
||||
}
|
||||
33
data/enemies.json
Normal file
33
data/enemies.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"enemies": {
|
||||
"slime": {
|
||||
"name": "슬라임",
|
||||
"maxHp": 45,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Defend", "value": 8 }
|
||||
]
|
||||
},
|
||||
"slime_elite": {
|
||||
"name": "정예 슬라임",
|
||||
"maxHp": 70,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 14 },
|
||||
{ "kind": "Attack", "value": 8 },
|
||||
{ "kind": "Defend", "value": 10 }
|
||||
]
|
||||
},
|
||||
"slime_boss": {
|
||||
"name": "슬라임 킹",
|
||||
"maxHp": 120,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 18 },
|
||||
{ "kind": "Defend", "value": 12 },
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "Attack", "value": 22 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"activeEnemy": "slime"
|
||||
}
|
||||
12
data/map.json
Normal file
12
data/map.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"start": ["A", "B"],
|
||||
"nodes": {
|
||||
"A": { "type": "combat", "enemy": "slime", "row": 1, "col": -1, "next": ["C", "D"] },
|
||||
"B": { "type": "combat", "enemy": "slime", "row": 1, "col": 1, "next": ["C", "D"] },
|
||||
"C": { "type": "rest", "row": 2, "col": -1, "next": ["E", "F"] },
|
||||
"D": { "type": "shop", "row": 2, "col": 1, "next": ["E", "F"] },
|
||||
"E": { "type": "elite", "enemy": "slime_elite", "row": 3, "col": -1, "next": ["BOSS"] },
|
||||
"F": { "type": "combat", "enemy": "slime", "row": 3, "col": 1, "next": ["BOSS"] },
|
||||
"BOSS": { "type": "boss", "enemy": "slime_boss", "row": 4, "col": 0, "next": [] }
|
||||
}
|
||||
}
|
||||
10
data/relics.json
Normal file
10
data/relics.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"relics": {
|
||||
"ironHeart": { "name": "강철 심장", "desc": "전투 시작 시 방어도 +6", "hook": "combatStart", "effect": "block", "value": 6 },
|
||||
"energyCore": { "name": "에너지 코어", "desc": "턴 시작 시 에너지 +1", "hook": "turnStart", "effect": "energy", "value": 1 },
|
||||
"vampire": { "name": "흡혈 송곳니", "desc": "공격 카드 사용 시 HP +1", "hook": "cardPlayed", "effect": "healOnAttack", "value": 1 },
|
||||
"goldIdol": { "name": "황금 우상", "desc": "전투 승리 시 골드 +10", "hook": "combatReward", "effect": "gold", "value": 10 }
|
||||
},
|
||||
"startingRelic": "ironHeart",
|
||||
"relicPool": ["energyCore", "vampire", "goldIdol"]
|
||||
}
|
||||
@@ -1,42 +1,84 @@
|
||||
# SlayMaple Basic Framework
|
||||
|
||||
This project now has a small deckbuilder roguelike foundation inspired by turn-based card combat games.
|
||||
This project has a working single-combat deckbuilder loop inspired by turn-based
|
||||
card combat games. Card play is wired to real combat state (enemy/player HP,
|
||||
block, enemy intent, win/lose).
|
||||
|
||||
## Components
|
||||
## Current Components (implemented)
|
||||
|
||||
- `SlayCardCatalog`: Defines card data, starter deck composition, reward pool, and card cloning.
|
||||
- `SlayRunState`: Owns persistent run data such as HP, gold, floor, deck, relics, and card rewards.
|
||||
- `SlayCombatManager`: Runs combat turns, draw/discard/exhaust piles, energy, enemy intents, block, damage, victory, and defeat.
|
||||
- `SlayDeckController`: The single combat component, attached to the `/common`
|
||||
entity in `Global/common.gamelogic`. Handles the card-hand UI (draw/discard/
|
||||
reshuffle, energy, card-click play), card effects (damage/block), enemy
|
||||
HP/block/intent, turn flow, and victory/defeat.
|
||||
- `Monster.codeblock`: A separate field-action monster system (HP, hit event,
|
||||
respawn). **Not** part of the card combat.
|
||||
|
||||
All three components are attached to the `/common` entity in `Global/common.gamelogic`.
|
||||
All card/deck/combat artifacts (`ui/DefaultGroup.ui`,
|
||||
`RootDesk/MyDesk/SlayDeckController.codeblock`, `Global/common.gamelogic`) are
|
||||
**generated from a single source, `tools/gen-slaydeck.mjs`** (deterministic
|
||||
output — do not hand-edit; change the generator and re-run `node
|
||||
tools/gen-slaydeck.mjs`).
|
||||
|
||||
If the Maker session was already open before these files were added, reopen or reload the local workspace so the new codeblock files are imported into the editor state.
|
||||
If the Maker session was already open before these files changed, reload the
|
||||
local workspace so the regenerated codeblock/UI files are imported into the
|
||||
editor state.
|
||||
|
||||
## Prototype Flow
|
||||
## Implemented Combat Loop
|
||||
|
||||
1. `SlayRunState` starts a new run with 80 HP and a 10-card starter deck.
|
||||
2. `SlayCombatManager` starts a demo combat automatically.
|
||||
3. Each player turn refreshes energy to 3, clears block, rolls enemy intent, and draws 5 cards.
|
||||
4. Playing a card spends energy, applies damage/block/draw/energy/status effects, then sends the card to discard or exhaust.
|
||||
5. Ending the turn discards the hand, resolves enemy intent, ticks statuses, and starts the next turn.
|
||||
6. Winning combat stores the remaining HP back into the run, grants 15 gold, and generates 3 card reward options.
|
||||
1. `StartCombat` initializes player (HP 80, Block 0) and a single enemy
|
||||
(HP 45, Block 0) with a deterministic 3-step intent cycle
|
||||
(Attack 10 → Attack 6 → Defend 8), then starts the first player turn.
|
||||
2. Each player turn refreshes energy to 3, resets player block, and draws 5
|
||||
cards. The enemy's upcoming intent is shown in advance.
|
||||
3. Cards: Strike (damage 6), Defend (block 5), Bash (damage 10). Each card has
|
||||
numeric `damage`/`block` fields; starter deck is 10 cards.
|
||||
4. Playing a card spends energy: `Attack` reduces enemy HP (block absorbs
|
||||
first); `Skill` adds player block. The card moves to the discard pile.
|
||||
5. Ending the turn discards the hand, runs the enemy turn (executes the current
|
||||
intent — attack the player or gain block), advances the intent index, then
|
||||
starts the next player turn.
|
||||
6. Enemy HP 0 → victory; player HP 0 → defeat. On combat end, input is locked
|
||||
and a result text is shown (combat-reward hook reserved for the roguelike
|
||||
meta — see Planned below).
|
||||
|
||||
> Player HP (80), enemy HP (45), and intent values (10/6/8) are temporary
|
||||
> placeholders for loop verification. They will move to per-character /
|
||||
> per-enemy data (see "Data externalization" under Planned).
|
||||
|
||||
## Useful Script Calls
|
||||
|
||||
From a script attached to the same `/common` entity:
|
||||
From the `/common` entity (or a Play Test context):
|
||||
|
||||
```lua
|
||||
self.Entity.SlayCombatManager:PlayCard(1, 1)
|
||||
self.Entity.SlayCombatManager:EndPlayerTurn()
|
||||
self.Entity.SlayCombatManager:DebugPlayFirstPlayable()
|
||||
self.Entity.SlayRunState:PickReward(1)
|
||||
self.Entity.SlayCombatManager:StartCombat("elite")
|
||||
local c = _EntityService:GetEntityByPath("/common").SlayDeckController
|
||||
c:PlayCard(1) -- play the hand card in the given slot
|
||||
c:EndPlayerTurn() -- end turn → enemy turn → next turn
|
||||
c:StartCombat() -- restart combat (reset state)
|
||||
```
|
||||
|
||||
## Planned (not yet implemented) — Target Architecture
|
||||
|
||||
The originally envisioned component split for the full roguelike. Currently
|
||||
**none of these exist**; `SlayDeckController` covers only the card combat above.
|
||||
|
||||
- `SlayCardCatalog`: Card data, starter deck composition, reward pool, card cloning.
|
||||
- `SlayRunState`: Persistent run data — HP, gold, floor, deck, relics, card rewards.
|
||||
- `SlayCombatManager`: Turn flow, draw/discard/exhaust piles, energy, enemy
|
||||
intents, block, damage, victory, and defeat (the role currently played by
|
||||
`SlayDeckController`).
|
||||
|
||||
## Next Implementation Steps
|
||||
|
||||
- Add a combat UI that renders HP, block, energy, enemy intent, and 5 hand-card buttons.
|
||||
- Add a map node UI with combat, elite, shop, rest, event, and boss node types.
|
||||
- Add relic definitions and hooks such as `OnCombatStart`, `OnCardPlayed`, `OnTurnStart`, and `OnCombatReward`.
|
||||
- Add enemy move sets as data instead of the current simple intent pattern.
|
||||
- Add save/load once the run loop is playable end to end.
|
||||
- [x] Combat UI rendering HP, block, energy, enemy intent, and hand cards
|
||||
(done — `SlayDeckController` + CombatHud).
|
||||
- [x] Card play wired to real damage/block/intent/win-lose (done).
|
||||
- [ ] Externalize card/enemy data to `data/cards.json` / `data/enemies.json`,
|
||||
injected by the generator.
|
||||
- [ ] Monte-Carlo balance simulator `tools/sim-balance.mjs` (requires the data
|
||||
externalization above).
|
||||
- [ ] Map node UI with combat, elite, shop, rest, event, and boss node types.
|
||||
- [ ] Relic definitions and hooks (`OnCombatStart`, `OnCardPlayed`,
|
||||
`OnTurnStart`, `OnCombatReward`).
|
||||
- [ ] Enemy move sets as data instead of the current deterministic intent cycle.
|
||||
- [ ] Run persistence (HP/gold/floor/deck/relics) + save/load once the loop is
|
||||
playable end to end.
|
||||
|
||||
481
docs/superpowers/plans/2026-06-08-card-combat-integration.md
Normal file
481
docs/superpowers/plans/2026-06-08-card-combat-integration.md
Normal file
@@ -0,0 +1,481 @@
|
||||
# 카드 전투 통합 (TODO B) 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:** 카드 사용이 실제 적 HP·플레이어 Block·적 의도·승패에 반영되는 단일 전투 루프를 완성한다.
|
||||
|
||||
**Architecture:** 모든 변경은 `tools/gen-slaydeck.mjs` 단일 생성기에서 만든다. 적/플레이어 전투 상태는 `SlayDeckController` codeblock 내부 속성으로 보유(필드 `Monster.codeblock`과 분리). UI는 `CombatHud` 그룹으로 DeckHud와 별도 생성. 수치(플레이어 80 / 적 45 / 의도 10·6·방8)는 임시 placeholder.
|
||||
|
||||
**Tech Stack:** Node.js ESM 생성기(`gen-slaydeck.mjs`), MSW Lua codeblock, MSW UI JSON. 검증은 `node --check` + 재생성 + sha1 결정성 + 메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `tools/gen-slaydeck.mjs` — 유일한 변경 대상.
|
||||
- `upsertUi()`: `CombatHud` 그룹(적/플레이어 패널·결과 텍스트) 생성 추가, 정리 필터 확장.
|
||||
- `writeCodeblocks()`: `SlayDeckController` 속성·메서드 추가/수정.
|
||||
- 생성물(자동, 직접 편집 금지): `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock`, `Global/common.gamelogic`.
|
||||
|
||||
검증 한계: MSW codeblock Lua는 단위 테스트 러너가 없다. 자동 검증은 생성기 문법·재생성·결정성·JSON 유효성까지, 실제 동작은 메이커 Play(사용자)로 확인.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 카드 데이터 수치화 (Cards 테이블 + UI 카드 배열)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`upsertUi` 내 `cards` 배열, `writeCodeblocks` 내 `StartCombat`의 `self.Cards`)
|
||||
|
||||
- [ ] **Step 1: `upsertUi`의 카드 배열은 표시용 그대로 두되, codeblock `Cards`에 수치 필드 추가**
|
||||
|
||||
`writeCodeblocks()`의 `StartCombat` 메서드 코드에서 `self.Cards` 정의를 아래로 교체:
|
||||
|
||||
```lua
|
||||
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 },
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음 (출력 없음, exit 0)
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): 카드 데이터에 damage/block 수치 필드 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 전투 상태 속성 + StartCombat 초기화
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`writeCodeblocks` 속성 배열, `StartCombat` 메서드)
|
||||
|
||||
- [ ] **Step 1: codeblock 속성 추가**
|
||||
|
||||
`codeblock('SlayDeckController', ...)`의 properties 배열 끝에 추가:
|
||||
|
||||
```js
|
||||
prop('number', 'PlayerHp', '0'),
|
||||
prop('number', 'PlayerMaxHp', '80'),
|
||||
prop('number', 'PlayerBlock', '0'),
|
||||
prop('number', 'EnemyHp', '0'),
|
||||
prop('number', 'EnemyMaxHp', '45'),
|
||||
prop('number', 'EnemyBlock', '0'),
|
||||
prop('number', 'EnemyIntentIndex', '1'),
|
||||
prop('boolean', 'CombatOver', 'false'),
|
||||
prop('any', 'EnemyIntents'),
|
||||
prop('any', 'EnemyName'),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `StartCombat`에 전투 상태 초기화 추가**
|
||||
|
||||
`StartCombat` 코드의 맨 위(`self.MaxEnergy = 3` 직후)에 삽입:
|
||||
|
||||
```lua
|
||||
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
|
||||
```
|
||||
|
||||
그리고 `StartCombat` 끝(`self:StartPlayerTurn()` 직전)에 `self:RenderCombat()` 추가.
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): 플레이어/적 전투 상태 속성·초기화 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 전투 헬퍼 메서드 (데미지/적턴/승패/렌더)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`writeCodeblocks` methods 배열에 신규 메서드 추가)
|
||||
|
||||
`SetText`는 엔티티 nil 가드가 있어, 참조하는 UI가 Task 5에서 생성되기 전이어도 안전(no-op).
|
||||
|
||||
- [ ] **Step 1: 신규 메서드들을 methods 배열에 추가 (`Toast` 메서드 정의 뒤)**
|
||||
|
||||
```js
|
||||
method('DealDamageToEnemy', `local dmg = amount
|
||||
if self.EnemyBlock > 0 then
|
||||
local absorbed = math.min(self.EnemyBlock, dmg)
|
||||
self.EnemyBlock = self.EnemyBlock - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
self.EnemyHp = self.EnemyHp - dmg
|
||||
if self.EnemyHp < 0 then
|
||||
self.EnemyHp = 0
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||||
method('DealDamageToPlayer', `local dmg = amount
|
||||
if self.PlayerBlock > 0 then
|
||||
local absorbed = math.min(self.PlayerBlock, dmg)
|
||||
self.PlayerBlock = self.PlayerBlock - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
self.PlayerHp = self.PlayerHp - dmg
|
||||
if self.PlayerHp < 0 then
|
||||
self.PlayerHp = 0
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||||
method('EnemyTurn', `self.EnemyBlock = 0
|
||||
local intent = self.EnemyIntents[self.EnemyIntentIndex]
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
self:DealDamageToPlayer(intent.value)
|
||||
elseif intent.kind == "Defend" then
|
||||
self.EnemyBlock = self.EnemyBlock + intent.value
|
||||
end
|
||||
end
|
||||
self.EnemyIntentIndex = self.EnemyIntentIndex + 1
|
||||
if self.EnemyIntentIndex > #self.EnemyIntents then
|
||||
self.EnemyIntentIndex = 1
|
||||
end
|
||||
self:RenderCombat()`),
|
||||
method('CheckCombatEnd', `if self.EnemyHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:ShowResult("승리!")
|
||||
-- TODO(E): 전투 보상 훅 — 카드 보상/골드/유물 선택 진입점
|
||||
elseif self.PlayerHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:ShowResult("패배...")
|
||||
end`),
|
||||
method('ShowResult', `self:SetText("/ui/DefaultGroup/CombatHud/Result", text)
|
||||
local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/Result")
|
||||
if entity ~= nil then
|
||||
entity.Enable = true
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||||
method('RenderCombat', `self:SetText("/ui/DefaultGroup/CombatHud/EnemyName", self.EnemyName)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyHp", "HP " .. tostring(self.EnemyHp) .. "/" .. tostring(self.EnemyMaxHp))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyBlock", "방어 " .. tostring(self.EnemyBlock))
|
||||
local intent = self.EnemyIntents[self.EnemyIntentIndex]
|
||||
local intentText = ""
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
intentText = "의도: 공격 " .. tostring(intent.value)
|
||||
elseif intent.kind == "Defend" then
|
||||
intentText = "의도: 방어 " .. tostring(intent.value)
|
||||
end
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyIntent", intentText)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerHp", "HP " .. tostring(self.PlayerHp) .. "/" .. tostring(self.PlayerMaxHp))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerBlock", "방어 " .. tostring(self.PlayerBlock))`),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): 데미지/적턴/승패/전투렌더 헬퍼 메서드 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 턴 흐름 배선 (PlayCard 효과·EndPlayerTurn·StartPlayerTurn)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`StartPlayerTurn`, `EndPlayerTurn`, `PlayCard` 메서드 코드)
|
||||
|
||||
- [ ] **Step 1: `StartPlayerTurn` 교체**
|
||||
|
||||
```lua
|
||||
self.Turn = self.Turn + 1
|
||||
self.Energy = self.MaxEnergy
|
||||
self.PlayerBlock = 0
|
||||
self:DrawCards(5)
|
||||
self:RenderHand(true)
|
||||
self:RenderCombat()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `EndPlayerTurn` 교체**
|
||||
|
||||
```lua
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
for i = 1, #self.Hand do
|
||||
table.insert(self.DiscardPile, self.Hand[i])
|
||||
end
|
||||
self.Hand = {}
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:EnemyTurn()
|
||||
self:CheckCombatEnd()
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `PlayCard` 효과 분기 교체**
|
||||
|
||||
`PlayCard` 코드를 아래로 교체(에너지 차감 후 Toast 대신 효과 적용):
|
||||
|
||||
```lua
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
if self.Hand == nil then
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
return
|
||||
end
|
||||
if self.Energy < c.cost then
|
||||
self:Toast("에너지가 부족합니다")
|
||||
return
|
||||
end
|
||||
self.Energy = self.Energy - c.cost
|
||||
if c.kind == "Attack" then
|
||||
if c.damage ~= nil then
|
||||
self:DealDamageToEnemy(c.damage)
|
||||
end
|
||||
elseif c.kind == "Skill" then
|
||||
if c.block ~= nil then
|
||||
self.PlayerBlock = self.PlayerBlock + c.block
|
||||
end
|
||||
end
|
||||
table.remove(self.Hand, slot)
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): PlayCard 효과 분기·적턴·승패 턴흐름 배선"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: CombatHud UI 엔티티 생성
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`upsertUi`: 정리 필터 확장 + CombatHud 그룹 생성)
|
||||
|
||||
- [ ] **Step 1: 정리 필터 확장**
|
||||
|
||||
`upsertUi()` 시작부의 필터를 CombatHud도 제거하도록 교체:
|
||||
|
||||
```js
|
||||
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud') && !e.path.startsWith('/ui/DefaultGroup/CombatHud'));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: DeckHud `hud` push 직후, CombatHud 엔티티 생성 블록 추가**
|
||||
|
||||
`ui.ContentProto.Entities.push(...hud);` 직전에 아래 블록 삽입(헬퍼 `entity`/`transform`/`sprite`/`text`/`guid` 재사용):
|
||||
|
||||
```js
|
||||
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
|
||||
const combat = [];
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 0),
|
||||
path: '/ui/DefaultGroup/CombatHud',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
],
|
||||
}));
|
||||
// 적 패널 배경
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 1),
|
||||
path: '/ui/DefaultGroup/CombatHud/EnemyBg',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 170 }, pos: { x: 0, y: 300 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: PANEL_BG, type: 1 }),
|
||||
],
|
||||
}));
|
||||
const enemyTexts = [
|
||||
['EnemyName', { x: 0, y: 58 }, { x: 360, y: 44 }, '슬라임', 28, true, GOLD, 1],
|
||||
['EnemyHp', { x: 0, y: 16 }, { x: 360, y: 40 }, 'HP 45/45', 24, true, { r: 1, g: 1, b: 1, a: 1 }, 2],
|
||||
['EnemyBlock', { x: 0, y: -20 }, { x: 360, y: 36 }, '방어 0', 20, false, { r: 0.6, g: 0.8, b: 1, a: 1 }, 3],
|
||||
['EnemyIntent', { x: 0, y: -56 }, { x: 360, y: 38 }, '의도: 공격 10', 22, true, { r: 1, g: 0.72, b: 0.5, a: 1 }, 4],
|
||||
];
|
||||
let cmbN = 2;
|
||||
for (const [suffix, pos, size, value, fontSize, bold, color] of enemyTexts) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: `/ui/DefaultGroup/CombatHud/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: enemyTexts.findIndex(([s]) => s === suffix) + 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize, bold, color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 플레이어 패널 배경 + 텍스트
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: '/ui/DefaultGroup/CombatHud/PlayerBg',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 110 }, pos: { x: -760, y: -260 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: PANEL_BG, type: 1 }),
|
||||
],
|
||||
}));
|
||||
const playerTexts = [
|
||||
['PlayerHp', { x: -760, y: -238 }, { x: 280, y: 44 }, 'HP 80/80', 26, true, { r: 1, g: 1, b: 1, a: 1 }],
|
||||
['PlayerBlock', { x: -760, y: -284 }, { x: 280, y: 38 }, '방어 0', 22, false, { r: 0.6, g: 0.8, b: 1, a: 1 }],
|
||||
];
|
||||
for (const [suffix, pos, size, value, fontSize, bold, color] of playerTexts) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: `/ui/DefaultGroup/CombatHud/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 6 + playerTexts.findIndex(([s]) => s === suffix),
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize, bold, color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 결과 텍스트 (기본 숨김)
|
||||
const result = entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: '/ui/DefaultGroup/CombatHud/Result',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 8,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
result.jsonString.enable = false;
|
||||
combat.push(result);
|
||||
ui.ContentProto.Entities.push(...combat);
|
||||
```
|
||||
|
||||
`guid` 프리픽스 `'cmb'`를 위해 `guid()`의 ns 매핑에 분기 추가:
|
||||
|
||||
```js
|
||||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : 0xfe;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): CombatHud(적/플레이어 패널·결과) UI 엔티티 생성"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 재생성 + 검증
|
||||
|
||||
**Files:** 생성물 3종 (생성기 실행 결과)
|
||||
|
||||
- [ ] **Step 1: 생성기 실행**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs`
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: 생성물 JSON 유효성 확인**
|
||||
|
||||
Run: `node -e "JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8')); JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); console.log('JSON OK')"`
|
||||
Expected: `JSON OK`
|
||||
|
||||
- [ ] **Step 3: 결정성 확인 (2회 실행 동일)**
|
||||
|
||||
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: CombatHud 엔티티·전투 메서드 생성 확인**
|
||||
|
||||
Run: `grep -c "CombatHud" ui/DefaultGroup.ui; grep -c "DealDamageToEnemy\|EnemyTurn\|RenderCombat" RootDesk/MyDesk/SlayDeckController.codeblock`
|
||||
Expected: 두 값 모두 > 0
|
||||
|
||||
- [ ] **Step 5: 의도한 파일만 변경됐는지 확인**
|
||||
|
||||
Run: `git status --short`
|
||||
Expected: `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (그리고 필요 시 `Global/common.gamelogic`)만 변경.
|
||||
|
||||
- [ ] **Step 6: 생성물 커밋**
|
||||
|
||||
```bash
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock Global/common.gamelogic
|
||||
git commit -m "재생성(B): 카드 전투 통합 — 적/플레이어 전투 상태·CombatHud 반영"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 메이커 Play 수동 검증 (사용자)**
|
||||
|
||||
메이커에서 로컬 워크스페이스 reload 후 Play:
|
||||
- 타격 카드 클릭 → 적 HP 감소(적 Block 있으면 먼저 차감).
|
||||
- 방어 카드 클릭 → 플레이어 `방어` 수치 증가.
|
||||
- 턴 종료 → 적이 표시된 의도대로 공격(플레이어 Block이 피해 흡수) 또는 방어, 다음 의도 갱신.
|
||||
- 적 HP 0 → "승리!" 표시·입력 잠금 / 플레이어 HP 0 → "패배..." 표시·입력 잠금.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** 전투 상태(Task 2), 카드 수치화(Task 1), 효과 분기(Task 4), 적 의도·적 턴(Task 3·4), 승패(Task 3·4), UI 노출(Task 5) — 스펙 5개 절 모두 태스크로 매핑됨. 검증은 Task 6.
|
||||
- **Placeholder scan:** 모든 코드 단계에 실제 코드 포함. "TODO(E)"는 의도된 미래 훅 주석(스펙 명시)으로 placeholder 아님.
|
||||
- **Type consistency:** UI 경로(`/ui/DefaultGroup/CombatHud/EnemyHp` 등)가 codeblock `RenderCombat`/`ShowResult`와 Task 5 생성 경로에서 동일. 메서드명(`DealDamageToEnemy`/`DealDamageToPlayer`/`EnemyTurn`/`CheckCombatEnd`/`ShowResult`/`RenderCombat`)이 호출부(Task 4)와 정의부(Task 3)에서 일치. 카드 필드(`damage`/`block`/`kind`)가 Cards 정의(Task 1)와 PlayCard 사용(Task 4)에서 일치.
|
||||
256
docs/superpowers/plans/2026-06-08-deck-controller-fixes.md
Normal file
256
docs/superpowers/plans/2026-06-08-deck-controller-fixes.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# 덱 컨트롤러 코드리뷰 수정 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:** 코드리뷰 6건(①self바인딩 ②Card5통일 ③카드클릭=사용 ④카드데이터단일화 ⑤매직넘버 ⑥pcall)을 `tools/gen-slaydeck.mjs`에서 수정·재생성한다.
|
||||
|
||||
**Architecture:** 모든 산출물(카드 UI·DeckHud·`SlayDeckController.codeblock`·`common.gamelogic`)을 생성하는 `tools/gen-slaydeck.mjs` 단일 소스를 수정하고 재실행한다. DRY는 카드 정의를 codeblock의 `self.Cards` 테이블 프로퍼티로 단일화하고, 카드 클릭은 카드 엔티티에 `ButtonComponent`를 추가한 뒤 `PlayCard(slot)` 메서드를 클로저로 연결해 구현한다.
|
||||
|
||||
**Tech Stack:** Node.js 생성기, MSW codeblock(MapleScript/Lua), msw-maker-mcp(검증).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Modify: `tools/gen-slaydeck.mjs` — 모든 수정의 단일 소스.
|
||||
- 재생성(출력): `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock`, `Global/common.gamelogic`.
|
||||
|
||||
기준: codeblock 메서드는 `method('Name', `<lua>`, [args])`로 정의되고 끝에서 전부 `ExecSpace=6`로 설정됨. 카드 엔티티(Card1~5)는 `upsertUi`의 루프가 스타일링함. `button()` 헬퍼 존재.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 생성기 수정 (① ③ ④ ⑥ + ⑤ 일부)
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: 카드에 ButtonComponent + raycast 추가 (③ 클릭 가능)**
|
||||
|
||||
`upsertUi`의 카드 루프에서 `sp.Color = cards[i - 1].tint;` 줄 바로 다음에 아래를 추가:
|
||||
```js
|
||||
sp.RaycastTarget = true;
|
||||
const comps = card.jsonString['@components'];
|
||||
if (!comps.some((c) => c['@type'] === 'MOD.Core.ButtonComponent')) {
|
||||
comps.push(button());
|
||||
}
|
||||
if (!card.componentNames.includes('MOD.Core.ButtonComponent')) {
|
||||
card.componentNames += ',MOD.Core.ButtonComponent';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `Cards` 프로퍼티 추가 (④ 단일화 준비)**
|
||||
|
||||
`writeCodeblocks`의 properties 배열(`prop('any', 'EndTurnHandler')` 가 있는 배열)에 항목 추가:
|
||||
```js
|
||||
prop('any', 'Cards'),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: StartCombat 메서드 교체 (④ 카드 테이블 정의)**
|
||||
|
||||
`method('StartCombat', ...)` 의 Lua 본문을 아래로 교체:
|
||||
```
|
||||
self.MaxEnergy = 3
|
||||
self.Turn = 0
|
||||
self.DiscardPile = {}
|
||||
self.Hand = {}
|
||||
self.Cards = {
|
||||
Strike = { name = "타격", cost = 1, desc = "피해 6", kind = "Attack" },
|
||||
Defend = { name = "방어", cost = 1, desc = "방어도 5", kind = "Skill" },
|
||||
Bash = { name = "강타", cost = 2, desc = "피해 10", kind = "Attack" },
|
||||
}
|
||||
self.DrawPile = { "Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash" }
|
||||
self:Shuffle(self.DrawPile)
|
||||
self:BindButtons()
|
||||
self:StartPlayerTurn()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: BindButtons 교체 (① 클로저 + ③ 카드 클릭 바인딩)**
|
||||
|
||||
`method('BindButtons', ...)` 의 Lua 본문을 아래로 교체:
|
||||
```
|
||||
local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton")
|
||||
if endTurn ~= nil and endTurn.ButtonComponent ~= nil then
|
||||
if self.EndTurnHandler ~= nil then
|
||||
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
|
||||
self.EndTurnHandler = nil
|
||||
end
|
||||
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
|
||||
end
|
||||
for i = 1, 5 do
|
||||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||||
if cardEntity ~= nil and cardEntity.ButtonComponent ~= nil then
|
||||
cardEntity:ConnectEvent(ButtonClickEvent, function() self:PlayCard(i) end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 5: ApplyCardVisual 교체 (④ self.Cards 사용 + ⑥ pcall 제거)**
|
||||
|
||||
`method('ApplyCardVisual', ...)` 의 Lua 본문을 아래로 교체(인자 slot, cardId 유지):
|
||||
```
|
||||
local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Cost", tostring(c.cost))
|
||||
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Name", c.name)
|
||||
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Desc", c.desc)
|
||||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||||
if cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then
|
||||
if c.kind == "Attack" then
|
||||
cardEntity.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
|
||||
elseif c.kind == "Skill" then
|
||||
cardEntity.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
||||
else
|
||||
cardEntity.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 6: PlayCard + Toast 메서드 추가 (③)**
|
||||
|
||||
`method('AnimateCardFrom', ...)` 항목 다음(메서드 배열 안)에 두 메서드를 추가:
|
||||
```js
|
||||
method('PlayCard', `if self.Hand == nil then
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
return
|
||||
end
|
||||
if self.Energy < c.cost then
|
||||
self:Toast("에너지가 부족합니다")
|
||||
return
|
||||
end
|
||||
self.Energy = self.Energy - c.cost
|
||||
self:Toast(c.name .. " — " .. c.desc)
|
||||
table.remove(self.Hand, slot)
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
||||
```
|
||||
|
||||
(⑤: 손패/슬롯 수 5는 UI 카드 엔티티가 정확히 5개라 고정값으로 둠 — 별도 상수 불필요. 시작 에너지/MaxEnergy는 이미 프로퍼티.)
|
||||
|
||||
- [ ] **Step 7: 구문 확인 + 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node --check tools/gen-slaydeck.mjs
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "덱 컨트롤러 생성기: 핸들러 클로저화·카드데이터 단일화·카드클릭 사용·pcall 제거"
|
||||
```
|
||||
Expected: `node --check` 무출력(exit 0).
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 재생성 + 데이터 검증
|
||||
|
||||
**Files:** Modify `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock`, `Global/common.gamelogic`
|
||||
|
||||
- [ ] **Step 1: 재생성**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node tools/gen-slaydeck.mjs
|
||||
```
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: codeblock 검증**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8'));const ms=j.ContentProto.Json.Methods;const names=ms.map(m=>m.Name);console.log('has PlayCard:',names.includes('PlayCard'));console.log('has Toast:',names.includes('Toast'));const bind=ms.find(m=>m.Name==='BindButtons').Code;console.log('endturn closure:',bind.includes('function() self:EndPlayerTurn() end'));console.log('card click bind:',bind.includes('function() self:PlayCard(i) end'));const av=ms.find(m=>m.Name==='ApplyCardVisual').Code;console.log('no pcall:',!av.includes('pcall'));console.log('uses self.Cards:',av.includes('self.Cards[cardId]'));const sc=ms.find(m=>m.Name==='StartCombat').Code;console.log('Cards table:',sc.includes('self.Cards ='))"
|
||||
```
|
||||
Expected: 모두 `true`.
|
||||
|
||||
- [ ] **Step 3: UI 검증 (카드 버튼 + Card5 통일)**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const j=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const E=j.ContentProto.Entities;let okBtn=true,okImg=true;for(let i=1;i<=5;i++){const c=E.find(e=>e.path==='/ui/DefaultGroup/CardHand/Card'+i);if(!c){okBtn=false;continue;}if(!(c.componentNames||'').includes('MOD.Core.ButtonComponent'))okBtn=false;const sp=c.jsonString['@components'].find(x=>x['@type']==='MOD.Core.SpriteGUIRendererComponent');if(sp.ImageRUID.DataId!=='')okImg=false;}const c5=E.find(e=>e.path==='/ui/DefaultGroup/CardHand/Card5');const hasDesc=E.some(e=>e.path==='/ui/DefaultGroup/CardHand/Card5/Desc');console.log('all cards have Button:',okBtn);console.log('all cards no image (uniform):',okImg);console.log('Card5 has Desc child:',hasDesc)"
|
||||
```
|
||||
Expected: `all cards have Button: true`, `all cards no image (uniform): true`, `Card5 has Desc child: true`.
|
||||
|
||||
- [ ] **Step 4: JSON 유효성 + 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8'));JSON.parse(require('fs').readFileSync('Global/common.gamelogic','utf8'));console.log('JSON ok')"
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock Global/common.gamelogic
|
||||
git commit -m "재생성: 카드 클릭 사용·균일 카드·핸들러 수정 반영"
|
||||
```
|
||||
Expected: `JSON ok`.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Maker Play 검증 (컨트롤러)
|
||||
|
||||
**Files:** 없음
|
||||
|
||||
- [ ] **Step 1: reload**: `maker_refresh_workspace`.
|
||||
- [ ] **Step 2: 시작 맵 활성화 확인**: `maker_get_current_map`. (어느 맵이든 카드 UI는 전역이라 표시됨)
|
||||
- [ ] **Step 3: play**: `maker_play`.
|
||||
- [ ] **Step 4: 클릭 시뮬레이션 + 상태 확인**: `maker_execute_script`(client)로 PlayCard 직접 호출해 동작 확인:
|
||||
```lua
|
||||
local ctrl = _EntityService:GetEntityByPath("/common")
|
||||
-- 초기 상태
|
||||
local c = ctrl.SlayDeckController
|
||||
log("BEFORE energy="..tostring(c.Energy).." hand="..tostring(#c.Hand).." discard="..tostring(#c.DiscardPile))
|
||||
c:PlayCard(1)
|
||||
log("AFTER energy="..tostring(c.Energy).." hand="..tostring(#c.Hand).." discard="..tostring(#c.DiscardPile))
|
||||
```
|
||||
→ `maker_logs(normal)`에서 카드 사용 후 energy 감소·hand 감소·discard 증가 확인. (또는 `maker_mouse_input`으로 카드 클릭)
|
||||
- [ ] **Step 5: screenshot**: `maker_screenshot` → Read로 5장 균일·DeckHud(에너지/덱 카운트) 확인.
|
||||
- [ ] **Step 6: stop**: `maker_stop`.
|
||||
|
||||
문제 시: 핸들러 self·PlayCard 동작 로그로 진단 후 Task 1 수정·재생성.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: stash 복구 + 무결성 검증
|
||||
|
||||
**Files:** `map/map02.map`, `map/map05.map`, `map/map06.map`, `map/map07.map`, `map/map10.map`, `map/map11.map` (복구 대상)
|
||||
|
||||
- [ ] **Step 1: stash 적용**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git stash list
|
||||
git stash apply 2>&1 | head -20
|
||||
```
|
||||
(충돌 시 해당 파일은 main 버전 유지하고 stash 변경만 수동 반영하거나, 무의미하면 제외 — 아래 검증으로 판단)
|
||||
|
||||
- [ ] **Step 2: 무결성 검증 (몬스터/타일셋 유지 확인)**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const old=['8ef238e0d0ca4bb783aca526cff35d11','6c7130f51a654803a1c39cbe30e2f427','3e76c89ae8e7477ca871f5bbcd6f6f29','6d381bea1bcb4504b518a1fbfa0904ac','c96c11f9a3f845a4b6a27d9ca10ab103'];for(const t of ['02','05','06','07','10','11']){const j=JSON.parse(require('fs').readFileSync('map/map'+t+'.map','utf8'));const E=j.ContentProto.Entities;const ms=E.filter(e=>(e.componentNames||'').includes('script.Monster'));const sprs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.SpriteRendererComponent').SpriteRUID);const okNoOld=sprs.every(s=>!old.includes(s));const ts=E.find(e=>(e.path||'').endsWith('/TileMap')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.TileMapComponent').TileSetRUID.DataId;console.log('map'+t,'monsters='+ms.length,'noOldSprite='+okNoOld,'tileset='+(ts!=='9dfea3808bbd49a5877d8624df21b1c7'))}"
|
||||
```
|
||||
Expected: 각 맵 `monsters=2`, `noOldSprite=true`, `tileset=true`. (= 몬스터/타일셋 작업 유지됨)
|
||||
|
||||
- [ ] **Step 3: 판정 및 커밋**
|
||||
|
||||
- 무결성 OK → 복구분 커밋:
|
||||
```bash
|
||||
git add map/map02.map map/map05.map map/map06.map map/map07.map map/map10.map map/map11.map
|
||||
git commit -m "Maker 세션 재저장분(맵 02/05/06/07/10/11) 복구 포함"
|
||||
git stash drop
|
||||
```
|
||||
- 무결성 실패(작업 되돌려짐/손상) → 복구 취소하고 사용자에게 보고:
|
||||
```bash
|
||||
git checkout -- map/map02.map map/map05.map map/map06.map map/map07.map map/map10.map map/map11.map
|
||||
```
|
||||
(stash는 보존)
|
||||
|
||||
---
|
||||
|
||||
## 검증 요약
|
||||
- 생성기 `node --check` 통과
|
||||
- codeblock: PlayCard/Toast 존재, EndTurn·카드클릭 클로저, self.Cards 사용, pcall 없음
|
||||
- UI: Card1~5 ButtonComponent+raycast, 5장 균일(이미지 없음·Desc 존재)
|
||||
- Maker Play: PlayCard 호출 시 energy↓·hand↓·discard↑, 5장 균일 렌더
|
||||
- stash 복구분 무결성(몬스터2·old미사용·타일셋교체) 검증 후 포함
|
||||
475
docs/superpowers/plans/2026-06-09-balance-simulator.md
Normal file
475
docs/superpowers/plans/2026-06-09-balance-simulator.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# AI 전투 시뮬레이터 (TODO F) 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/*.json`을 입력으로 전투를 몬테카를로 N회 시뮬레이션해 승률·턴·OP 카드 리포트를 출력하는 오프라인 CLI `tools/sim-balance.mjs`.
|
||||
|
||||
**Architecture:** 순수 함수(PRNG·applyDamage·chooseAction·simulateCombat·runBatch)로 분리해 `node:test`로 단위 테스트. CLI main은 직접 실행 시에만 동작. 전투 규칙은 gen-slaydeck.mjs의 Lua를 JS로 미러, 데이터는 D의 JSON 공유.
|
||||
|
||||
**Tech Stack:** Node.js ESM, `node:test`+`node:assert`. 검증은 단위 테스트 + CLI 실행 + 결정성 + 데이터 반영.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `tools/sim-balance.mjs` — 시뮬레이터(엔진·정책·집계·리포트·CLI). 순수 함수 export.
|
||||
- Create: `tools/sim-balance.test.mjs` — 단위 테스트(node:test).
|
||||
|
||||
전투 규칙은 `tools/gen-slaydeck.mjs` Lua와 중복 → 파일 상단 동기화 주석.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: PRNG·applyDamage·loadData (기반 순수 함수)
|
||||
|
||||
**Files:**
|
||||
- Create: `tools/sim-balance.mjs`
|
||||
- Create: `tools/sim-balance.test.mjs`
|
||||
|
||||
- [ ] **Step 1: 테스트 작성 `tools/sim-balance.test.mjs`**
|
||||
|
||||
```js
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mulberry32, applyDamage } from './sim-balance.mjs';
|
||||
|
||||
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
||||
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
||||
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
||||
assert.deepEqual(applyDamage(80, 12, 10), { hp: 80, block: 2 });
|
||||
assert.deepEqual(applyDamage(3, 0, 10), { hp: 0, block: 0 });
|
||||
});
|
||||
|
||||
test('mulberry32: 동일 시드 동일 수열', () => {
|
||||
const a = mulberry32(1), b = mulberry32(1);
|
||||
assert.equal(a(), b());
|
||||
assert.equal(a(), b());
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 테스트 실패 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: FAIL (`Cannot find module './sim-balance.mjs'` 또는 export 없음)
|
||||
|
||||
- [ ] **Step 3: `tools/sim-balance.mjs` 작성(기반부)**
|
||||
|
||||
```js
|
||||
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
|
||||
// ⚠️ 전투 규칙은 tools/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것.
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
export const PLAYER_HP = 80; // 데이터 미포함 placeholder (codeblock과 일치)
|
||||
export const ENERGY = 3;
|
||||
export const HAND_SIZE = 5;
|
||||
export const MAX_TURNS = 100;
|
||||
|
||||
export function mulberry32(seed) {
|
||||
let a = seed >>> 0;
|
||||
return function () {
|
||||
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
export function shuffle(arr, rng) {
|
||||
const a = arr.slice();
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rng() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// 방어 우선 차감 후 hp 적용 → { hp, block }
|
||||
export function applyDamage(hp, block, amount) {
|
||||
let dmg = amount;
|
||||
if (block > 0) {
|
||||
const absorbed = Math.min(block, dmg);
|
||||
block -= absorbed;
|
||||
dmg -= absorbed;
|
||||
}
|
||||
hp -= dmg;
|
||||
if (hp < 0) hp = 0;
|
||||
return { hp, block };
|
||||
}
|
||||
|
||||
export function loadData() {
|
||||
const cardsData = JSON.parse(readFileSync('data/cards.json', 'utf8'));
|
||||
const enemiesData = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
|
||||
const enemy = enemiesData.enemies[enemiesData.activeEnemy];
|
||||
if (!enemy) throw new Error(`activeEnemy 없음: ${enemiesData.activeEnemy}`);
|
||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDeck, enemy };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 테스트 통과 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: PASS (2 tests)
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/sim-balance.mjs tools/sim-balance.test.mjs
|
||||
git commit -m "sim-balance(F): PRNG·applyDamage·loadData 기반 함수 + 테스트"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: chooseAction 정책 (휴리스틱 A)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/sim-balance.mjs`, `tools/sim-balance.test.mjs`
|
||||
|
||||
- [ ] **Step 1: 테스트 추가 (test.mjs 하단)**
|
||||
|
||||
```js
|
||||
import { chooseAction } from './sim-balance.mjs';
|
||||
|
||||
const CARDS = {
|
||||
Strike: { name: '타격', cost: 1, kind: 'Attack', damage: 6 },
|
||||
Defend: { name: '방어', cost: 1, kind: 'Skill', block: 5 },
|
||||
Bash: { name: '강타', cost: 2, kind: 'Attack', damage: 10 },
|
||||
};
|
||||
|
||||
test('chooseAction: 치사 가능하면 공격 선택', () => {
|
||||
// 적 hp 5, block 0, 손패 Strike(6) → 공격(인덱스 0)
|
||||
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 5, 0, { kind: 'Attack', value: 10 });
|
||||
assert.equal(idx, 0);
|
||||
});
|
||||
|
||||
test('chooseAction: 치사 불가 + 적 공격 의도면 방어 선택', () => {
|
||||
// 적 hp 40(이번 턴 못 죽임), 의도 공격 → Defend(인덱스 1)
|
||||
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 40, 0, { kind: 'Attack', value: 10 });
|
||||
assert.equal(idx, 1);
|
||||
});
|
||||
|
||||
test('chooseAction: 적 방어 의도면 공격 우선', () => {
|
||||
const idx = chooseAction(['Defend', 'Strike'], CARDS, 3, 40, 0, { kind: 'Defend', value: 8 });
|
||||
assert.equal(idx, 1);
|
||||
});
|
||||
|
||||
test('chooseAction: 사용 가능 카드 없으면 -1', () => {
|
||||
const idx = chooseAction(['Bash'], CARDS, 1, 40, 0, { kind: 'Attack', value: 10 });
|
||||
assert.equal(idx, -1);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 테스트 실패 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: FAIL (`chooseAction is not a function`)
|
||||
|
||||
- [ ] **Step 3: 구현 추가 (sim-balance.mjs)**
|
||||
|
||||
```js
|
||||
// 손패에서 다음에 낼 카드의 인덱스 반환(-1=턴 종료). hand=카드 id 배열.
|
||||
export function chooseAction(hand, cards, energy, enemyHp, enemyBlock, enemyIntent) {
|
||||
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id].cost <= energy);
|
||||
const attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
|
||||
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
|
||||
const dmgEff = (x) => (cards[x.id].damage || 0) / cards[x.id].cost;
|
||||
const blkEff = (x) => (cards[x.id].block || 0) / cards[x.id].cost;
|
||||
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
|
||||
|
||||
// 1) 치사: 에너지 한도 내 효율순 공격 데미지 합 >= 적 유효 hp?
|
||||
let e = energy, lethalDmg = 0;
|
||||
for (const x of attacks.slice().sort((a, b) => dmgEff(b) - dmgEff(a))) {
|
||||
if (cards[x.id].cost <= e) { e -= cards[x.id].cost; lethalDmg += cards[x.id].damage || 0; }
|
||||
}
|
||||
if (attacks.length && lethalDmg >= enemyHp + enemyBlock) return bestBy(attacks, dmgEff).i;
|
||||
|
||||
// 2) 적 공격 의도면 방어 우선
|
||||
if (enemyIntent && enemyIntent.kind === 'Attack' && skills.length) return bestBy(skills, blkEff).i;
|
||||
|
||||
// 3) 공격 우선, 없으면 스킬, 없으면 종료
|
||||
if (attacks.length) return bestBy(attacks, dmgEff).i;
|
||||
if (skills.length) return bestBy(skills, blkEff).i;
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 테스트 통과 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: PASS (6 tests)
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/sim-balance.mjs tools/sim-balance.test.mjs
|
||||
git commit -m "sim-balance(F): 플레이어 휴리스틱 정책 chooseAction + 테스트"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: simulateCombat 엔진
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/sim-balance.mjs`, `tools/sim-balance.test.mjs`
|
||||
|
||||
- [ ] **Step 1: 테스트 추가**
|
||||
|
||||
```js
|
||||
import { simulateCombat, mulberry32 as m32 } from './sim-balance.mjs';
|
||||
|
||||
const DATA = {
|
||||
cards: {
|
||||
Strike: { name: '타격', cost: 1, kind: 'Attack', damage: 6 },
|
||||
Defend: { name: '방어', cost: 1, kind: 'Skill', block: 5 },
|
||||
Bash: { name: '강타', cost: 2, kind: 'Attack', damage: 10 },
|
||||
},
|
||||
starterDeck: ['Strike','Strike','Strike','Strike','Strike','Defend','Defend','Defend','Defend','Bash'],
|
||||
enemy: { name: '슬라임', maxHp: 45, intents: [
|
||||
{ kind: 'Attack', value: 10 }, { kind: 'Attack', value: 6 }, { kind: 'Defend', value: 8 },
|
||||
] },
|
||||
};
|
||||
|
||||
test('simulateCombat: 결정적 결과(동일 시드)', () => {
|
||||
const r1 = simulateCombat(DATA, m32(1));
|
||||
const r2 = simulateCombat(DATA, m32(1));
|
||||
assert.deepEqual(r1, r2);
|
||||
assert.equal(typeof r1.win, 'boolean');
|
||||
assert.ok(r1.turns >= 1);
|
||||
});
|
||||
|
||||
test('simulateCombat: 약한 적이면 대체로 승리', () => {
|
||||
let wins = 0;
|
||||
for (let i = 0; i < 50; i++) if (simulateCombat(DATA, m32(i + 1)).win) wins++;
|
||||
assert.ok(wins >= 40, `예상 승리 다수, 실제 ${wins}/50`);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 테스트 실패 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: FAIL (`simulateCombat is not a function`)
|
||||
|
||||
- [ ] **Step 3: 구현 추가 (sim-balance.mjs)**
|
||||
|
||||
```js
|
||||
function bump(s, cost, dmg, blk) {
|
||||
s = s || { plays: 0, energy: 0, damage: 0, block: 0 };
|
||||
s.plays++; s.energy += cost; s.damage += dmg; s.block += blk;
|
||||
return s;
|
||||
}
|
||||
|
||||
// 단일 전투 시뮬. stats(선택): {cardId: {plays,energy,damage,block}} 누적.
|
||||
// 반환: { win, turns, playerHpRemaining, draw? }
|
||||
export function simulateCombat(data, rng, stats) {
|
||||
const { cards, starterDeck, enemy } = data;
|
||||
let drawPile = shuffle(starterDeck, rng);
|
||||
let discard = [];
|
||||
let hand = [];
|
||||
let pHp = PLAYER_HP, pBlock = 0;
|
||||
let eHp = enemy.maxHp, eBlock = 0, intentIdx = 0;
|
||||
let turns = 0;
|
||||
|
||||
function draw(n) {
|
||||
for (let k = 0; k < n; k++) {
|
||||
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
|
||||
if (drawPile.length === 0) break;
|
||||
hand.push(drawPile.pop());
|
||||
}
|
||||
}
|
||||
|
||||
while (turns < MAX_TURNS) {
|
||||
turns++;
|
||||
let energy = ENERGY; pBlock = 0; hand = []; draw(HAND_SIZE);
|
||||
while (true) {
|
||||
const intent = enemy.intents[intentIdx];
|
||||
const idx = chooseAction(hand, cards, energy, eHp, eBlock, intent);
|
||||
if (idx < 0) break;
|
||||
const id = hand[idx], c = cards[id];
|
||||
energy -= c.cost;
|
||||
if (c.kind === 'Attack') {
|
||||
const r = applyDamage(eHp, eBlock, c.damage || 0); eHp = r.hp; eBlock = r.block;
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, c.damage || 0, 0);
|
||||
} else {
|
||||
pBlock += c.block || 0;
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, 0, c.block || 0);
|
||||
}
|
||||
hand.splice(idx, 1); discard.push(id);
|
||||
if (eHp <= 0) return { win: true, turns, playerHpRemaining: pHp };
|
||||
}
|
||||
discard.push(...hand); hand = [];
|
||||
eBlock = 0;
|
||||
const intent = enemy.intents[intentIdx];
|
||||
if (intent.kind === 'Attack') { const r = applyDamage(pHp, pBlock, intent.value); pHp = r.hp; pBlock = r.block; }
|
||||
else if (intent.kind === 'Defend') { eBlock += intent.value; }
|
||||
intentIdx = (intentIdx + 1) % enemy.intents.length;
|
||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||
}
|
||||
return { win: false, turns, playerHpRemaining: pHp, draw: true };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 테스트 통과 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: PASS (8 tests)
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/sim-balance.mjs tools/sim-balance.test.mjs
|
||||
git commit -m "sim-balance(F): 단일 전투 시뮬 엔진 simulateCombat + 테스트"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: runBatch·리포트·OP 탐지·CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/sim-balance.mjs`, `tools/sim-balance.test.mjs`
|
||||
|
||||
- [ ] **Step 1: 테스트 추가**
|
||||
|
||||
```js
|
||||
import { runBatch } from './sim-balance.mjs';
|
||||
|
||||
test('runBatch: 집계 필드·승률 범위', () => {
|
||||
const r = runBatch(100, 1);
|
||||
assert.equal(r.N, 100);
|
||||
assert.ok(r.winRate >= 0 && r.winRate <= 1);
|
||||
assert.ok(r.avgTurns > 0);
|
||||
assert.ok(r.cardStats.Strike.plays > 0);
|
||||
});
|
||||
|
||||
test('runBatch: 동일 시드 동일 결과', () => {
|
||||
assert.deepEqual(runBatch(100, 7), runBatch(100, 7));
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 테스트 실패 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: FAIL (`runBatch is not a function`)
|
||||
|
||||
- [ ] **Step 3: 구현 추가 (sim-balance.mjs)**
|
||||
|
||||
```js
|
||||
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }
|
||||
function median(a) {
|
||||
if (!a.length) return 0;
|
||||
const s = a.slice().sort((x, y) => x - y), m = Math.floor(s.length / 2);
|
||||
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
||||
}
|
||||
|
||||
export function runBatch(N, seed) {
|
||||
const data = loadData();
|
||||
const rng = mulberry32(seed);
|
||||
const cardStats = {};
|
||||
let wins = 0, draws = 0;
|
||||
const turnsArr = [], hpArr = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
const r = simulateCombat(data, rng, cardStats);
|
||||
if (r.draw) draws++;
|
||||
if (r.win) { wins++; hpArr.push(r.playerHpRemaining); }
|
||||
turnsArr.push(r.turns);
|
||||
}
|
||||
return {
|
||||
N, wins, draws, losses: N - wins - draws,
|
||||
winRate: wins / N,
|
||||
avgTurns: mean(turnsArr), medianTurns: median(turnsArr),
|
||||
avgHpOnWin: mean(hpArr),
|
||||
cardStats, cards: data.cards, enemy: data.enemy, seed,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatReport(r) {
|
||||
const L = [];
|
||||
L.push(`=== 밸런스 시뮬레이션 (적: ${r.enemy.name} HP ${r.enemy.maxHp}) ===`);
|
||||
L.push(`시뮬 ${r.N}회 (seed=${r.seed})`);
|
||||
L.push(`승률: ${(r.winRate * 100).toFixed(1)}% (승 ${r.wins} / 패 ${r.losses}${r.draws ? ` / 무 ${r.draws}` : ''})`);
|
||||
L.push(`평균 턴: ${r.avgTurns.toFixed(2)} 중앙값 턴: ${r.medianTurns}`);
|
||||
L.push(`승리 시 평균 잔여 HP: ${r.avgHpOnWin.toFixed(1)} / ${PLAYER_HP}`);
|
||||
if (r.draws) L.push(`⚠️ 무승부 ${r.draws}건 (턴 상한 ${MAX_TURNS} 초과)`);
|
||||
L.push('');
|
||||
L.push('카드별:');
|
||||
// 효율 계산 + kind별 중앙값으로 OP 플래그
|
||||
const rows = Object.entries(r.cardStats).map(([id, s]) => {
|
||||
const kind = r.cards[id].kind;
|
||||
const eff = kind === 'Attack' ? s.damage / s.energy : s.block / s.energy;
|
||||
return { id, name: r.cards[id].name, kind, plays: s.plays, eff };
|
||||
});
|
||||
for (const kind of ['Attack', 'Skill']) {
|
||||
const kr = rows.filter((x) => x.kind === kind);
|
||||
if (!kr.length) continue;
|
||||
const med = median(kr.map((x) => x.eff));
|
||||
for (const x of kr) {
|
||||
const op = med > 0 && x.eff >= med * 1.5 ? ' ⚠️ OP 의심' : '';
|
||||
const unit = kind === 'Attack' ? '뎀/E' : '블록/E';
|
||||
L.push(` ${x.name}(${id2(x.id)}): 사용 ${x.plays}, 효율 ${x.eff.toFixed(2)} ${unit}${op}`);
|
||||
}
|
||||
}
|
||||
const sorted = rows.slice().sort((a, b) => b.plays - a.plays);
|
||||
if (sorted.length) L.push(`최다 사용: ${sorted[0].name} / 최소 사용: ${sorted[sorted.length - 1].name}`);
|
||||
return L.join('\n');
|
||||
}
|
||||
function id2(id) { return id; }
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let N = 2000, seed = 1;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--seed') seed = parseInt(args[++i], 10);
|
||||
else if (/^\d+$/.test(args[i])) N = parseInt(args[i], 10);
|
||||
}
|
||||
console.log(formatReport(runBatch(N, seed)));
|
||||
}
|
||||
|
||||
if (process.argv[1] && process.argv[1].endsWith('sim-balance.mjs')) main();
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 테스트 통과 확인**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: PASS (10 tests)
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/sim-balance.mjs tools/sim-balance.test.mjs
|
||||
git commit -m "sim-balance(F): runBatch·리포트·OP 탐지·CLI + 테스트"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 검증 (CLI 실행·결정성·데이터 반영)
|
||||
|
||||
**Files:** 없음(실행 검증)
|
||||
|
||||
- [ ] **Step 1: 전체 테스트**
|
||||
|
||||
Run: `node --test tools/sim-balance.test.mjs`
|
||||
Expected: PASS (10 tests, 0 fail)
|
||||
|
||||
- [ ] **Step 2: CLI 실행 (기본)**
|
||||
|
||||
Run: `node tools/sim-balance.mjs 2000`
|
||||
Expected: 승률·평균턴·승리시 잔여HP·카드별 효율 리포트 출력.
|
||||
|
||||
- [ ] **Step 3: 결정성 (동일 시드 동일 출력)**
|
||||
|
||||
Run: `node tools/sim-balance.mjs 500 --seed 3 > /tmp/r1.txt && node tools/sim-balance.mjs 500 --seed 3 > /tmp/r2.txt && diff /tmp/r1.txt /tmp/r2.txt && echo DETERMINISTIC`
|
||||
Expected: `DETERMINISTIC`
|
||||
|
||||
- [ ] **Step 4: 데이터 반영 (강타 데미지↑ → 승률·턴 변동)**
|
||||
|
||||
Run: `node tools/sim-balance.mjs 1000 --seed 1 | grep 승률` (기준값 기록) → `data/cards.json`에서 Bash.damage 10→20으로 임시 변경 → `node tools/sim-balance.mjs 1000 --seed 1 | grep 승률`(변동 확인) → `git checkout -- data/cards.json`(원복).
|
||||
Expected: 두 승률/턴 수치가 다름(데이터 반영). 원복 후 기준 복귀.
|
||||
|
||||
- [ ] **Step 5: 최종 커밋(있다면 없음 — 검증 전용)**
|
||||
|
||||
검증 전용 태스크. 변경 없음. `git status`로 `data/cards.json` 원복 확인.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** PRNG·applyDamage·loadData(Task1), 정책(Task2), 엔진(Task3), 집계·리포트·OP·CLI(Task4), 검증·데이터반영(Task5). 스펙 전 항목 매핑.
|
||||
- **Placeholder scan:** 모든 단계 실제 코드/명령. 동기화 주석은 의도된 문서.
|
||||
- **Type consistency:** `mulberry32/shuffle/applyDamage/loadData/chooseAction/simulateCombat/runBatch/formatReport` 시그니처가 정의(Task1·2·3·4)와 사용(테스트·CLI)에서 일치. `cardStats` 형태 `{plays,energy,damage,block}`가 `bump`·`runBatch`·`formatReport`에서 일치. 카드 필드 `kind/damage/block/cost`가 데이터·정책·엔진에서 일치.
|
||||
341
docs/superpowers/plans/2026-06-09-data-externalization.md
Normal file
341
docs/superpowers/plans/2026-06-09-data-externalization.md
Normal 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)에서 일치.
|
||||
206
docs/superpowers/plans/2026-06-09-floors.md
Normal file
206
docs/superpowers/plans/2026-06-09-floors.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 다음 층 / 멀티 act (TODO E6a) 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:** 보스 클리어 시 즉시 종료 대신 다음 막으로 진행(적 스케일), 최종 막 보스에서 진짜 런 클리어.
|
||||
|
||||
**Architecture:** `Floor`를 막 카운터로 재정의(1..ACT_COUNT). StartCombat에서 적을 막 배율로 스케일, CheckCombatEnd 보스 승리 시 다음 막(같은 맵 재사용)으로. 모두 `gen-slaydeck.mjs`에서 생성.
|
||||
|
||||
**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock. 검증은 node --check+재생성+결정성+메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Modify: `tools/gen-slaydeck.mjs` — ACT_COUNT 상수, StartRun(Floor=1·RunLength=ACT_COUNT), StartCombat(Floor 제거·적 스케일), CheckCombatEnd(보스 다음 막), RenderRun(막 라벨).
|
||||
|
||||
검증: MSW Lua 단위테스트 불가 → 생성기 문법·재생성·결정성·메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: ACT_COUNT 상수 + StartRun
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: ACT_COUNT 상수** — `const RELIC_PRICE = 60;` 다음에:
|
||||
|
||||
```js
|
||||
const ACT_COUNT = 3;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: StartRun의 Floor·RunLength 변경** — StartRun 코드에서 아래 두 줄을 교체:
|
||||
|
||||
기존:
|
||||
```
|
||||
self.Floor = 0
|
||||
self.RunLength = ${MAX_ROW}
|
||||
```
|
||||
신규:
|
||||
```
|
||||
self.Floor = 1
|
||||
self.RunLength = ${ACT_COUNT}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E6a): ACT_COUNT·StartRun 막 카운터 초기화"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: StartCombat — Floor 제거 + 적 막 스케일
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: Floor=node.row 블록 제거 + 적 스케일 적용** — StartCombat 코드의 아래 블록을 교체:
|
||||
|
||||
기존:
|
||||
```
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil then
|
||||
self.Floor = node.row
|
||||
end
|
||||
local enemy = self.Enemies[self.CurrentEnemyId]
|
||||
self.PlayerBlock = 0
|
||||
self.EnemyName = enemy.name
|
||||
self.EnemyMaxHp = enemy.maxHp
|
||||
self.EnemyHp = self.EnemyMaxHp
|
||||
self.EnemyBlock = 0
|
||||
self.EnemyIntents = enemy.intents
|
||||
self.EnemyIntentIndex = 1
|
||||
```
|
||||
신규:
|
||||
```
|
||||
local enemy = self.Enemies[self.CurrentEnemyId]
|
||||
local mult = 1 + (self.Floor - 1) * 0.6
|
||||
self.PlayerBlock = 0
|
||||
self.EnemyName = enemy.name
|
||||
self.EnemyMaxHp = math.floor(enemy.maxHp * mult)
|
||||
self.EnemyHp = self.EnemyMaxHp
|
||||
self.EnemyBlock = 0
|
||||
self.EnemyIntents = {}
|
||||
for i = 1, #enemy.intents do
|
||||
self.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) }
|
||||
end
|
||||
self.EnemyIntentIndex = 1
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E6a): StartCombat 적 막 스케일·Floor 제거"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: CheckCombatEnd 보스 다음 막 + RenderRun 막 라벨
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: CheckCombatEnd 보스 분기 교체** — 아래 블록을 교체:
|
||||
|
||||
기존:
|
||||
```
|
||||
if node ~= nil and node.type == "boss" then
|
||||
self:ShowResult("런 클리어!")
|
||||
self.RunActive = false
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
```
|
||||
신규:
|
||||
```
|
||||
if node ~= nil and node.type == "boss" then
|
||||
if self.Floor < self.RunLength then
|
||||
self.Floor = self.Floor + 1
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self:RenderRun()
|
||||
self:ShowMap()
|
||||
else
|
||||
self:ShowResult("런 클리어!")
|
||||
self.RunActive = false
|
||||
end
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 2: RenderRun 막 라벨** — RenderRun의 Floor 텍스트 줄을 교체:
|
||||
|
||||
기존:
|
||||
```
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/Floor", "층 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength))
|
||||
```
|
||||
신규:
|
||||
```
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/Floor", "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E6a): 보스 승리 다음 막 진행·막 라벨"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 재생성 + 검증
|
||||
|
||||
**Files:** 생성물
|
||||
|
||||
- [ ] **Step 1: 생성**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs`
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: 스케일·막 진행 코드 확인**
|
||||
|
||||
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(/mult = 1 \+ \(self.Floor - 1\) \* 0.6/.test(sc)&&/math.floor\(enemy.maxHp \* mult\)/.test(sc)?'SCALE OK':'NO SCALE'); const cc=j.ContentProto.Json.Methods.find(m=>m.Name==='CheckCombatEnd').Code; console.log(/self.Floor = self.Floor \+ 1/.test(cc)?'NEXT-ACT OK':'NO NEXT-ACT')"`
|
||||
Expected: `SCALE OK` / `NEXT-ACT 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: git status**
|
||||
|
||||
Run: `git checkout -- Global/common.gamelogic 2>/dev/null; git status --short`
|
||||
Expected: `tools/gen-slaydeck.mjs`, `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (+docs). (data 변경 없음)
|
||||
|
||||
- [ ] **Step 5: 생성물 커밋**
|
||||
|
||||
```bash
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
git commit -m "재생성(E6a): 멀티 act·적 스케일 반영"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 메이커 Play 수동 검증 (MCP)**
|
||||
|
||||
reload→Play: 1막 보스(슬라임 킹 120) 처치 → 2막 맵(Floor 2)·적 HP 스케일(슬라임 72·보스 192) → 3막 보스 처치 → "런 클리어!". HP/골드/덱/유물 막 간 유지. MCP는 PickNode/PlayCard/CheckCombatEnd 직접 호출 + 로그.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **Spec coverage:** ACT_COUNT·StartRun(Task1), StartCombat 스케일·Floor제거(Task2), 보스 다음막·막라벨(Task3), 검증(Task4). 스펙 전 항목 매핑.
|
||||
- **Placeholder scan:** 모든 단계 실제 코드/명령.
|
||||
- **Type consistency:** `Floor`(막 카운터)·`RunLength`(=ACT_COUNT)·`mult` 사용 일관. `EnemyIntents` 새 테이블 생성(공유 변형 없음). CheckCombatEnd의 `node`는 기존 정의 사용. ACT_COUNT 상수 Task1 정의·Task1·3 사용.
|
||||
493
docs/superpowers/plans/2026-06-09-map-nodes.md
Normal file
493
docs/superpowers/plans/2026-06-09-map-nodes.md
Normal file
@@ -0,0 +1,493 @@
|
||||
# 분기 맵 노드 진행 (TODO E3) 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:** 플레이어가 작성된 분기 맵(DAG)에서 다음 노드를 선택해 전투/엘리트/보스로 진행, 보스 클리어 시 "런 클리어".
|
||||
|
||||
**Architecture:** `data/map.json`(그래프)·`data/enemies.json`(다중 적)을 `gen-slaydeck.mjs`가 로드·주입. SlayDeckController에 맵 상태·네비게이션 메서드 추가, MapHud UI 생성. 자동 진행 대신 ShowMap→PickNode→StartCombat→보상→ShowMap 루프.
|
||||
|
||||
**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock/UI. 검증은 node --check+재생성+결정성+메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Create: `data/map.json` — 분기 맵.
|
||||
- Modify: `data/enemies.json` — slime_elite·slime_boss 추가.
|
||||
- Modify: `tools/gen-slaydeck.mjs` — 맵/적 로드·검증·직렬화 헬퍼, method() returnType, 속성·메서드·MapHud UI.
|
||||
|
||||
검증: MSW Lua 단위테스트 불가 → 생성기 문법·재생성·결정성·메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 데이터 + 로드·검증·직렬화 헬퍼
|
||||
|
||||
**Files:** Create `data/map.json`; Modify `data/enemies.json`, `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: `data/map.json` 작성**
|
||||
|
||||
```json
|
||||
{
|
||||
"start": ["A", "B"],
|
||||
"nodes": {
|
||||
"A": { "type": "combat", "enemy": "slime", "row": 1, "col": -1, "next": ["C", "D"] },
|
||||
"B": { "type": "combat", "enemy": "slime", "row": 1, "col": 1, "next": ["D", "E"] },
|
||||
"C": { "type": "elite", "enemy": "slime_elite", "row": 2, "col": -2, "next": ["BOSS"] },
|
||||
"D": { "type": "combat", "enemy": "slime", "row": 2, "col": 0, "next": ["BOSS"] },
|
||||
"E": { "type": "combat", "enemy": "slime", "row": 2, "col": 2, "next": ["BOSS"] },
|
||||
"BOSS": { "type": "boss", "enemy": "slime_boss", "row": 3, "col": 0, "next": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `data/enemies.json`에 엘리트·보스 추가** — `slime` 항목 다음에:
|
||||
|
||||
```json
|
||||
"slime_elite": {
|
||||
"name": "정예 슬라임",
|
||||
"maxHp": 70,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 14 },
|
||||
{ "kind": "Attack", "value": 8 },
|
||||
{ "kind": "Defend", "value": 10 }
|
||||
]
|
||||
},
|
||||
"slime_boss": {
|
||||
"name": "슬라임 킹",
|
||||
"maxHp": 120,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 18 },
|
||||
{ "kind": "Defend", "value": 12 },
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "Attack", "value": 22 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 생성기 상단에 map 로드·검증·헬퍼 추가** — `const ACTIVE_ENEMY = ...;` 다음에:
|
||||
|
||||
```js
|
||||
const MAP = JSON.parse(readFileSync('data/map.json', 'utf8'));
|
||||
for (const id of MAP.start) {
|
||||
if (!MAP.nodes[id]) throw new Error(`[gen-slaydeck] map.start에 없는 노드 id: ${id}`);
|
||||
}
|
||||
for (const [id, n] of Object.entries(MAP.nodes)) {
|
||||
if (!ENEMIES.enemies[n.enemy]) throw new Error(`[gen-slaydeck] 노드 ${id}의 enemy 없음: ${n.enemy}`);
|
||||
for (const nx of n.next) {
|
||||
if (!MAP.nodes[nx]) throw new Error(`[gen-slaydeck] 노드 ${id}.next에 없는 노드 id: ${nx}`);
|
||||
}
|
||||
}
|
||||
const MAX_ROW = Math.max(...Object.values(MAP.nodes).map((n) => n.row));
|
||||
|
||||
function luaIntentsArray(intents) {
|
||||
return '{ ' + intents.map((it) => `{ kind = ${luaStr(it.kind)}, value = ${it.value} }`).join(', ') + ' }';
|
||||
}
|
||||
function luaEnemiesTable(enemies) {
|
||||
const lines = Object.entries(enemies).map(([id, e]) =>
|
||||
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
|
||||
return `self.Enemies = {\n${lines.join('\n')}\n}`;
|
||||
}
|
||||
function luaMapNodesTable(nodes) {
|
||||
const lines = Object.entries(nodes).map(([id, n]) => {
|
||||
const nx = '{ ' + n.next.map(luaStr).join(', ') + ' }';
|
||||
return `\t${id} = { type = ${luaStr(n.type)}, enemy = ${luaStr(n.enemy)}, row = ${n.row}, col = ${n.col}, next = ${nx} },`;
|
||||
});
|
||||
return `self.MapNodes = {\n${lines.join('\n')}\n}`;
|
||||
}
|
||||
function luaStartArray(start) {
|
||||
return 'self.MapStart = { ' + start.map(luaStr).join(', ') + ' }';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: method()에 ReturnType 파라미터 추가** — 기존 method 함수를:
|
||||
|
||||
```js
|
||||
function method(Name, Code, Arguments = [], ExecSpace = 0, ReturnType = 'void') {
|
||||
return {
|
||||
Return: { Type: ReturnType, DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null },
|
||||
Arguments,
|
||||
Code,
|
||||
Scope: 2,
|
||||
ExecSpace,
|
||||
Attributes: [],
|
||||
Name,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: JSON·문법 검사**
|
||||
|
||||
Run: `node -e "JSON.parse(require('fs').readFileSync('data/map.json','utf8')); JSON.parse(require('fs').readFileSync('data/enemies.json','utf8')); console.log('JSON OK')" && node --check tools/gen-slaydeck.mjs`
|
||||
Expected: `JSON OK` + 오류 없음
|
||||
|
||||
- [ ] **Step 6: 커밋**
|
||||
|
||||
```bash
|
||||
git add data/map.json data/enemies.json tools/gen-slaydeck.mjs
|
||||
git commit -m "data(E3): 분기 맵 map.json·엘리트/보스 적 + 직렬화 헬퍼"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 맵 속성 + StartRun(맵 빌드·ShowMap)
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: 맵 상태 속성 추가** — `prop('boolean', 'RunActive', 'false'),` 다음에:
|
||||
|
||||
```js
|
||||
prop('any', 'Enemies'),
|
||||
prop('any', 'MapNodes'),
|
||||
prop('any', 'MapStart'),
|
||||
prop('string', 'CurrentNodeId', '""'),
|
||||
prop('string', 'CurrentEnemyId', '""'),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: StartRun 교체** — 맵 빌드 + ShowMap:
|
||||
|
||||
```js
|
||||
method('StartRun', `self.PlayerMaxHp = 80
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
self.Gold = 0
|
||||
self.Floor = 0
|
||||
self.RunLength = ${MAX_ROW}
|
||||
self.RunDeck = { ${CARDS.starterDeck.map(luaStr).join(', ')} }
|
||||
self.RunActive = true
|
||||
${luaEnemiesTable(ENEMIES.enemies)}
|
||||
${luaMapNodesTable(MAP.nodes)}
|
||||
${luaStartArray(MAP.start)}
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self:BindButtons()
|
||||
self:ShowMap()`),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E3): 맵 상태 속성·StartRun 맵 빌드/ShowMap"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: StartCombat·CheckCombatEnd·PickReward (맵 연동)
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: StartCombat 교체** — 적을 self.Enemies에서 로드, Floor=노드 row:
|
||||
|
||||
```js
|
||||
method('StartCombat', `self.MaxEnergy = 3
|
||||
self.Turn = 0
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil then
|
||||
self.Floor = node.row
|
||||
end
|
||||
local enemy = self.Enemies[self.CurrentEnemyId]
|
||||
self.PlayerBlock = 0
|
||||
self.EnemyName = enemy.name
|
||||
self.EnemyMaxHp = enemy.maxHp
|
||||
self.EnemyHp = self.EnemyMaxHp
|
||||
self.EnemyBlock = 0
|
||||
self.EnemyIntents = enemy.intents
|
||||
self.EnemyIntentIndex = 1
|
||||
self.CombatOver = false
|
||||
self.DiscardPile = {}
|
||||
self.Hand = {}
|
||||
${luaCardsTable(CARDS.cards)}
|
||||
self.DrawPile = {}
|
||||
for i = 1, #self.RunDeck do
|
||||
self.DrawPile[i] = self.RunDeck[i]
|
||||
end
|
||||
self:Shuffle(self.DrawPile)
|
||||
self:RenderCombat()
|
||||
self:StartPlayerTurn()`),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: CheckCombatEnd 교체** — 보스 노드면 런 클리어:
|
||||
|
||||
```js
|
||||
method('CheckCombatEnd', `if self.EnemyHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self.Gold = self.Gold + ${GOLD_PER_WIN}
|
||||
self:RenderRun()
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil and node.type == "boss" then
|
||||
self:ShowResult("런 클리어!")
|
||||
self.RunActive = false
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
elseif self.PlayerHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:ShowResult("패배...")
|
||||
self.RunActive = false
|
||||
end`),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: PickReward 마지막을 ShowMap으로** — PickReward 코드의 마지막 `self:StartCombat()`를 `self:ShowMap()`로 교체. (그 외 동일)
|
||||
|
||||
```js
|
||||
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
||||
return
|
||||
end
|
||||
if slot ~= 0 and self.RewardChoices ~= nil then
|
||||
local id = self.RewardChoices[slot]
|
||||
if id ~= nil then
|
||||
table.insert(self.RunDeck, id)
|
||||
end
|
||||
end
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
self:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E3): StartCombat 적 데이터화·보스 런클리어·보상후 맵복귀"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: ShowMap·IsReachable·PickNode·RenderMap + BindButtons
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: 맵 메서드 추가** — PickReward 메서드 다음(마지막 `]);` 직전)에 삽입:
|
||||
|
||||
```js
|
||||
method('ShowMap', `self:RenderMap()
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('IsReachable', `local list
|
||||
if self.CurrentNodeId == "" then
|
||||
list = self.MapStart
|
||||
else
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node == nil then
|
||||
return false
|
||||
end
|
||||
list = node.next
|
||||
end
|
||||
for i = 1, #list do
|
||||
if list[i] == id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
||||
method('RenderMap', `for id, node in pairs(self.MapNodes) do
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. id)
|
||||
if e ~= nil then
|
||||
local reachable = self:IsReachable(id)
|
||||
if e.SpriteGUIRendererComponent ~= nil then
|
||||
if reachable then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.3, 0.55, 0.85, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
end
|
||||
end
|
||||
if e.ButtonComponent ~= nil then
|
||||
e.ButtonComponent.Enable = reachable
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('PickNode', `if self.RunActive ~= true then
|
||||
return
|
||||
end
|
||||
if self:IsReachable(id) ~= true then
|
||||
return
|
||||
end
|
||||
self.CurrentNodeId = id
|
||||
self.CurrentEnemyId = self.MapNodes[id].enemy
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
self:StartCombat()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: BindButtons에 맵 노드 버튼 바인딩 추가** — BindButtons 코드의 마지막 `end`(skip 바인딩) 다음에 추가. BindButtons 끝부분의 skip 블록 다음에 붙이도록, skip 블록을 아래로 교체:
|
||||
|
||||
```js
|
||||
local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip")
|
||||
if skip ~= nil and skip.ButtonComponent ~= nil then
|
||||
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
|
||||
end
|
||||
local mapNodeIds = { ${Object.keys(MAP.nodes).map(luaStr).join(', ')} }
|
||||
for i = 1, #mapNodeIds do
|
||||
local nid = mapNodeIds[i]
|
||||
local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid)
|
||||
if mn ~= nil and mn.ButtonComponent ~= nil then
|
||||
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
|
||||
end
|
||||
end`),
|
||||
```
|
||||
(BindButtons 전체에서 기존 skip 블록 `local skip = ... end`)` 부분을 위 블록으로 교체)
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E3): ShowMap/IsReachable/PickNode/RenderMap·맵 노드 바인딩"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: MapHud UI 생성
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs` (`guid`, `upsertUi`)
|
||||
|
||||
- [ ] **Step 1: guid 'map' 분기** — ns 매핑에 추가:
|
||||
|
||||
```js
|
||||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : 0xfe;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 필터 확장** — upsertUi 필터에 MapHud 추가:
|
||||
|
||||
```js
|
||||
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud') && !e.path.startsWith('/ui/DefaultGroup/CombatHud') && !e.path.startsWith('/ui/DefaultGroup/RewardHud') && !e.path.startsWith('/ui/DefaultGroup/MapHud'));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: MapHud 그룹 생성** — `ui.ContentProto.Entities.push(...reward);` 다음에 삽입:
|
||||
|
||||
```js
|
||||
const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스' };
|
||||
const map = [];
|
||||
const mapHud = entity({
|
||||
id: guid('map', 0),
|
||||
path: '/ui/DefaultGroup/MapHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 7,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.9 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
mapHud.jsonString.enable = false;
|
||||
map.push(mapHud);
|
||||
map.push(entity({
|
||||
id: guid('map', 1),
|
||||
path: '/ui/DefaultGroup/MapHud/Title',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 510 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '다음 노드 선택', fontSize: 40, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
let mapN = 2;
|
||||
for (const [id, node] of Object.entries(MAP.nodes)) {
|
||||
const nodePath = `/ui/DefaultGroup/MapHud/Node_${id}`;
|
||||
const pos = { x: node.col * 180, y: node.row * 170 - 80 };
|
||||
map.push(entity({
|
||||
id: guid('map', mapN++),
|
||||
path: nodePath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: node.row,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 150, y: 80 }, pos }),
|
||||
sprite({ color: { r: 0.3, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
map.push(entity({
|
||||
id: guid('map', mapN++),
|
||||
path: `${nodePath}/Label`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 150, parentH: 80, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 144, y: 72 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: `${TYPE_KO[node.type]}\n${ENEMIES.enemies[node.enemy].name}`, fontSize: 20, bold: true }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
ui.ContentProto.Entities.push(...map);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E3): MapHud 노드 맵 UI 생성"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 재생성 + 검증
|
||||
|
||||
**Files:** 생성물
|
||||
|
||||
- [ ] **Step 1: 생성**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs`
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: 메서드·UI·적 주입 확인**
|
||||
|
||||
Run: `node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); const n=j.ContentProto.Json.Methods.map(m=>m.Name); console.log(['ShowMap','PickNode','IsReachable','RenderMap'].every(x=>n.includes(x))?'METHODS OK':'MISSING'); const sc=j.ContentProto.Json.Methods.find(m=>m.Name==='StartRun').Code; console.log(/slime_boss/.test(sc)&&/슬라임 킹/.test(sc)?'ENEMIES OK':'NO ENEMIES'); const u=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8')); const has=p=>u.ContentProto.Entities.some(e=>e.path===p); console.log(has('/ui/DefaultGroup/MapHud')&&has('/ui/DefaultGroup/MapHud/Node_BOSS')&&has('/ui/DefaultGroup/MapHud/Node_A/Label')?'UI OK':'UI MISSING')"`
|
||||
Expected: `METHODS OK` / `ENEMIES OK` / `UI 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: git status**
|
||||
|
||||
Run: `git checkout -- Global/common.gamelogic 2>/dev/null; git status --short`
|
||||
Expected: `data/*`, `tools/gen-slaydeck.mjs`, `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (+docs).
|
||||
|
||||
- [ ] **Step 5: 생성물 커밋**
|
||||
|
||||
```bash
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
git commit -m "재생성(E3): 분기 맵·다중 적 반영"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 메이커 Play 수동 검증 (MCP)**
|
||||
|
||||
reload→Play: StartRun → MapHud(A·B만 클릭 가능) → PickNode("A") → 슬라임 전투 → 승리 → 보상 → 맵(C·D 활성) → PickNode("C") → 정예 슬라임(HP70) → ... → BOSS → 슬라임킹(HP120) → 승리 → "런 클리어!". 도달 불가 노드 PickNode → 무시. MCP는 `PickNode`/`PlayCard`/`PickReward` 직접 호출 + 상태 로그로 검증.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **Spec coverage:** map.json/적(Task1), 맵 상태·StartRun(Task2), StartCombat 적데이터·보스클리어·보상후맵(Task3), Show/Pick/Reachable/RenderMap·바인딩(Task4), MapHud UI(Task5), 검증(Task6). 스펙 전 항목 매핑.
|
||||
- **Placeholder scan:** 모든 단계 실제 코드/명령.
|
||||
- **Type consistency:** 메서드 `StartRun/ShowMap/IsReachable/PickNode/RenderMap/StartCombat/CheckCombatEnd/PickReward` 정의·호출 일치. 속성 `Enemies/MapNodes/MapStart/CurrentNodeId/CurrentEnemyId` 정의(Task2)·사용(Task3·4) 일치. UI 경로 `/ui/DefaultGroup/MapHud/Node_{id}`·`/Label`가 codeblock(RenderMap/PickNode/BindButtons)·생성(Task5)에서 동일(노드 id는 map.json 키). `IsReachable`는 boolean 반환(method returnType param, Task1). enemy 필드 `name/maxHp/intents`가 데이터·luaEnemiesTable·StartCombat에서 일치.
|
||||
400
docs/superpowers/plans/2026-06-09-relics.md
Normal file
400
docs/superpowers/plans/2026-06-09-relics.md
Normal file
@@ -0,0 +1,400 @@
|
||||
# 유물 (TODO E5) 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:** 훅 기반 유물 패시브 + 3획득 경로(시작/엘리트/상점)를 추가한다.
|
||||
|
||||
**Architecture:** `data/relics.json`을 생성기가 주입(self.Relics). `ApplyRelics(hook)`을 전투시작/턴시작/카드사용/보상 4지점에서 호출. `AddRelic`을 3경로가 공유. ShopHud 유물 슬롯·상단 유물 바 UI. 모두 `gen-slaydeck.mjs`에서 생성.
|
||||
|
||||
**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock/UI. 검증은 node --check+재생성+결정성+메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Create: `data/relics.json`.
|
||||
- Modify: `tools/gen-slaydeck.mjs` — 로드/검증/직렬화, 상수, 속성, 훅 메서드(ApplyRelics/AddRelic/RenderRelics), 4지점 통합, 상점 유물(BuyRelic), UI(유물 바·상점 유물 슬롯).
|
||||
|
||||
검증: MSW Lua 단위테스트 불가 → 생성기 문법·재생성·결정성·메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 데이터 + 로드/직렬화 + 상수/속성 + 훅 메서드
|
||||
|
||||
**Files:** Create `data/relics.json`; Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: `data/relics.json` 작성**
|
||||
|
||||
```json
|
||||
{
|
||||
"relics": {
|
||||
"ironHeart": { "name": "강철 심장", "desc": "전투 시작 시 방어도 +6", "hook": "combatStart", "effect": "block", "value": 6 },
|
||||
"energyCore": { "name": "에너지 코어", "desc": "턴 시작 시 에너지 +1", "hook": "turnStart", "effect": "energy", "value": 1 },
|
||||
"vampire": { "name": "흡혈 송곳니", "desc": "공격 카드 사용 시 HP +1", "hook": "cardPlayed", "effect": "healOnAttack", "value": 1 },
|
||||
"goldIdol": { "name": "황금 우상", "desc": "전투 승리 시 골드 +10", "hook": "combatReward", "effect": "gold", "value": 10 }
|
||||
},
|
||||
"startingRelic": "ironHeart",
|
||||
"relicPool": ["energyCore", "vampire", "goldIdol"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 로드·검증·직렬화 헬퍼** — `const MAP = ...` 로드 블록 다음(MAX_ROW 정의 뒤)에 추가:
|
||||
|
||||
```js
|
||||
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
||||
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
|
||||
for (const id of RELICS.relicPool) {
|
||||
if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`);
|
||||
}
|
||||
function luaRelicsTable(relics) {
|
||||
const lines = Object.entries(relics).map(([id, r]) =>
|
||||
`\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value} },`);
|
||||
return `self.Relics = {\n${lines.join('\n')}\n}`;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: RELIC_PRICE 상수** — `const REST_HEAL = 30;` 다음에:
|
||||
|
||||
```js
|
||||
const RELIC_PRICE = 60;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 속성 추가** — `prop('any', 'ShopBought'),` 다음에:
|
||||
|
||||
```js
|
||||
prop('any', 'Relics'),
|
||||
prop('any', 'RunRelics'),
|
||||
prop('any', 'RelicPool'),
|
||||
prop('string', 'ShopRelic', '""'),
|
||||
prop('boolean', 'ShopRelicBought', 'false'),
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 훅 메서드 추가** — PickReward 메서드 다음(ShowMap 앞 아무 곳, 마지막 `]);` 전 임의 위치)에 삽입:
|
||||
|
||||
```js
|
||||
method('ApplyRelics', `if self.RunRelics == nil then
|
||||
return
|
||||
end
|
||||
for i = 1, #self.RunRelics do
|
||||
local r = self.Relics[self.RunRelics[i]]
|
||||
if r ~= nil and r.hook == hook then
|
||||
if r.effect == "block" then
|
||||
self.PlayerBlock = self.PlayerBlock + r.value
|
||||
elseif r.effect == "energy" then
|
||||
self.Energy = self.Energy + r.value
|
||||
elseif r.effect == "healOnAttack" then
|
||||
self.PlayerHp = self.PlayerHp + r.value
|
||||
if self.PlayerHp > self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
elseif r.effect == "gold" then
|
||||
self.Gold = self.Gold + r.value
|
||||
end
|
||||
end
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hook' }]),
|
||||
method('AddRelic', `if self.RunRelics == nil then
|
||||
self.RunRelics = {}
|
||||
end
|
||||
table.insert(self.RunRelics, id)
|
||||
self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
method('RenderRelics', `local names = ""
|
||||
if self.RunRelics ~= nil then
|
||||
for i = 1, #self.RunRelics do
|
||||
local r = self.Relics[self.RunRelics[i]]
|
||||
if r ~= nil then
|
||||
if names == "" then
|
||||
names = r.name
|
||||
else
|
||||
names = names .. ", " .. r.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if names == "" then
|
||||
names = "없음"
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/Relics", "유물: " .. names)`),
|
||||
```
|
||||
|
||||
- [ ] **Step 6: JSON·문법 검사**
|
||||
|
||||
Run: `node -e "JSON.parse(require('fs').readFileSync('data/relics.json','utf8')); console.log('JSON OK')" && node --check tools/gen-slaydeck.mjs`
|
||||
Expected: `JSON OK` + 오류 없음
|
||||
|
||||
- [ ] **Step 7: 커밋**
|
||||
|
||||
```bash
|
||||
git add data/relics.json tools/gen-slaydeck.mjs
|
||||
git commit -m "data(E5): 유물 데이터 + 훅 시스템(ApplyRelics/AddRelic/RenderRelics)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 훅 4지점 통합 + 시작/엘리트 획득
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: StartRun에 유물 주입·시작 유물** — StartRun 코드에서 `self.RunActive = true` 다음에 삽입:
|
||||
|
||||
```
|
||||
self.RunRelics = {}
|
||||
${luaRelicsTable(RELICS.relics)}
|
||||
self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} }
|
||||
```
|
||||
그리고 StartRun의 `self:ShowMap()` **직전**에 삽입:
|
||||
```
|
||||
self:AddRelic("${RELICS.startingRelic}")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: StartCombat에 combatStart 훅** — StartCombat 끝 `self:StartPlayerTurn()`를 아래로 교체:
|
||||
|
||||
```
|
||||
self:StartPlayerTurn()
|
||||
self:ApplyRelics("combatStart")
|
||||
self:RenderCombat()
|
||||
```
|
||||
|
||||
- [ ] **Step 3: StartPlayerTurn에 turnStart 훅** — `self.Energy = self.MaxEnergy` 다음 줄에 삽입:
|
||||
|
||||
```
|
||||
self:ApplyRelics("turnStart")
|
||||
```
|
||||
|
||||
- [ ] **Step 4: PlayCard Attack 분기에 cardPlayed 훅** — PlayCard의 Attack 분기를 교체:
|
||||
|
||||
```
|
||||
if c.kind == "Attack" then
|
||||
if c.damage ~= nil then
|
||||
self:DealDamageToEnemy(c.damage)
|
||||
end
|
||||
self:ApplyRelics("cardPlayed")
|
||||
elseif c.kind == "Skill" then
|
||||
```
|
||||
(기존: `if c.kind == "Attack" then\n\tif c.damage ~= nil then\n\t\tself:DealDamageToEnemy(c.damage)\n\tend\nelseif c.kind == "Skill" then` 에서 `end` 다음에 `\n\tself:ApplyRelics("cardPlayed")` 추가)
|
||||
|
||||
- [ ] **Step 5: CheckCombatEnd에 combatReward 훅 + 엘리트 유물** — CheckCombatEnd 승리부를 교체:
|
||||
|
||||
```
|
||||
if self.EnemyHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self.Gold = self.Gold + ${GOLD_PER_WIN}
|
||||
self:ApplyRelics("combatReward")
|
||||
self:RenderRun()
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil and node.type == "elite" then
|
||||
self:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)])
|
||||
end
|
||||
if node ~= nil and node.type == "boss" then
|
||||
self:ShowResult("런 클리어!")
|
||||
self.RunActive = false
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
elseif self.PlayerHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:ShowResult("패배...")
|
||||
self.RunActive = false
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 7: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E5): 훅 4지점 통합·시작/엘리트 유물 획득"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 상점 유물 (ShowShop/RenderShop/BuyRelic) + 바인딩
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: ShowShop에 유물 선택 추가** — ShowShop의 `self.ShopBought = { false, false, false }` 다음에:
|
||||
|
||||
```
|
||||
self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)]
|
||||
self.ShopRelicBought = false
|
||||
```
|
||||
|
||||
- [ ] **Step 2: RenderShop 끝에 유물 슬롯 렌더 + BuyRelic 메서드** — RenderShop 코드의 마지막 카드 for-loop `end` 다음(닫는 백틱 직전)에 추가:
|
||||
|
||||
```
|
||||
local rr = self.Relics[self.ShopRelic]
|
||||
if rr ~= nil then
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 골드")
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopRelicBought == true then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
그리고 RenderShop 메서드 다음에 BuyRelic 메서드 추가:
|
||||
|
||||
```js
|
||||
method('BuyRelic', `if self.ShopRelicBought == true then
|
||||
return
|
||||
end
|
||||
if self.Gold < ${RELIC_PRICE} then
|
||||
return
|
||||
end
|
||||
self.Gold = self.Gold - ${RELIC_PRICE}
|
||||
self:AddRelic(self.ShopRelic)
|
||||
self.ShopRelicBought = true
|
||||
self:RenderShop()
|
||||
self:RenderRun()`),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: BindButtons에 유물 슬롯 바인딩** — BindButtons의 shopLeave 바인딩 다음(restLeave 앞)에 삽입:
|
||||
|
||||
```
|
||||
local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
||||
if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then
|
||||
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E5): 상점 유물 슬롯·BuyRelic·바인딩"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: UI — 유물 바 + 상점 유물 슬롯
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs` (`upsertUi`)
|
||||
|
||||
- [ ] **Step 1: CombatHud 유물 바 추가** — CombatHud의 Floor/Gold for-loop 다음(`const result = entity({` 앞)에 삽입:
|
||||
|
||||
```js
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: '/ui/DefaultGroup/CombatHud/Relics',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1000, y: 40 }, pos: { x: 0, y: 430 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '유물: 없음', fontSize: 22, bold: true, color: { r: 0.8, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: ShopHud 유물 슬롯 추가** — ShopHud의 Leave 버튼 push 직전에 삽입:
|
||||
|
||||
```js
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: '/ui/DefaultGroup/ShopHud/Relic',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -190 } }),
|
||||
sprite({ color: { r: 0.7, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: '/ui/DefaultGroup/ShopHud/Relic/Label',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '유물', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: '/ui/DefaultGroup/ShopHud/Relic/Price',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '60 골드', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E5): 유물 바·상점 유물 슬롯 UI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 재생성 + 검증
|
||||
|
||||
**Files:** 생성물
|
||||
|
||||
- [ ] **Step 1: 생성**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs`
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: 메서드·UI·데이터 확인**
|
||||
|
||||
Run: `node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); const n=j.ContentProto.Json.Methods.map(m=>m.Name); console.log(['ApplyRelics','AddRelic','RenderRelics','BuyRelic'].every(x=>n.includes(x))?'METHODS OK':'MISSING'); const sr=j.ContentProto.Json.Methods.find(m=>m.Name==='StartRun').Code; console.log(/ironHeart/.test(sr)&&/강철 심장/.test(sr)?'RELICS OK':'NO RELICS'); const u=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8')); const has=p=>u.ContentProto.Entities.some(e=>e.path===p); console.log(has('/ui/DefaultGroup/CombatHud/Relics')&&has('/ui/DefaultGroup/ShopHud/Relic/Label')?'UI OK':'UI MISSING')"`
|
||||
Expected: `METHODS OK` / `RELICS OK` / `UI 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: git status**
|
||||
|
||||
Run: `git checkout -- Global/common.gamelogic 2>/dev/null; git status --short`
|
||||
Expected: `data/relics.json`, `tools/gen-slaydeck.mjs`, `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (+docs).
|
||||
|
||||
- [ ] **Step 5: 생성물 커밋**
|
||||
|
||||
```bash
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
git commit -m "재생성(E5): 유물 시스템·UI 반영"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 메이커 Play 수동 검증 (MCP)**
|
||||
|
||||
reload→Play: 시작 유물(강철심장)→전투 시작 PlayerBlock 6 / energyCore 보유 시 턴 에너지 4 / vampire 보유 시 공격 HP+1 / goldIdol 승리 골드+25 / 엘리트 승리→유물 획득(바 갱신) / 상점 유물 구매(골드-60). MCP는 AddRelic/BuyRelic/PlayCard/PickNode 직접 호출 + 로그.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **Spec coverage:** 데이터/로드/훅메서드(Task1), 4지점통합·시작·엘리트(Task2), 상점유물(Task3), UI(Task4), 검증(Task5). 스펙 전 항목 매핑.
|
||||
- **Placeholder scan:** 모든 단계 실제 코드/명령.
|
||||
- **Type consistency:** 메서드 `ApplyRelics/AddRelic/RenderRelics/BuyRelic` 정의·호출·바인딩 일치. 속성 `Relics/RunRelics/RelicPool/ShopRelic/ShopRelicBought` 정의(Task1·1)·사용(Task2·3) 일치. UI 경로 `/CombatHud/Relics`·`/ShopHud/Relic/{Label,Price}`가 codeblock(RenderRelics/RenderShop)·생성(Task4)에서 동일. 유물 필드 `name/desc/hook/effect/value` 데이터·luaRelicsTable·ApplyRelics 일치. 상수 `RELIC_PRICE` Task1 정의·Task3 사용.
|
||||
438
docs/superpowers/plans/2026-06-09-run-loop-core.md
Normal file
438
docs/superpowers/plans/2026-06-09-run-loop-core.md
Normal file
@@ -0,0 +1,438 @@
|
||||
# 런 루프 코어 (TODO E1+E2) 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:** 단일 전투를 연속 N전투 런으로 확장 — 런 상태(HP/골드/덱) 영속 + 승리 후 카드 1택 보상 + 다음 전투 + N전투 후 "런 클리어".
|
||||
|
||||
**Architecture:** 기존 `SlayDeckController`(gen-slaydeck.mjs 생성)에 런 상태·보상 메서드 추가. StartRun(영속 초기화·버튼 1회 바인딩) vs StartCombat(전투별 초기화, RunDeck에서 드로) 분리. RewardHud UI 생성.
|
||||
|
||||
**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock/UI. 검증은 node --check+재생성+결정성+메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Modify: `tools/gen-slaydeck.mjs` — 유일 변경 대상.
|
||||
- `writeCodeblocks`: 런 상수, 새 속성, OnBeginPlay/StartRun/StartCombat/BindButtons/CheckCombatEnd/OfferReward/ApplyRewardVisual/PickReward/RenderRun/RenderCombat.
|
||||
- `upsertUi`: CombatHud에 Floor/Gold, RewardHud 그룹 생성, 필터 확장, guid 'rwd' 분기.
|
||||
|
||||
MSW Lua 단위 테스트 불가 → 검증은 생성기 문법·재생성·결정성·메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 런 상수·속성·StartRun
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: 런 상수 추가** — `writeCodeblocks()` 함수 본문 첫 줄에 삽입:
|
||||
|
||||
```js
|
||||
const RUN_LENGTH = 3;
|
||||
const GOLD_PER_WIN = 15;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 새 속성 추가** — 속성 배열의 `prop('any', 'EnemyName'),` 다음에:
|
||||
|
||||
```js
|
||||
prop('any', 'RunDeck'),
|
||||
prop('number', 'Gold', '0'),
|
||||
prop('number', 'Floor', '0'),
|
||||
prop('number', 'RunLength', String(RUN_LENGTH)),
|
||||
prop('any', 'RewardChoices'),
|
||||
prop('boolean', 'RunActive', 'false'),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: OnBeginPlay → StartRun** — `method('OnBeginPlay', \`self:StartCombat()\`),` 를:
|
||||
|
||||
```js
|
||||
method('OnBeginPlay', `self:StartRun()`),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: StartRun 메서드 추가** — OnBeginPlay 다음에 삽입:
|
||||
|
||||
```js
|
||||
method('StartRun', `self.PlayerMaxHp = 80
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
self.Gold = 0
|
||||
self.Floor = 0
|
||||
self.RunLength = ${RUN_LENGTH}
|
||||
self.RunDeck = { ${CARDS.starterDeck.map(luaStr).join(', ')} }
|
||||
self.RunActive = true
|
||||
self:BindButtons()
|
||||
self:StartCombat()`),
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 6: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E1): 런 상태 속성·StartRun 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: StartCombat 수정 + BindButtons 수정
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: StartCombat 본문 교체** — `method('StartCombat', \`...\`)`의 코드를 아래로(HP 보존·Floor++·RunDeck에서 드로·BindButtons 호출 제거):
|
||||
|
||||
```js
|
||||
method('StartCombat', `self.MaxEnergy = 3
|
||||
self.Turn = 0
|
||||
self.Floor = self.Floor + 1
|
||||
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)}
|
||||
self.DrawPile = {}
|
||||
for i = 1, #self.RunDeck do
|
||||
self.DrawPile[i] = self.RunDeck[i]
|
||||
end
|
||||
self:Shuffle(self.DrawPile)
|
||||
self:RenderCombat()
|
||||
self:StartPlayerTurn()`),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: BindButtons에 보상 버튼 바인딩 추가** — BindButtons 코드 끝(마지막 `end` 다음)에 추가. 현재 마지막 부분:
|
||||
```
|
||||
for i = 1, 5 do
|
||||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||||
if cardEntity ~= nil and cardEntity.ButtonComponent ~= nil then
|
||||
cardEntity:ConnectEvent(ButtonClickEvent, function() self:PlayCard(i) end)
|
||||
end
|
||||
end
|
||||
```
|
||||
뒤에 이어붙이도록 BindButtons 코드를 아래 전체로 교체:
|
||||
|
||||
```js
|
||||
method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton")
|
||||
if endTurn ~= nil and endTurn.ButtonComponent ~= nil then
|
||||
if self.EndTurnHandler ~= nil then
|
||||
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
|
||||
self.EndTurnHandler = nil
|
||||
end
|
||||
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
|
||||
end
|
||||
for i = 1, 5 do
|
||||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||||
if cardEntity ~= nil and cardEntity.ButtonComponent ~= nil then
|
||||
cardEntity:ConnectEvent(ButtonClickEvent, function() self:PlayCard(i) end)
|
||||
end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i))
|
||||
if rc ~= nil and rc.ButtonComponent ~= nil then
|
||||
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
|
||||
end
|
||||
end
|
||||
local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip")
|
||||
if skip ~= nil and skip.ButtonComponent ~= nil then
|
||||
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
|
||||
end`),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E1): StartCombat 런 분리(HP보존·RunDeck드로)·BindButtons 1회+보상버튼"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: CheckCombatEnd·OfferReward·PickReward·RenderRun
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: CheckCombatEnd 교체** — 보상/런클리어/패배 분기:
|
||||
|
||||
```js
|
||||
method('CheckCombatEnd', `if self.EnemyHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self.Gold = self.Gold + ${GOLD_PER_WIN}
|
||||
self:RenderRun()
|
||||
if self.Floor >= self.RunLength then
|
||||
self:ShowResult("런 클리어!")
|
||||
self.RunActive = false
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
elseif self.PlayerHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:ShowResult("패배...")
|
||||
self.RunActive = false
|
||||
end`),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: OfferReward·ApplyRewardVisual·PickReward·RenderRun 추가** — RenderCombat 메서드 다음에 삽입:
|
||||
|
||||
```js
|
||||
method('OfferReward', `local pool = {}
|
||||
for id, _ in pairs(self.Cards) do
|
||||
table.insert(pool, id)
|
||||
end
|
||||
self.RewardChoices = {}
|
||||
for i = 1, 3 do
|
||||
self.RewardChoices[i] = pool[math.random(1, #pool)]
|
||||
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
||||
end
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('ApplyRewardVisual', `local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
return
|
||||
end
|
||||
local base = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot)
|
||||
self:SetText(base .. "/Name", c.name)
|
||||
self:SetText(base .. "/Cost", tostring(c.cost))
|
||||
self:SetText(base .. "/Desc", c.desc)
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if c.kind == "Attack" then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
|
||||
elseif c.kind == "Skill" then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
||||
return
|
||||
end
|
||||
if slot ~= 0 and self.RewardChoices ~= nil then
|
||||
local id = self.RewardChoices[slot]
|
||||
if id ~= nil then
|
||||
table.insert(self.RunDeck, id)
|
||||
end
|
||||
end
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
self:StartCombat()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('RenderRun', `self:SetText("/ui/DefaultGroup/CombatHud/Floor", "층 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/Gold", "골드 " .. string.format("%d", self.Gold))`),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: RenderCombat 끝에 RenderRun 호출 추가** — RenderCombat 코드의 마지막 줄(`...PlayerBlock...`) 다음에 `\nself:RenderRun()` 추가. 즉 RenderCombat 마지막을:
|
||||
```
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerBlock", "방어 " .. string.format("%d", self.PlayerBlock))
|
||||
self:RenderRun()
|
||||
```
|
||||
로. (Edit: 기존 마지막 줄 끝에 `\nself:RenderRun()` 삽입)
|
||||
|
||||
- [ ] **Step 4: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E2): 보상(OfferReward/PickReward)·런 클리어·층/골드 렌더"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: UI — CombatHud 층/골드 + RewardHud
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs` (`upsertUi`, `guid`)
|
||||
|
||||
- [ ] **Step 1: guid 'rwd' 분기 추가** — guid()의 ns 매핑을:
|
||||
|
||||
```js
|
||||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : 0xfe;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 정리 필터 확장** — upsertUi 시작부 필터를:
|
||||
|
||||
```js
|
||||
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud') && !e.path.startsWith('/ui/DefaultGroup/CombatHud') && !e.path.startsWith('/ui/DefaultGroup/RewardHud'));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: CombatHud에 Floor·Gold 텍스트 추가** — `const result = entity({` 선언 직전(즉 result 추가 전)에 삽입:
|
||||
|
||||
```js
|
||||
for (const [suffix, pos, value, color] of [
|
||||
['Floor', { x: -820, y: 480 }, '층 1/3', GOLD],
|
||||
['Gold', { x: 820, y: 480 }, '골드 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
|
||||
]) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: `/ui/DefaultGroup/CombatHud/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 240, y: 44 }, pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize: 26, bold: true, color, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: RewardHud 그룹 생성** — `ui.ContentProto.Entities.push(...combat);` 직후, `JSON.parse(JSON.stringify(ui));` 직전에 삽입:
|
||||
|
||||
```js
|
||||
const reward = [];
|
||||
const rewardHud = entity({
|
||||
id: guid('rwd', 0),
|
||||
path: '/ui/DefaultGroup/RewardHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 6,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.86 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
rewardHud.jsonString.enable = false;
|
||||
reward.push(rewardHud);
|
||||
reward.push(entity({
|
||||
id: guid('rwd', 1),
|
||||
path: '/ui/DefaultGroup/RewardHud/Title',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 64 }, pos: { x: 0, y: 300 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '보상 카드 선택', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
let rwdN = 2;
|
||||
const rewardXs = [-300, 0, 300];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`;
|
||||
reward.push(entity({
|
||||
id: guid('rwd', rwdN++),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: rewardXs[i - 1], y: 0 } }),
|
||||
sprite({ color: ATTACK, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
for (const [suffix, cfg] of [
|
||||
['Cost', { size: { x: 50, y: 50 }, pos: { x: -60, y: 95 }, value: '1', fontSize: 34, bold: true }],
|
||||
['Name', { size: { x: 160, y: 50 }, pos: { x: 0, y: 50 }, value: '카드', fontSize: 26, bold: true }],
|
||||
['Desc', { size: { x: 160, y: 82 }, pos: { x: 0, y: -80 }, value: '', fontSize: 20, bold: false }],
|
||||
]) {
|
||||
reward.push(entity({
|
||||
id: guid('rwd', rwdN++),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
reward.push(entity({
|
||||
id: guid('rwd', rwdN++),
|
||||
path: '/ui/DefaultGroup/RewardHud/Skip',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 10,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -260 } }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '건너뛰기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
ui.ContentProto.Entities.push(...reward);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 6: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E2): CombatHud 층/골드 + RewardHud(보상 카드 3+건너뛰기) UI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 재생성 + 검증
|
||||
|
||||
**Files:** 생성물 2종
|
||||
|
||||
- [ ] **Step 1: 생성**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs`
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: 메서드·UI 생성 확인**
|
||||
|
||||
Run: `node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); const n=j.ContentProto.Json.Methods.map(m=>m.Name); console.log(['StartRun','OfferReward','PickReward','RenderRun','ApplyRewardVisual'].every(x=>n.includes(x))?'METHODS OK':'MISSING'); const u=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8')); console.log(u.ContentProto.Entities.some(e=>e.path==='/ui/DefaultGroup/RewardHud')&&u.ContentProto.Entities.some(e=>e.path==='/ui/DefaultGroup/CombatHud/Gold')?'UI OK':'UI MISSING')"`
|
||||
Expected: `METHODS OK` / `UI 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: git status (의도 파일만)**
|
||||
|
||||
Run: `git checkout -- Global/common.gamelogic 2>/dev/null; git status --short`
|
||||
Expected: `tools/gen-slaydeck.mjs`, `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (+docs).
|
||||
|
||||
- [ ] **Step 5: 생성물 커밋**
|
||||
|
||||
```bash
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
git commit -m "재생성(E1+E2): 런 루프·보상 UI 반영"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 메이커 Play 수동 검증 (사용자/MCP)**
|
||||
|
||||
reload→Play: 승리 → RewardHud 카드 3장·골드+15·층 표시 → 1택 시 RunDeck+1·다음 전투(HP 유지) → 3전투째 승리 시 "런 클리어!". 패배 시 "패배...". MCP는 `PlayCard`/`EndPlayerTurn`/`PickReward` 직접 호출 + 상태 로그로 검증.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **Spec coverage:** 상수·속성·StartRun(Task1), StartCombat분리·BindButtons1회(Task2), 보상·런클리어·렌더(Task3), 층/골드·RewardHud UI(Task4), 검증(Task5). 스펙 전 항목 매핑.
|
||||
- **Placeholder scan:** 모든 단계 실제 코드/명령.
|
||||
- **Type consistency:** 메서드명 `StartRun/StartCombat/BindButtons/CheckCombatEnd/OfferReward/ApplyRewardVisual/PickReward/RenderRun/RenderCombat` 정의·호출 일치. UI 경로 `/ui/DefaultGroup/RewardHud/Reward{1..3}/{Name,Cost,Desc}`·`/Skip`·`/CombatHud/{Floor,Gold}`가 codeblock(ApplyRewardVisual/RenderRun/BindButtons)과 생성(Task4) 일치. 속성 `RunDeck/Gold/Floor/RunLength/RewardChoices/RunActive` 정의(Task1)·사용(Task2·3) 일치.
|
||||
488
docs/superpowers/plans/2026-06-09-shop-rest.md
Normal file
488
docs/superpowers/plans/2026-06-09-shop-rest.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# 상점/휴식 노드 (TODO E4) 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:** 맵에 상점(골드→카드)·휴식(HP 회복) 노드를 추가하고, 진입 시 전투 대신 상점/휴식 UI로 분기.
|
||||
|
||||
**Architecture:** `data/map.json`에 shop/rest 노드 추가(enemy 없음). SlayDeckController에 상점/휴식 메서드, PickNode 타입 분기, ShopHud/RestHud UI. 모두 `gen-slaydeck.mjs`에서 생성.
|
||||
|
||||
**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock/UI. 검증은 node --check+재생성+결정성+메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Modify: `data/map.json` — 4행, shop/rest 노드.
|
||||
- Modify: `tools/gen-slaydeck.mjs` — 검증 완화, enemy 조건부 직렬화, 상수, 속성, PickNode 분기, 상점/휴식 메서드, ShopHud/RestHud UI, MapHud y 중앙정렬.
|
||||
|
||||
검증: MSW Lua 단위테스트 불가 → 생성기 문법·재생성·결정성·메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 데이터 + 검증완화 + enemy 조건부 직렬화 + 상수·속성
|
||||
|
||||
**Files:** Modify `data/map.json`, `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: `data/map.json` 교체**
|
||||
|
||||
```json
|
||||
{
|
||||
"start": ["A", "B"],
|
||||
"nodes": {
|
||||
"A": { "type": "combat", "enemy": "slime", "row": 1, "col": -1, "next": ["C", "D"] },
|
||||
"B": { "type": "combat", "enemy": "slime", "row": 1, "col": 1, "next": ["C", "D"] },
|
||||
"C": { "type": "rest", "row": 2, "col": -1, "next": ["E", "F"] },
|
||||
"D": { "type": "shop", "row": 2, "col": 1, "next": ["E", "F"] },
|
||||
"E": { "type": "elite", "enemy": "slime_elite", "row": 3, "col": -1, "next": ["BOSS"] },
|
||||
"F": { "type": "combat", "enemy": "slime", "row": 3, "col": 1, "next": ["BOSS"] },
|
||||
"BOSS": { "type": "boss", "enemy": "slime_boss", "row": 4, "col": 0, "next": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 검증 완화 (enemy 선택적)** — 생성기의 맵 검증 루프를 교체:
|
||||
|
||||
```js
|
||||
for (const [id, n] of Object.entries(MAP.nodes)) {
|
||||
if (n.enemy && !ENEMIES.enemies[n.enemy]) throw new Error(`[gen-slaydeck] 노드 ${id}의 enemy 없음: ${n.enemy}`);
|
||||
for (const nx of n.next) {
|
||||
if (!MAP.nodes[nx]) throw new Error(`[gen-slaydeck] 노드 ${id}.next에 없는 노드 id: ${nx}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: luaMapNodesTable enemy 조건부** — 함수를 교체:
|
||||
|
||||
```js
|
||||
function luaMapNodesTable(nodes) {
|
||||
const lines = Object.entries(nodes).map(([id, n]) => {
|
||||
const nx = '{ ' + n.next.map(luaStr).join(', ') + ' }';
|
||||
const enemyField = n.enemy ? `enemy = ${luaStr(n.enemy)}, ` : '';
|
||||
return `\t${id} = { type = ${luaStr(n.type)}, ${enemyField}row = ${n.row}, col = ${n.col}, next = ${nx} },`;
|
||||
});
|
||||
return `self.MapNodes = {\n${lines.join('\n')}\n}`;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 상수 추가** — `writeCodeblocks()` 안 `const GOLD_PER_WIN = 15;` 다음에:
|
||||
|
||||
```js
|
||||
const CARD_PRICE = 30;
|
||||
const REST_HEAL = 30;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 상점 상태 속성 추가** — `prop('string', 'CurrentEnemyId', '""'),` 다음에:
|
||||
|
||||
```js
|
||||
prop('any', 'ShopChoices'),
|
||||
prop('any', 'ShopBought'),
|
||||
```
|
||||
|
||||
- [ ] **Step 6: JSON·문법 검사**
|
||||
|
||||
Run: `node -e "JSON.parse(require('fs').readFileSync('data/map.json','utf8')); console.log('JSON OK')" && node --check tools/gen-slaydeck.mjs`
|
||||
Expected: `JSON OK` + 오류 없음
|
||||
|
||||
- [ ] **Step 7: 커밋**
|
||||
|
||||
```bash
|
||||
git add data/map.json tools/gen-slaydeck.mjs
|
||||
git commit -m "data(E4): 상점/휴식 노드 맵 + enemy 선택적 검증/직렬화 + 상수/속성"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: PickNode 분기 + 상점/휴식 메서드
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: PickNode 교체 (타입 분기)**
|
||||
|
||||
```js
|
||||
method('PickNode', `if self.RunActive ~= true then
|
||||
return
|
||||
end
|
||||
if self:IsReachable(id) ~= true then
|
||||
return
|
||||
end
|
||||
self.CurrentNodeId = id
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
local node = self.MapNodes[id]
|
||||
if node.type == "shop" then
|
||||
self:ShowShop()
|
||||
elseif node.type == "rest" then
|
||||
self:ShowRest()
|
||||
else
|
||||
self.CurrentEnemyId = node.enemy
|
||||
self:StartCombat()
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 상점/휴식 메서드 추가** — PickNode 메서드 다음에 삽입:
|
||||
|
||||
```js
|
||||
method('ShowShop', `local pool = {}
|
||||
for cid, _ in pairs(self.Cards) do
|
||||
table.insert(pool, cid)
|
||||
end
|
||||
self.ShopChoices = {}
|
||||
self.ShopBought = { false, false, false }
|
||||
for i = 1, 3 do
|
||||
self.ShopChoices[i] = pool[math.random(1, #pool)]
|
||||
end
|
||||
self:RenderShop()
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "골드 " .. string.format("%d", self.Gold))
|
||||
for i = 1, 3 do
|
||||
local cid = self.ShopChoices[i]
|
||||
local c = self.Cards[cid]
|
||||
local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
||||
if c ~= nil then
|
||||
self:SetText(base .. "/Name", c.name)
|
||||
self:SetText(base .. "/Cost", tostring(c.cost))
|
||||
self:SetText(base .. "/Desc", c.desc)
|
||||
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 골드")
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopBought[i] == true then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
elseif c.kind == "Attack" then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
|
||||
elseif c.kind == "Skill" then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('BuyCard', `if self.ShopBought == nil or self.ShopBought[slot] == true then
|
||||
return
|
||||
end
|
||||
if self.Gold < ${CARD_PRICE} then
|
||||
return
|
||||
end
|
||||
self.Gold = self.Gold - ${CARD_PRICE}
|
||||
table.insert(self.RunDeck, self.ShopChoices[slot])
|
||||
self.ShopBought[slot] = true
|
||||
self:RenderShop()
|
||||
self:RenderRun()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('ShowRest', `local old = self.PlayerHp
|
||||
self.PlayerHp = self.PlayerHp + ${REST_HEAL}
|
||||
if self.PlayerHp > self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
local healed = self.PlayerHp - old
|
||||
self:SetText("/ui/DefaultGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")")
|
||||
self:RenderCombat()
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud")
|
||||
if s ~= nil then
|
||||
s.Enable = false
|
||||
end
|
||||
local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud")
|
||||
if r ~= nil then
|
||||
r.Enable = false
|
||||
end
|
||||
self:ShowMap()`),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E4): PickNode 타입 분기·상점(구매)/휴식(회복) 메서드"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: BindButtons 바인딩
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1: BindButtons 맵 노드 루프 다음에 상점/휴식 바인딩 추가** — BindButtons 코드의 맵 노드 for-loop(`...PickNode(nid)...end\nend`) 다음, 닫는 백틱 직전에 삽입. 맵 노드 루프 끝 부분을 아래로 교체:
|
||||
|
||||
```js
|
||||
for i = 1, #mapNodeIds do
|
||||
local nid = mapNodeIds[i]
|
||||
local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid)
|
||||
if mn ~= nil and mn.ButtonComponent ~= nil then
|
||||
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
|
||||
end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i))
|
||||
if sc ~= nil and sc.ButtonComponent ~= nil then
|
||||
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
|
||||
end
|
||||
end
|
||||
local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave")
|
||||
if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then
|
||||
shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||
end
|
||||
local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave")
|
||||
if restLeave ~= nil and restLeave.ButtonComponent ~= nil then
|
||||
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||
end`),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E4): 상점 구매/나가기·휴식 나가기 버튼 바인딩"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: ShopHud·RestHud UI + MapHud 4행 정렬
|
||||
|
||||
**Files:** Modify `tools/gen-slaydeck.mjs` (`guid`, `upsertUi`)
|
||||
|
||||
- [ ] **Step 1: guid 'shp'·'rst' 분기** — ns 매핑에 추가(map 분기 다음):
|
||||
|
||||
```js
|
||||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : 0xfe;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 필터 확장** — upsertUi 필터에 ShopHud·RestHud 추가:
|
||||
|
||||
```js
|
||||
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud') && !e.path.startsWith('/ui/DefaultGroup/CombatHud') && !e.path.startsWith('/ui/DefaultGroup/RewardHud') && !e.path.startsWith('/ui/DefaultGroup/MapHud') && !e.path.startsWith('/ui/DefaultGroup/ShopHud') && !e.path.startsWith('/ui/DefaultGroup/RestHud'));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: MapHud 노드 y 중앙정렬** — upsertUi의 노드 pos 계산을 교체:
|
||||
|
||||
```js
|
||||
const pos = { x: node.col * 180, y: (node.row - (MAX_ROW + 1) / 2) * 140 };
|
||||
```
|
||||
|
||||
- [ ] **Step 4: ShopHud·RestHud 생성** — `ui.ContentProto.Entities.push(...map);` 다음에 삽입:
|
||||
|
||||
```js
|
||||
const shop = [];
|
||||
const shopHud = entity({
|
||||
id: guid('shp', 0),
|
||||
path: '/ui/DefaultGroup/ShopHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 8,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
shopHud.jsonString.enable = false;
|
||||
shop.push(shopHud);
|
||||
shop.push(entity({
|
||||
id: guid('shp', 1),
|
||||
path: '/ui/DefaultGroup/ShopHud/Title',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '상점', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
shop.push(entity({
|
||||
id: guid('shp', 2),
|
||||
path: '/ui/DefaultGroup/ShopHud/Gold',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 44 }, pos: { x: 0, y: 330 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '골드 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
let shpN = 3;
|
||||
const shopXs = [-300, 0, 300];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`;
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: shopXs[i - 1], y: 20 } }),
|
||||
sprite({ color: ATTACK, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
for (const [suffix, cfg] of [
|
||||
['Cost', { size: { x: 50, y: 50 }, pos: { x: -60, y: 95 }, value: '1', fontSize: 34, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Name', { size: { x: 160, y: 50 }, pos: { x: 0, y: 50 }, value: '카드', fontSize: 26, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Desc', { size: { x: 160, y: 60 }, pos: { x: 0, y: -50 }, value: '', fontSize: 20, bold: false, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -105 }, value: '30 골드', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }],
|
||||
]) {
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : suffix === 'Desc' ? 2 : 3,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: '/ui/DefaultGroup/ShopHud/Leave',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 10,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -300 } }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
ui.ContentProto.Entities.push(...shop);
|
||||
|
||||
const rest = [];
|
||||
const restHud = entity({
|
||||
id: guid('rst', 0),
|
||||
path: '/ui/DefaultGroup/RestHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.05, g: 0.08, b: 0.06, a: 0.92 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
restHud.jsonString.enable = false;
|
||||
rest.push(restHud);
|
||||
rest.push(entity({
|
||||
id: guid('rst', 1),
|
||||
path: '/ui/DefaultGroup/RestHud/Title',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 140 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '휴식', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
rest.push(entity({
|
||||
id: guid('rst', 2),
|
||||
path: '/ui/DefaultGroup/RestHud/Info',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: 'HP 회복', fontSize: 30, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
rest.push(entity({
|
||||
id: guid('rst', 3),
|
||||
path: '/ui/DefaultGroup/RestHud/Leave',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -120 } }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
ui.ContentProto.Entities.push(...rest);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 6: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(E4): ShopHud/RestHud UI·MapHud 4행 중앙정렬"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 재생성 + 검증
|
||||
|
||||
**Files:** 생성물
|
||||
|
||||
- [ ] **Step 1: 생성**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs`
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: 메서드·UI 확인**
|
||||
|
||||
Run: `node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); const n=j.ContentProto.Json.Methods.map(m=>m.Name); console.log(['ShowShop','BuyCard','ShowRest','LeaveNode','RenderShop'].every(x=>n.includes(x))?'METHODS OK':'MISSING'); const u=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8')); const has=p=>u.ContentProto.Entities.some(e=>e.path===p); console.log(has('/ui/DefaultGroup/ShopHud/Card1/Price')&&has('/ui/DefaultGroup/RestHud/Info')&&has('/ui/DefaultGroup/MapHud/Node_D')?'UI OK':'UI MISSING')"`
|
||||
Expected: `METHODS OK` / `UI 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: git status**
|
||||
|
||||
Run: `git checkout -- Global/common.gamelogic 2>/dev/null; git status --short`
|
||||
Expected: `data/map.json`, `tools/gen-slaydeck.mjs`, `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (+docs).
|
||||
|
||||
- [ ] **Step 5: 생성물 커밋**
|
||||
|
||||
```bash
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
git commit -m "재생성(E4): 상점/휴식 노드·UI 반영"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 메이커 Play 수동 검증 (MCP)**
|
||||
|
||||
reload→Play: 맵(4행, 2행에 휴식C·상점D) → PickNode("D")→상점(카드3·골드) → BuyCard(골드≥30이면 -30·RunDeck+1·비활성; 부족하면 무시) → LeaveNode→맵 / PickNode("C")→휴식(HP+30 클램프)→LeaveNode→맵 / 전투·보스·런 클리어 회귀 확인. MCP는 PickNode/BuyCard/LeaveNode 직접 호출 + 로그.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **Spec coverage:** 맵/검증/직렬화/상수/속성(Task1), PickNode분기·상점·휴식(Task2), 바인딩(Task3), UI·MapHud정렬(Task4), 검증(Task5). 스펙 전 항목 매핑.
|
||||
- **Placeholder scan:** 모든 단계 실제 코드/명령.
|
||||
- **Type consistency:** 메서드 `PickNode/ShowShop/RenderShop/BuyCard/ShowRest/LeaveNode` 정의·호출·바인딩 일치. 속성 `ShopChoices/ShopBought` 정의(Task1)·사용(Task2) 일치. UI 경로 `/ui/DefaultGroup/ShopHud/Card{1..3}/{Name,Cost,Desc,Price}`·`/Gold`·`/Leave`, `/RestHud/{Info,Leave}`가 codeblock(RenderShop/ShowRest/BindButtons)·생성(Task4)에서 동일. 상수 `CARD_PRICE/REST_HEAL` Task1 정의·Task2 사용 일치.
|
||||
@@ -0,0 +1,74 @@
|
||||
# 카드 전투 통합 (TODO 항목 B) — 설계
|
||||
|
||||
> 작성: 2026-06-08 / 상태: 승인됨 / 근거: TODO.md 항목 B + 코드 직접 분석.
|
||||
> 선행 작업: 항목 C(미커밋 노이즈 정리) 완료 — 작업 트리 클린.
|
||||
|
||||
## 문제
|
||||
|
||||
현재 `SlayDeckController.codeblock`의 `PlayCard`는 에너지만 차감하고 `Toast(log)`만 띄운다.
|
||||
실제 전투 상태(적 HP/방어, 플레이어 HP/Block, 적 의도, 승패)가 없어 STS식 덱빌딩 루프가
|
||||
닫히지 않는다. 필드 액션 몬스터(`Monster.codeblock` — HP·피격·리스폰)는 카드 시스템과 분리돼 있다.
|
||||
|
||||
## 범위
|
||||
|
||||
**포함**: 단일 적 카드 전투 루프(데미지·방어·적 의도·턴 진행·승패), 카드 수치화, DeckHud UI 노출.
|
||||
**제외(금지)**: 로그라이크 메타(E), 신규 카드 대량 추가, 전체 데이터 외부화(D — 본 작업은 인라인 수치화까지).
|
||||
|
||||
## 단일 소스 원칙
|
||||
|
||||
모든 변경은 `tools/gen-slaydeck.mjs`에서 생성한다. `SlayDeckController.codeblock` /
|
||||
`ui/DefaultGroup.ui` / `Global/common.gamelogic`을 직접 손으로 편집하지 않는다.
|
||||
변경 = 생성기 수정 → `node tools/gen-slaydeck.mjs` 재실행.
|
||||
|
||||
## 수치는 임시 placeholder
|
||||
|
||||
> 플레이어 수치는 향후 **캐릭터 특성별**, 몬스터 수치는 **몬스터별**로 다르게 설정 예정.
|
||||
> 본 작업의 값(플레이어 80 / 적 45 / 의도 10·6·방8)은 루프 검증용 임시값이며,
|
||||
> D(데이터 외부화) 단계에서 캐릭터/몬스터별 데이터로 분리한다.
|
||||
|
||||
## 설계
|
||||
|
||||
### 1) 전투 상태 (codeblock 속성 추가)
|
||||
- 플레이어: `PlayerHp`, `PlayerMaxHp`(임시 80), `PlayerBlock`
|
||||
- 적: `EnemyHp`, `EnemyMaxHp`(임시 45), `EnemyBlock`, `EnemyIntentIndex`
|
||||
- `CombatOver`(승패 후 입력 잠금)
|
||||
- 적은 codeblock 내부 상태로 보유(필드 `Monster.codeblock`과 분리).
|
||||
|
||||
### 2) 카드 데이터 수치화 (desc 파싱 폐기)
|
||||
| id | 이름 | cost | kind | 효과 |
|
||||
|----|------|------|------|------|
|
||||
| Strike | 타격 | 1 | Attack | damage 6 |
|
||||
| Defend | 방어 | 1 | Skill | block 5 |
|
||||
| Bash | 강타 | 2 | Attack | damage 10 |
|
||||
|
||||
`desc`는 표시용으로만 유지. 효과는 `damage`/`block` 숫자 필드로 처리.
|
||||
시작 덱: Strike×5, Defend×4, Bash×1 (10장).
|
||||
|
||||
### 3) 적 의도 — 결정적 사이클 (사용자 선택: A안)
|
||||
- 의도 사이클(3스텝 회전): `[공격 10] → [공격 6] → [방어 8]`
|
||||
- 매 플레이어 턴 시작 시 **다음 의도를 미리 표시**.
|
||||
- 결정적이라 F(밸런스 시뮬레이터)에서 동일 규칙 재현 가능.
|
||||
|
||||
### 4) 전투 규칙 (STS 관례)
|
||||
- 데미지는 **방어도 먼저 차감** 후 잔여만 HP에 적용.
|
||||
- 플레이어 Block은 **플레이어 턴 시작 시 0 리셋**, 적 Block은 **적 턴 시작 시 리셋**.
|
||||
- `PlayCard(slot)`: `kind=="Attack"` → 적 HP 감소(적 Block 우선 차감);
|
||||
`kind=="Skill"` → 플레이어 Block 증가.
|
||||
- `EndPlayerTurn` → 적 턴: 적 Block 리셋 → 현재 의도 실행(공격이면 플레이어에 피해,
|
||||
방어면 적 Block↑) → 의도 인덱스 전진 → 패배 체크 → 다음 플레이어 턴(Block/에너지 리셋·드로우)
|
||||
→ 다음 의도 표시.
|
||||
- 승패: 적 HP≤0 → 승리 / 플레이어 HP≤0 → 패배. 승패 시 `CombatOver=true`로 입력 잠금 +
|
||||
결과 텍스트 표시 + **보상 훅 자리(E용 주석)**.
|
||||
|
||||
### 5) UI — DeckHud 엔티티 추가 (생성기 생성)
|
||||
- 상단 적 패널: 적 이름 · `HP 45/45` · `방어 0` · `의도: 공격 10`
|
||||
- 좌측 플레이어 패널: `HP 80/80` · `방어 0`
|
||||
- 승패 결과 텍스트(중앙, 평소 숨김 → 승패 시 표시).
|
||||
|
||||
## 검증 (메이커 Play)
|
||||
- 타격 카드 → 적 HP 감소(적 Block 있으면 먼저 차감).
|
||||
- 방어 카드 → 플레이어 Block 증가.
|
||||
- 턴 종료 → 적이 표시된 의도대로 공격(플레이어 Block이 피해 흡수) 또는 방어.
|
||||
- 적 HP 0 → 승리 / 플레이어 HP 0 → 패배, 입력 잠금.
|
||||
- `node tools/gen-slaydeck.mjs` 2회 실행 결과 동일(결정적).
|
||||
- `git status` — 의도한 생성물만 변경.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 덱 컨트롤러 코드리뷰 수정 설계
|
||||
|
||||
- 날짜: 2026-06-08
|
||||
- 브랜치: feature/deck-controller-fixes (main 기준)
|
||||
- 대상: `tools/gen-slaydeck.mjs` (단일 소스) → 재생성으로 `ui/DefaultGroup.ui`·`RootDesk/MyDesk/SlayDeckController.codeblock`·`Global/common.gamelogic` 갱신
|
||||
|
||||
## 배경
|
||||
|
||||
PR #6의 `SlayDeckController` 코드 리뷰에서 6건을 발견. 모든 산출물(카드 UI·DeckHud·codeblock·common 패치)은 `tools/gen-slaydeck.mjs` 한 곳에서 생성되므로, 이 생성기를 고치고 재실행하면 전부 반영된다.
|
||||
|
||||
## 수정 항목
|
||||
|
||||
- **① [Important] EndTurn 핸들러 self 바인딩**: `buttonEntity:ConnectEvent(ButtonClickEvent, self.EndPlayerTurn)` → `ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)`. 메서드 직접 전달 시 self가 event로 잘못 바인딩되는 문제 제거. (타이머는 이미 클로저 사용 — 일관성)
|
||||
- **② [Important] Card5 이미지 충돌**: 이미 `gen-slaydeck.upsertUi`가 Card1~5를 동일 텍스트 카드로 통일(ImageRUID='', 틴트, Cost/Name/Desc 추가)하므로 재생성으로 해결됨. 추가 코드 변경 없음 — 검증만.
|
||||
- **③ [기능] 카드 클릭 = 사용**:
|
||||
- `upsertUi`의 카드 스타일 루프에서 Card1~5에 `ButtonComponent` 추가 + 카드 스프라이트 `RaycastTarget=true`.
|
||||
- codeblock에 `PlayCard(slot)` 메서드 추가: `Hand[slot]`의 카드 코스트를 `CARDS`에서 조회 → `Energy >= cost`면 `Energy -= cost`, 효과 표시(토스트/로그, 예: "타격 — 피해 6"), `Hand`에서 제거 후 `DiscardPile`에 삽입, `RenderHand(false)`+`RenderPiles()`. 부족하면 사용 불가(토스트/로그).
|
||||
- `BindButtons`에서 각 카드의 `ButtonClickEvent`를 `function() self:PlayCard(i) end` 클로저로 연결(루프 변수 i는 Lua에서 반복마다 새 지역변수라 안전). 재연결 전 이전 핸들러 해제.
|
||||
- **④ [Minor] 카드 데이터 단일화**: `CARDS = { Strike={name,cost,desc,kind}, Defend={...}, Bash={...} }` 테이블을 codeblock 상단에 두고, 시작덱 구성·`ApplyCardVisual`·`PlayCard`가 공유(if/elseif 중복 제거).
|
||||
- **⑤ [Minor] 매직넘버 상수화**: 손패/드로우 수(5), 시작 에너지(3) 등 의미 있는 상수로.
|
||||
- **⑥ [Nit] pcall 제거**: `ApplyCardVisual`의 `pcall(function() return Color(...) end)` → 직접 `Color(...)` 호출(틴트는 `CARDS[id].kind`별 색).
|
||||
|
||||
## 효과 표시(③)
|
||||
|
||||
적/데미지 시스템이 없으므로 카드 사용 효과는 **토스트 또는 로그**로만 표현(예: `log("타격 — 피해 6")` 또는 UIToast). 실제 데미지 적용은 범위 밖.
|
||||
|
||||
## 재생성·검증
|
||||
|
||||
1. `node --check tools/gen-slaydeck.mjs` → `node tools/gen-slaydeck.mjs`
|
||||
2. 검증(데이터): codeblock에 `PlayCard` 존재, `BindButtons`/EndTurn이 클로저, `CARDS` 단일 테이블, `ApplyCardVisual`에 pcall 없음. DefaultGroup.ui의 Card1~5에 `ButtonComponent` + RaycastTarget true, Card5가 균일 텍스트 카드(ImageRUID 빈값·Cost/Name/Desc 존재).
|
||||
3. Maker Play: 카드 클릭 → 에너지 감소·카드가 버림더미로·재렌더, EndTurn 버튼 동작, 5장 균일.
|
||||
|
||||
## stash 복구
|
||||
이전 Maker 세션에서 stash해 둔 로컬 맵 변경(map02/05/06/07/10/11)을 이 브랜치에 복구해 포함. 단 복구분이 몬스터/타일셋 작업을 유지하는지(되돌리지 않는지) 무결성 검증 후 커밋. 손상/무의미하면 사용자에게 알리고 제외.
|
||||
|
||||
## 범위 밖 (YAGNI)
|
||||
적 턴, 카드 효과의 실제 전투 적용, 신규 카드 종류.
|
||||
@@ -0,0 +1,68 @@
|
||||
# AI 전투 시뮬레이터 (TODO 항목 F) — 설계
|
||||
|
||||
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO.md 항목 F + gen-slaydeck.mjs 전투 규칙 + D 데이터.
|
||||
> 선행: D(데이터 외부화) 완료.
|
||||
|
||||
## 문제
|
||||
|
||||
박재오 강점(백엔드·AI 자동화) 활용처로 기획됐으나 코드 없음. 카드/적 밸런싱을 손으로
|
||||
검증해야 한다. 데이터 기반 자동 밸런스 검증 도구가 필요하다.
|
||||
|
||||
## 목표
|
||||
|
||||
`data/cards.json`·`data/enemies.json`를 입력으로, 전투를 몬테카를로로 N회 자동 시뮬레이션해
|
||||
승률·평균 턴·OP 카드 탐지 리포트를 출력하는 오프라인 Node CLI(`tools/sim-balance.mjs`).
|
||||
|
||||
## 설계
|
||||
|
||||
### 구조
|
||||
`tools/sim-balance.mjs` 단일 파일, 섹션 분리:
|
||||
1. **데이터 로드**: `data/cards.json`·`data/enemies.json`(D와 동일 소스). `activeEnemy` 사용.
|
||||
2. **시드 PRNG**: mulberry32(시드 고정 → 재현 가능, 데이터 바꾸면 결과 변동).
|
||||
3. **전투 엔진**(Lua 규칙 미러): 아래 규칙을 JS로 재현.
|
||||
4. **플레이어 정책**(휴리스틱 A).
|
||||
5. **집계·리포트**.
|
||||
6. **CLI 파싱·출력**.
|
||||
|
||||
### 전투 규칙 (gen-slaydeck.mjs Lua와 동일)
|
||||
- 시작: 플레이어 `hp=PLAYER_HP(상수 80)`, `block=0`; 적 `hp=maxHp`, `block=0`, `intentIdx=0`(0-base).
|
||||
덱 = `starterDeck` 셔플(PRNG).
|
||||
- 플레이어 턴 시작: `energy=3`, `block=0`, 5장 드로우(덱 소진 시 버림 더미 셔플해 재활용).
|
||||
- 플레이어 행동: 정책이 카드 선택 → 사용 시 `energy -= cost`, `Attack`→적에 `damage`(적 block 우선 차감),
|
||||
`Skill`→플레이어 `block += block`. 사용 카드는 버림. 더 둘 수 없으면 턴 종료.
|
||||
- 적 턴: 적 `block=0` → 현재 의도 실행(`Attack`→플레이어에 피해(플레이어 block 우선 차감),
|
||||
`Defend`→적 `block += value`) → `intentIdx=(intentIdx+1)%len`.
|
||||
- 승패: 적 hp≤0 승리, 플레이어 hp≤0 패배. 턴 상한 `MAX_TURNS=100`(초과 시 무승부로 집계, 경고).
|
||||
|
||||
### 플레이어 정책 (휴리스틱 A)
|
||||
매 플레이어 행동 루프:
|
||||
1. **치사 판단**: 손패의 Attack 카드들로 이번 턴 낼 수 있는 최대 데미지(에너지 한도 내) ≥
|
||||
`적 hp + 적 block` 이면 → 그 Attack들을 사용(킬).
|
||||
2. 아니면 **적 의도가 Attack**이면 → 손패 Defend(Skill+block) 카드를 사용(에너지 닿는 한),
|
||||
이후 잔여 에너지로 Attack 사용.
|
||||
3. 아니면(적 Defend 의도) → Attack 우선 사용.
|
||||
4. 사용 가능한 카드(에너지≥cost)가 없으면 턴 종료.
|
||||
- 동률 선택은 에너지 효율(뎀/E 또는 블록/E) 높은 카드 우선.
|
||||
|
||||
### 리포트 지표
|
||||
- 전체: 승률(%), 평균·중앙값 턴 수, 승리 시 평균 잔여 HP, 패배율, (무승부 시 경고).
|
||||
- 카드별: 사용 횟수, 누적 데미지/방어, **에너지당 효율**(Attack=총뎀/총E, Skill=총블록/총E).
|
||||
- **OP 탐지**: 같은 kind 내 효율이 그 kind 중앙값의 ≥1.5배인 카드를 ⚠️로 플래그. 최다/최소 사용 카드 표기.
|
||||
|
||||
### CLI
|
||||
`node tools/sim-balance.mjs [N] [--seed S]` — 기본 `N=2000`, `seed=1`. 표 형식 출력.
|
||||
|
||||
### 동기화 위험
|
||||
JS 전투 규칙은 Lua(`gen-slaydeck.mjs`)와 **중복**이다(공유 불가). 데이터(JSON)는 공유.
|
||||
파일 상단에 "전투 규칙 변경 시 gen-slaydeck.mjs Lua와 동기화" 주석 명시.
|
||||
|
||||
## 검증 (TDD + CLI)
|
||||
- 전투 엔진/정책 핵심을 순수 함수로 분리해 단위 테스트(Node 내장 `node:test`):
|
||||
데미지 방어차감, 치사 판단, 적 의도 사이클, 승/패 종료.
|
||||
- `node tools/sim-balance.mjs` → 승률·턴·카드 통계 출력.
|
||||
- `data/cards.json`에서 강타 damage↑ → 승률·강타 효율 상승(데이터 반영).
|
||||
- 동일 시드 2회 → 동일 출력(결정성).
|
||||
|
||||
## 범위 밖 (금지)
|
||||
- 상태이상·드로우·복합효과, 다중 적, 로그라이크 메타. 메이커 런타임 연동.
|
||||
- 새 카드/적 추가(현 데이터로 검증). 정책 고도화(MCTS 등).
|
||||
@@ -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(메타).
|
||||
64
docs/superpowers/specs/2026-06-09-floors-design.md
Normal file
64
docs/superpowers/specs/2026-06-09-floors-design.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 다음 층 / 멀티 act (TODO E6a) — 설계
|
||||
|
||||
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO E 분해(E6a) + 기존 맵/전투 구조.
|
||||
> 선행: E1~E5 완료. 제외: E6b 저장/불러오기(사용자가 안 함으로 결정 — MSW 저장 API 필요).
|
||||
|
||||
## 문제
|
||||
|
||||
현재 보스 클리어 = 즉시 런 종료. 로그라이크의 "여러 층(act)을 점점 깊이 진행" 느낌이 없다.
|
||||
보스 클리어 후 다음 막으로 이어지고, 최종 막 보스에서 진짜 런 클리어가 필요하다.
|
||||
|
||||
## 설계
|
||||
|
||||
### 파라미터 (생성기 상수)
|
||||
- `ACT_COUNT = 3` (막 수).
|
||||
- 적 스케일: `mult = 1 + (Act-1)*0.6` → 1막 ×1, 2막 ×1.6, 3막 ×2.2.
|
||||
|
||||
### 상태 재정의
|
||||
- 기존 `Floor`를 **현재 막 카운터**(1..ACT_COUNT)로 사용. `RunLength = ACT_COUNT`.
|
||||
- 맵 내 행 진행은 맵 UI가 표현 → 별도 숫자 표시 없음.
|
||||
|
||||
### 메서드 변경
|
||||
- `StartRun`: `Floor = 1`, `RunLength = ${ACT_COUNT}`. (맵 1회 빌드는 그대로.)
|
||||
- `StartCombat`: `self.Floor = node.row` 줄 **제거**. 적 로드 시 막 스케일 적용:
|
||||
```lua
|
||||
local mult = 1 + (self.Floor - 1) * 0.6
|
||||
self.EnemyMaxHp = math.floor(enemy.maxHp * mult)
|
||||
self.EnemyHp = self.EnemyMaxHp
|
||||
self.EnemyIntents = {}
|
||||
for i = 1, #enemy.intents do
|
||||
self.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) }
|
||||
end
|
||||
```
|
||||
(공유 enemy.intents 변형 금지 — 새 테이블 생성.)
|
||||
- `CheckCombatEnd` 보스 승리 분기:
|
||||
```lua
|
||||
if node ~= nil and node.type == "boss" then
|
||||
if self.Floor < self.RunLength then
|
||||
self.Floor = self.Floor + 1
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self:RenderRun()
|
||||
self:ShowMap()
|
||||
else
|
||||
self:ShowResult("런 클리어!")
|
||||
self.RunActive = false
|
||||
end
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
```
|
||||
(다음 막은 같은 맵 구조 재사용 — CurrentNodeId 리셋만. 적은 막 스케일로 강해짐.)
|
||||
- HP·골드·덱·유물은 막 간 유지(기존 영속). combatStart 유물은 전투마다 재적용.
|
||||
|
||||
### UI
|
||||
- `RenderRun`: 층 텍스트를 `"막 " .. Floor .. "/" .. RunLength`로 (라벨 "층"→"막"). 골드 표시 유지.
|
||||
|
||||
## 검증 (메이커 Play)
|
||||
- 1막 보스(슬라임 킹 120) 처치 → 2막 맵·Floor 2 → 적 HP 스케일(슬라임 45→72, 보스 120→192).
|
||||
- 3막 보스 처치 → "런 클리어!". HP/골드/덱/유물 막 간 유지.
|
||||
- 패배 시 종료. 생성기 결정적·JSON 유효.
|
||||
- (버튼 런타임 — MCP는 PickNode/PlayCard/CheckCombatEnd 직접 호출 + 상태 로그.)
|
||||
|
||||
## 범위 밖 (금지)
|
||||
- 저장/불러오기(E6b). 막별 다른 맵 디자인·신규 적/배경·막별 보상 차등.
|
||||
82
docs/superpowers/specs/2026-06-09-map-nodes-design.md
Normal file
82
docs/superpowers/specs/2026-06-09-map-nodes-design.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 분기 맵 노드 진행 (TODO E3) — 설계
|
||||
|
||||
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO E 분해(E3) + SlayDeckController/run-loop-core 분석.
|
||||
> 선행: E1+E2(런 루프 코어) 완료. 후속: E4(상점/휴식)·E5(유물)·E6(보스 연출/저장).
|
||||
|
||||
## 문제
|
||||
|
||||
E1+E2는 보상 후 자동으로 다음 전투로 넘어간다(고정 N). 로그라이크는 **플레이어가 맵에서 경로를
|
||||
선택**해야 한다. 분기 노드 맵과 노드별 적 차등이 필요하다.
|
||||
|
||||
## 범위
|
||||
|
||||
플레이어가 **분기 맵(작성된 DAG)** 에서 다음 노드를 선택 → 노드 타입(전투/엘리트/보스)대로 전투
|
||||
(적은 데이터로 차등) → 보상 → 맵으로 복귀 → 보스 클리어 시 "런 클리어". **상점/휴식·유물·저장·
|
||||
절차적 생성·연결선 그리기는 범위 밖**. 맵 스키마는 상점/휴식 타입을 미래 수용.
|
||||
|
||||
## 설계
|
||||
|
||||
### 데이터
|
||||
**`data/map.json`** (분기 DAG):
|
||||
```json
|
||||
{
|
||||
"start": ["A", "B"],
|
||||
"nodes": {
|
||||
"A": { "type": "combat", "enemy": "slime", "row": 1, "col": -1, "next": ["C", "D"] },
|
||||
"B": { "type": "combat", "enemy": "slime", "row": 1, "col": 1, "next": ["D", "E"] },
|
||||
"C": { "type": "elite", "enemy": "slime_elite", "row": 2, "col": -2, "next": ["BOSS"] },
|
||||
"D": { "type": "combat", "enemy": "slime", "row": 2, "col": 0, "next": ["BOSS"] },
|
||||
"E": { "type": "combat", "enemy": "slime", "row": 2, "col": 2, "next": ["BOSS"] },
|
||||
"BOSS": { "type": "boss", "enemy": "slime_boss", "row": 3, "col": 0, "next": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
- `type` ∈ {combat, elite, boss} (이 슬라이스). `enemy`는 enemies.json id. `row`(1=시작), `col`(레이아웃 x 단위), `next`(도달 노드 ids).
|
||||
|
||||
**`data/enemies.json`** 확장:
|
||||
```json
|
||||
"slime_elite": { "name": "정예 슬라임", "maxHp": 70,
|
||||
"intents": [ {Attack 14}, {Attack 8}, {Defend 10} ] },
|
||||
"slime_boss": { "name": "슬라임 킹", "maxHp": 120,
|
||||
"intents": [ {Attack 18}, {Defend 12}, {Attack 10}, {Attack 22} ] }
|
||||
```
|
||||
(`activeEnemy`는 유지하되 런은 맵 노드의 enemy로 전투. F 시뮬레이터는 여전히 activeEnemy 기준 — 맵 적 시뮬은 후속.)
|
||||
|
||||
### 상태 (SlayDeckController 속성 추가)
|
||||
- `Enemies`(any) — 전체 적 테이블(id→정의). 생성기가 enemies.json 전체 주입.
|
||||
- `MapNodes`(any) — 그래프(id→{type, enemy, row, col, next}).
|
||||
- `MapStart`(any) — 1행 노드 id 배열.
|
||||
- `CurrentNodeId`(string) — 현재 위치("" = 시작 전).
|
||||
- `CurrentEnemyId`(string) — 현재 전투 적 id.
|
||||
|
||||
### 메서드
|
||||
- `StartRun`(수정): 런 상태 초기화 + `Enemies`/`MapNodes`/`MapStart` 세팅 + `CurrentNodeId=""` +
|
||||
BindButtons(맵 노드 버튼 포함, 1회) → `self:ShowMap()` (기존 StartCombat 대신).
|
||||
- `ShowMap`(신규): 선택 가능 노드 결정(CurrentNodeId=="" 면 MapStart, 아니면 MapNodes[CurrentNodeId].next).
|
||||
각 노드 버튼 활성/비활성·라벨 갱신, 전투 UI 가리고 MapHud 표시(Enable).
|
||||
- `IsReachable(id)`(헬퍼) — 현재 선택 가능 목록에 id 포함 여부.
|
||||
- `PickNode(id)`(신규): `IsReachable(id)` 아니면 무시. `CurrentNodeId=id`,
|
||||
`CurrentEnemyId=MapNodes[id].enemy`, MapHud 숨김 → `StartCombat()`.
|
||||
- `StartCombat`(수정): 적을 `self.Enemies[self.CurrentEnemyId]`에서 로드(이름/HP/의도). Floor 증가 로직 제거.
|
||||
- `CheckCombatEnd`(수정): 승리 시 골드+15 → 현재 노드 `type=="boss"`면 `ShowResult("런 클리어!")`+RunActive=false;
|
||||
아니면 `OfferReward`. 패배 → "패배..."+RunActive=false.
|
||||
- `PickReward`(수정): 카드 처리 후 `StartCombat` 대신 `self:ShowMap()`.
|
||||
|
||||
### UI (MapHud, 신규)
|
||||
- 평소 숨김. 풀스크린 모달 배경 + 제목 "다음 노드 선택".
|
||||
- 노드 버튼 6개: 위치 = (col×스페이싱, 화면중앙+row×행간), 라벨(전투/엘리트/보스 + 적 이름).
|
||||
- 선택 가능 노드만 밝게·클릭, 나머지 어둡게(반투명). 클릭 → `PickNode(id)`.
|
||||
- 연결선은 생략(도달성=활성/비활성으로 표현; 연결선 그리기는 후속 폴리시).
|
||||
|
||||
### 단일 소스
|
||||
모든 변경은 `tools/gen-slaydeck.mjs`에서 생성. map.json/enemies.json은 데이터 단일 소스.
|
||||
|
||||
## 검증 (메이커 Play)
|
||||
- StartRun → MapHud, 1행 A·B만 선택 가능(나머지 비활성).
|
||||
- A 선택 → 슬라임 전투 → 승리 → 보상 → 맵 복귀, 이제 C·D 선택 가능(B쪽 E는 불가).
|
||||
- 엘리트 노드 → 정예 슬라임(HP 70) 전투. 보스 노드 → 슬라임 킹(HP 120).
|
||||
- 보스 승리 → "런 클리어!". 패배 → "패배...". 도달 불가 노드 클릭 → 무시.
|
||||
- 생성기 결정적, JSON 유효. (버튼 클릭은 런타임 — MCP는 PickNode/PlayCard/PickReward 직접 호출로 검증.)
|
||||
|
||||
## 범위 밖 (금지)
|
||||
- 상점/휴식 노드 동작(E4)·유물(E5)·저장(E6). 절차적 맵·무작위 분기·연결선 그리기. 새 카드.
|
||||
69
docs/superpowers/specs/2026-06-09-relics-design.md
Normal file
69
docs/superpowers/specs/2026-06-09-relics-design.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 유물 (TODO E5) — 설계
|
||||
|
||||
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO E 분해(E5) + 기존 전투/맵/상점 구조.
|
||||
> 선행: E1~E4 완료. 후속: E6(저장/다음 층). 사용자 요청: 획득 3경로(시작·엘리트·상점) 모두.
|
||||
|
||||
## 문제
|
||||
|
||||
런 영속·맵·보상·상점은 됐으나 유물(패시브 빌드 요소)이 없다. 훅 기반 패시브 + 다양한 획득 경로가 필요하다.
|
||||
|
||||
## 설계
|
||||
|
||||
### 데이터 `data/relics.json`
|
||||
```json
|
||||
{
|
||||
"relics": {
|
||||
"ironHeart": { "name": "강철 심장", "desc": "전투 시작 시 방어도 +6", "hook": "combatStart", "effect": "block", "value": 6 },
|
||||
"energyCore": { "name": "에너지 코어", "desc": "턴 시작 시 에너지 +1", "hook": "turnStart", "effect": "energy", "value": 1 },
|
||||
"vampire": { "name": "흡혈 송곳니", "desc": "공격 카드 사용 시 HP +1", "hook": "cardPlayed", "effect": "healOnAttack", "value": 1 },
|
||||
"goldIdol": { "name": "황금 우상", "desc": "전투 승리 시 골드 +10", "hook": "combatReward", "effect": "gold", "value": 10 }
|
||||
},
|
||||
"startingRelic": "ironHeart",
|
||||
"relicPool": ["energyCore", "vampire", "goldIdol"]
|
||||
}
|
||||
```
|
||||
- `relicPool` = 엘리트/상점에서 무작위로 줄 후보(시작 유물 제외). 중복 허용(스택).
|
||||
|
||||
### 파라미터 (생성기 상수)
|
||||
- `RELIC_PRICE = 60`.
|
||||
|
||||
### 상태 추가
|
||||
- `Relics`(any) — 전체 유물 정의(주입).
|
||||
- `RunRelics`(any) — 보유 유물 id 목록.
|
||||
- `ShopRelic`(string) — 상점 제시 유물 id.
|
||||
- `ShopRelicBought`(boolean).
|
||||
|
||||
### 훅 시스템
|
||||
- `ApplyRelics(hook)`: RunRelics 순회, `hook` 일치 유물의 effect 적용:
|
||||
- `block`→PlayerBlock+=value, `energy`→Energy+=value, `healOnAttack`→PlayerHp+=value(상한 클램프), `gold`→Gold+=value.
|
||||
- 연결 지점:
|
||||
- `combatStart` → StartCombat 끝(StartPlayerTurn 호출 뒤 — 방어도 리셋 이후 적용 → RenderCombat).
|
||||
- `turnStart` → StartPlayerTurn(에너지 회복 직후).
|
||||
- `cardPlayed` → PlayCard의 Attack 분기(데미지 적용 후).
|
||||
- `combatReward` → CheckCombatEnd 승리(기본 골드 += 후).
|
||||
|
||||
### 획득 (공통 `AddRelic(id)` → RunRelics 추가·RenderRelics)
|
||||
- **C 시작**: `StartRun`에서 `RunRelics={}` → `AddRelic(startingRelic)`.
|
||||
- **A 엘리트**: `CheckCombatEnd` 승리 시 노드 `type=="elite"`면 `relicPool`에서 무작위 `AddRelic`(보스는 런 종료라 제외).
|
||||
- **B 상점**: `ShowShop`에서 `ShopRelic = relicPool 무작위`, ShopRelicBought=false; `BuyRelic`(ShopRelicBought거나 Gold<RELIC_PRICE면 무시; 아니면 Gold-=60·AddRelic·비활성).
|
||||
|
||||
### UI
|
||||
- 상단 유물 바: `/ui/DefaultGroup/CombatHud/Relics` 텍스트, `RenderRelics`가 보유 유물 이름을 ", "로 join해 "유물: …" 표시(없으면 "유물: 없음").
|
||||
- ShopHud에 유물 슬롯: `/ui/DefaultGroup/ShopHud/Relic`(sprite+button) + Name/Desc/Price 자식. `RenderShop`이 ShopRelic 비주얼·가격·구매상태 갱신.
|
||||
- 엘리트 유물 획득은 유물 바 갱신으로 표시.
|
||||
|
||||
### 단일 소스
|
||||
모든 변경은 `tools/gen-slaydeck.mjs`에서 생성. relics.json은 데이터 단일 소스.
|
||||
|
||||
## 검증 (메이커 Play)
|
||||
- 시작 유물(강철심장) → 전투 시작 시 PlayerBlock 6.
|
||||
- energyCore 보유 → 턴 시작 에너지 4(3+1).
|
||||
- vampire 보유 → 공격 카드 사용 시 HP +1(상한).
|
||||
- goldIdol 보유 → 승리 시 골드 +25(15+10).
|
||||
- 엘리트 승리 → relicPool 유물 1개 RunRelics 추가(바 갱신).
|
||||
- 상점 유물 구매 → 골드 -60·RunRelics 추가·슬롯 비활성. 골드 부족/재구매 무시.
|
||||
- 생성기 결정적·JSON 유효.
|
||||
- (버튼은 런타임 — MCP는 AddRelic/BuyRelic/PlayCard 등 직접 호출 + 상태 로그로 검증.)
|
||||
|
||||
## 범위 밖 (금지)
|
||||
- 부정적 유물·복합/조건부 효과·유물 제거·보스 유물·유물 등급/툴팁. 카드 제거(별도).
|
||||
68
docs/superpowers/specs/2026-06-09-run-loop-core-design.md
Normal file
68
docs/superpowers/specs/2026-06-09-run-loop-core-design.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 런 루프 코어 (TODO E1+E2) — 설계
|
||||
|
||||
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO.md 항목 E(분해) + SlayDeckController 분석.
|
||||
> E(로그라이크 메타)의 첫 하위 프로젝트. 선행: B·D 완료. 후속: E3(맵)·E4(상점)·E5(유물)·E6(보스/저장).
|
||||
|
||||
## 문제
|
||||
|
||||
단일 전투(B)는 닫혔으나 승리 후 보상·다음 전투·덱 성장이 없다(보상 훅 자리만 비어 있음).
|
||||
전투를 한 "런"으로 확장해야 덱빌딩 로그라이크가 된다.
|
||||
|
||||
## 범위 (이 슬라이스)
|
||||
|
||||
전투를 **연속 N전투 런**으로 확장: 런 상태 영속(HP/골드/덱) + 승리 후 카드 1택 보상 +
|
||||
다음 전투 연결 + 고정 N전투 후 "런 클리어". **맵 노드·상점·유물·보스·저장은 범위 밖**(후속 E3~E6).
|
||||
아키텍처: 기존 `SlayDeckController` 확장(별도 RunState 분리는 후속).
|
||||
|
||||
## 설계
|
||||
|
||||
### 런 파라미터 (생성기 상수 — 향후 외부화)
|
||||
- `RUN_LENGTH = 3` (런당 전투 수), `GOLD_PER_WIN = 15`.
|
||||
|
||||
### 새 상태 (SlayDeckController 속성)
|
||||
- `RunDeck`(any) — 보유 카드 id 누적 배열(영속).
|
||||
- `Gold`(number) — 누적 골드.
|
||||
- `Floor`(number) — 현재 전투 번호(1-base).
|
||||
- `RunLength`(number) — 런당 전투 수.
|
||||
- `RewardChoices`(any) — 현재 제시 중인 보상 카드 id 3개.
|
||||
- `RunActive`(boolean) — 런 진행 중.
|
||||
- 플레이어 HP는 전투 간 **유지**(StartCombat에서 리셋 안 함).
|
||||
|
||||
### 메서드
|
||||
- `OnBeginPlay` → `self:StartRun()`.
|
||||
- **`StartRun`**(신규): `PlayerMaxHp=80`, `PlayerHp=PlayerMaxHp`, `Gold=0`, `Floor=0`,
|
||||
`RunLength=RUN_LENGTH`, `RunDeck = starterDeck 복사`, `RunActive=true` → `BindButtons()`(1회) → `StartCombat()`.
|
||||
- **`StartCombat`**(수정): `Floor += 1`; 적 데이터(activeEnemy) 세팅; 전투별 리셋(Energy/Turn/Block/
|
||||
EnemyHp/EnemyBlock/EnemyIntentIndex/DiscardPile/Hand/CombatOver); `DrawPile = RunDeck 복사` → Shuffle;
|
||||
`Cards` 테이블 세팅. **HP·Gold·RunDeck 보존, BindButtons 호출 제거.** → RenderCombat → StartPlayerTurn.
|
||||
- **`BindButtons`**(수정): EndTurn·카드5·**보상카드3·건너뛰기** 버튼을 1회 바인딩(StartRun에서 호출).
|
||||
- **`CheckCombatEnd`**(수정):
|
||||
- 적 HP≤0(승리): `Gold += GOLD_PER_WIN`; `CombatOver=true`;
|
||||
`Floor >= RunLength`이면 `ShowResult("런 클리어!")` + `RunActive=false`;
|
||||
아니면 `self:OfferReward()`.
|
||||
- 플레이어 HP≤0(패배): `CombatOver=true`; `ShowResult("패배...")`; `RunActive=false`.
|
||||
- **`OfferReward`**(신규): `RewardChoices = 카드풀에서 3개 무작위`(math.random); 각 보상 카드 UI 갱신
|
||||
(이름/코스트/설명/색); RewardHud 표시(Enable).
|
||||
- **`PickReward(slot)`**(신규): `slot`(1~3)이면 `RewardChoices[slot]`을 `RunDeck`에 추가; `slot=0`(건너뛰기)이면 추가 안 함;
|
||||
RewardHud 숨김 → `StartCombat()`(다음 층).
|
||||
- **`RenderRun`**(신규): `층 Floor/RunLength`·`골드 Gold` 텍스트 갱신. RenderCombat에서 호출.
|
||||
|
||||
### UI (생성기 신규)
|
||||
- `RewardHud`(평소 숨김): 제목 "보상 카드 선택" + 보상 카드 3장(UISprite+버튼, 이름/코스트/설명 자식) + "건너뛰기" 버튼.
|
||||
- HUD 표시 추가: `/ui/DefaultGroup/CombatHud/Floor`("층 1/3"), `/Gold`("골드 0").
|
||||
- 보상 카드 클릭 → `PickReward(slot)`, 건너뛰기 → `PickReward(0)`.
|
||||
|
||||
### 버그 예방
|
||||
- `BindButtons`가 매 전투(StartCombat)마다 카드 버튼에 `ConnectEvent` → 런에서 핸들러 중첩.
|
||||
**StartRun에서 1회만 바인딩**으로 이동(StartCombat의 BindButtons 호출 제거).
|
||||
|
||||
## 검증 (메이커 Play)
|
||||
- 전투 승리 → RewardHud에 카드 3장 표시; 골드 +15·층 표시.
|
||||
- 보상 1택 → RunDeck +1(다음 전투 손패/덱에 등장 가능), RewardHud 숨김, 다음 전투 시작(HP 유지).
|
||||
- 건너뛰기 → 덱 변화 없이 다음 전투.
|
||||
- 3전투째 승리 → "런 클리어!"·런 종료. 도중 패배 → "패배..."·런 종료.
|
||||
- 카드/보상 버튼 클릭은 런타임(MCP는 `PlayCard`/`EndPlayerTurn`/`PickReward` 직접 호출로 검증).
|
||||
- 생성기 결정적, JSON 유효.
|
||||
|
||||
## 범위 밖 (금지)
|
||||
- 맵 노드(E3)·상점/휴식(E4)·유물(E5)·보스/층전환/저장(E6). 골드 소비(E4). 보상 풀 확장(메이플 IP 추후).
|
||||
70
docs/superpowers/specs/2026-06-09-shop-rest-design.md
Normal file
70
docs/superpowers/specs/2026-06-09-shop-rest-design.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 상점/휴식 노드 (TODO E4) — 설계
|
||||
|
||||
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO E 분해(E4) + E3 맵 노드 구조.
|
||||
> 선행: E3(분기 맵) 완료. 후속: E5(유물)·E6(저장). 카드 제거는 덱 보기 UI 필요 → 후속 분리.
|
||||
|
||||
## 문제
|
||||
|
||||
E3로 분기 맵은 됐으나 모든 노드가 전투다. 골드는 적립만 되고 소비처가 없다. 상점(골드→카드)·
|
||||
휴식(HP 회복) 노드가 필요하다.
|
||||
|
||||
## 범위
|
||||
|
||||
맵에 상점/휴식 노드 추가, 진입 시 전투 대신 상점/휴식 UI. 상점 = 카드 구매(골드). 휴식 = HP 회복.
|
||||
**카드 제거(덱 보기 UI 필요)·유물·저장·휴식 업그레이드는 범위 밖.**
|
||||
|
||||
## 설계
|
||||
|
||||
### 데이터 (`data/map.json` 교체 — 4행)
|
||||
```json
|
||||
{
|
||||
"start": ["A", "B"],
|
||||
"nodes": {
|
||||
"A": { "type": "combat", "enemy": "slime", "row": 1, "col": -1, "next": ["C", "D"] },
|
||||
"B": { "type": "combat", "enemy": "slime", "row": 1, "col": 1, "next": ["C", "D"] },
|
||||
"C": { "type": "rest", "row": 2, "col": -1, "next": ["E", "F"] },
|
||||
"D": { "type": "shop", "row": 2, "col": 1, "next": ["E", "F"] },
|
||||
"E": { "type": "elite", "enemy": "slime_elite", "row": 3, "col": -1, "next": ["BOSS"] },
|
||||
"F": { "type": "combat", "enemy": "slime", "row": 3, "col": 1, "next": ["BOSS"] },
|
||||
"BOSS": { "type": "boss", "enemy": "slime_boss", "row": 4, "col": 0, "next": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
- rest/shop 노드는 `enemy` 없음. 생성기 검증을 "`enemy` 있을 때만 ENEMIES 확인"으로 완화.
|
||||
Lua MapNodes 직렬화도 enemy 있을 때만 `enemy = "..."` 포함.
|
||||
|
||||
### 파라미터 (생성기 상수)
|
||||
- `CARD_PRICE = 30`, `REST_HEAL = 30`.
|
||||
|
||||
### 상태 추가
|
||||
- `ShopChoices`(any) — 상점 제시 카드 id 3개.
|
||||
- `ShopBought`(any) — 슬롯별 구매 여부 {bool×3}.
|
||||
|
||||
### 메서드
|
||||
- `PickNode`(수정): CurrentNodeId 세팅·맵 숨김 후 타입 분기 —
|
||||
`shop`→`ShowShop`, `rest`→`ShowRest`, 그 외→`CurrentEnemyId=node.enemy`·`StartCombat`.
|
||||
- `ShowShop`(신규): 카드 풀에서 3개 무작위→ShopChoices, ShopBought 초기화(false),
|
||||
각 슬롯 비주얼·가격·골드 갱신, ShopHud 표시.
|
||||
- `BuyCard(slot)`(신규): ShopBought[slot]==true 또는 Gold<CARD_PRICE면 무시. 아니면
|
||||
Gold-=CARD_PRICE, RunDeck에 ShopChoices[slot] 추가, ShopBought[slot]=true, 해당 카드 어둡게, 골드 갱신.
|
||||
- `ShowRest`(신규): `PlayerHp = min(PlayerMaxHp, PlayerHp + REST_HEAL)`, RestHud에 "HP 옛→새 (+회복)" 표시, RestHud 표시.
|
||||
- `LeaveNode`(신규): ShopHud·RestHud 숨김 → `ShowMap`. (상점·휴식 나가기 공용)
|
||||
- `RenderShop`(신규): 3 카드 비주얼/가격/구매상태 + 골드 텍스트 갱신.
|
||||
|
||||
### UI (신규)
|
||||
- `ShopHud`(모달, 숨김): 제목 "상점", 골드 텍스트, 카드 3장(sprite+button + Name/Cost/Desc/Price 자식), "나가기" 버튼.
|
||||
- `RestHud`(모달, 숨김): 제목 "휴식", 정보 텍스트(런타임), "나가기" 버튼.
|
||||
- BindButtons: 상점 카드 버튼 3(→BuyCard i)·상점 나가기(→LeaveNode)·휴식 나가기(→LeaveNode) 바인딩.
|
||||
|
||||
### MapHud (4행 대응)
|
||||
- 노드 y = `(row - (MAX_ROW+1)/2) * 140` (행 수에 맞춰 세로 중앙 정렬). col×180 유지.
|
||||
|
||||
## 검증 (메이커 Play)
|
||||
- 맵→상점(D) 진입 → 카드 3장·가격·골드 표시 → 구매 시 골드 -30·RunDeck +1·해당 카드 비활성 →
|
||||
골드 부족 시 구매 무시 → 나가기 → 맵(다음 노드).
|
||||
- 맵→휴식(C) 진입 → HP +30(상한 클램프) → 나가기 → 맵.
|
||||
- 전투/엘리트/보스/런 클리어/패배 회귀 없음. 생성기 결정적·JSON 유효.
|
||||
- (버튼 클릭은 런타임 — MCP는 PickNode/BuyCard/ShowRest/LeaveNode 직접 호출로 검증.)
|
||||
|
||||
## 범위 밖 (금지)
|
||||
- 카드 제거·덱 보기 UI(후속)·유물(E5)·저장(E6)·휴식 업그레이드·상점 유물/물약.
|
||||
250
map/map02.map
250
map/map02.map
@@ -47,7 +47,7 @@
|
||||
"FootholdsByLayer": {
|
||||
"1": [
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.29000092,
|
||||
"NextFootholdId": 2,
|
||||
"PreviousFootholdId": 27,
|
||||
"groupID": 1,
|
||||
@@ -62,15 +62,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 1,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.940001,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -7.65000057,
|
||||
"y": -0.04000002
|
||||
"x": -7.64999962,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -93,15 +93,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 2,
|
||||
"StartPoint": {
|
||||
"x": -7.64999962,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -124,15 +124,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 3,
|
||||
"StartPoint": {
|
||||
"x": -6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -155,15 +155,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 4,
|
||||
"StartPoint": {
|
||||
"x": -5.84999943,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -186,15 +186,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 5,
|
||||
"StartPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -217,15 +217,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 6,
|
||||
"StartPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -248,15 +248,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 7,
|
||||
"StartPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -264,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.899999738,
|
||||
"Length": 0.9,
|
||||
"NextFootholdId": 9,
|
||||
"PreviousFootholdId": 7,
|
||||
"groupID": 1,
|
||||
@@ -279,15 +279,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 8,
|
||||
"StartPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"x": -1.34999979,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -310,15 +310,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 9,
|
||||
"StartPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -341,15 +341,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 10,
|
||||
"StartPoint": {
|
||||
"x": -0.45,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -372,15 +372,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 11,
|
||||
"StartPoint": {
|
||||
"x": 0.450000018,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -403,15 +403,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 12,
|
||||
"StartPoint": {
|
||||
"x": 1.34999979,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -434,15 +434,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 13,
|
||||
"StartPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"x": 2.24999976,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 3.15,
|
||||
"y": -0.04000002
|
||||
"x": 3.14999986,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -465,15 +465,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 14,
|
||||
"StartPoint": {
|
||||
"x": 3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.04999971,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -496,15 +496,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 15,
|
||||
"StartPoint": {
|
||||
"x": 4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -527,15 +527,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 16,
|
||||
"StartPoint": {
|
||||
"x": 4.95000029,
|
||||
"y": -0.04000002
|
||||
"x": 4.95,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"x": 5.84999943,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -558,15 +558,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 17,
|
||||
"StartPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -574,7 +574,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.289999,
|
||||
"NextFootholdId": 19,
|
||||
"PreviousFootholdId": 17,
|
||||
"groupID": 1,
|
||||
@@ -589,15 +589,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 18,
|
||||
"StartPoint": {
|
||||
"x": 6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 8.039999,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -605,7 +605,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.849999964,
|
||||
"NextFootholdId": 20,
|
||||
"PreviousFootholdId": 18,
|
||||
"groupID": 1,
|
||||
@@ -620,14 +620,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 19,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 8.039999,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -0.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -651,14 +651,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 20,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -682,14 +682,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 21,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -713,14 +713,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 22,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
@@ -744,15 +744,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 23,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -2.70000029
|
||||
"x": 8.039999,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -3.30000019
|
||||
"x": 8.039999,
|
||||
"y": -3.3
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -775,14 +775,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 24,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -3.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -806,14 +806,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 25,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"Variance": {
|
||||
@@ -837,14 +837,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 26,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -4.5
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 8.039999,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"Variance": {
|
||||
@@ -853,7 +853,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.849999964,
|
||||
"NextFootholdId": 1,
|
||||
"PreviousFootholdId": 28,
|
||||
"groupID": 1,
|
||||
@@ -868,15 +868,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 27,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -0.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.940001,
|
||||
"y": -0.0500000119
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -899,14 +899,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 28,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"Variance": {
|
||||
@@ -930,14 +930,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 29,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -961,14 +961,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 30,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -992,15 +992,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 31,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -3.30000019
|
||||
"x": -8.940001,
|
||||
"y": -3.3
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -2.70000029
|
||||
"x": -8.940001,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -1023,14 +1023,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 32,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -3.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1054,14 +1054,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 33,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1085,14 +1085,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "000007d3-0000-4000-8000-0000000007d3",
|
||||
"Id": 34,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.940001,
|
||||
"y": -4.5
|
||||
},
|
||||
"Variance": {
|
||||
@@ -6381,8 +6381,8 @@
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"y": 0.03499998,
|
||||
"x": 3.488205,
|
||||
"y": 0.0249999911,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
@@ -6514,7 +6514,7 @@
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"displayOrder": 5,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
@@ -6530,7 +6530,7 @@
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"y": 0.03499998,
|
||||
"y": 0.0249999911,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
|
||||
252
map/map05.map
252
map/map05.map
@@ -47,7 +47,7 @@
|
||||
"FootholdsByLayer": {
|
||||
"1": [
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.18000031,
|
||||
"NextFootholdId": 2,
|
||||
"PreviousFootholdId": 27,
|
||||
"groupID": 1,
|
||||
@@ -62,15 +62,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 1,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.83,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -7.65000057,
|
||||
"y": -0.04000002
|
||||
"x": -7.64999962,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -93,15 +93,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 2,
|
||||
"StartPoint": {
|
||||
"x": -7.64999962,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -124,15 +124,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 3,
|
||||
"StartPoint": {
|
||||
"x": -6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -155,15 +155,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 4,
|
||||
"StartPoint": {
|
||||
"x": -5.84999943,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -186,15 +186,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 5,
|
||||
"StartPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -217,15 +217,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 6,
|
||||
"StartPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -248,15 +248,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 7,
|
||||
"StartPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -264,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.899999738,
|
||||
"Length": 0.9,
|
||||
"NextFootholdId": 9,
|
||||
"PreviousFootholdId": 7,
|
||||
"groupID": 1,
|
||||
@@ -279,15 +279,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 8,
|
||||
"StartPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"x": -1.34999979,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -310,15 +310,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 9,
|
||||
"StartPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -341,15 +341,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 10,
|
||||
"StartPoint": {
|
||||
"x": -0.45,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -372,15 +372,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 11,
|
||||
"StartPoint": {
|
||||
"x": 0.450000018,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -403,15 +403,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 12,
|
||||
"StartPoint": {
|
||||
"x": 1.34999979,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -434,15 +434,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 13,
|
||||
"StartPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"x": 2.24999976,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 3.15,
|
||||
"y": -0.04000002
|
||||
"x": 3.14999986,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -465,15 +465,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 14,
|
||||
"StartPoint": {
|
||||
"x": 3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.04999971,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -496,15 +496,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 15,
|
||||
"StartPoint": {
|
||||
"x": 4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -527,15 +527,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 16,
|
||||
"StartPoint": {
|
||||
"x": 4.95000029,
|
||||
"y": -0.04000002
|
||||
"x": 4.95,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"x": 5.84999943,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -558,15 +558,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 17,
|
||||
"StartPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -574,7 +574,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.17999983,
|
||||
"NextFootholdId": 19,
|
||||
"PreviousFootholdId": 17,
|
||||
"groupID": 1,
|
||||
@@ -589,15 +589,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 18,
|
||||
"StartPoint": {
|
||||
"x": 6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.93,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -605,7 +605,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.819999933,
|
||||
"NextFootholdId": 20,
|
||||
"PreviousFootholdId": 18,
|
||||
"groupID": 1,
|
||||
@@ -620,14 +620,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 19,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.93,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -0.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -651,14 +651,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 20,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -682,14 +682,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 21,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -713,14 +713,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 22,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
@@ -744,15 +744,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 23,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -2.70000029
|
||||
"x": 7.93,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -3.30000019
|
||||
"x": 7.93,
|
||||
"y": -3.3
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -775,14 +775,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 24,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -3.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -806,14 +806,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 25,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"Variance": {
|
||||
@@ -837,14 +837,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 26,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -4.5
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.93,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"Variance": {
|
||||
@@ -853,7 +853,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.819999933,
|
||||
"NextFootholdId": 1,
|
||||
"PreviousFootholdId": 28,
|
||||
"groupID": 1,
|
||||
@@ -868,15 +868,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 27,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -0.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.83,
|
||||
"y": -0.08000001
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -899,14 +899,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 28,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"Variance": {
|
||||
@@ -930,14 +930,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 29,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -961,14 +961,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 30,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -992,15 +992,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 31,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -3.30000019
|
||||
"x": -8.83,
|
||||
"y": -3.3
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -2.70000029
|
||||
"x": -8.83,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -1023,14 +1023,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 32,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -3.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1054,14 +1054,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 33,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1085,14 +1085,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "0000138b-0000-4000-8000-00000000138b",
|
||||
"Id": 34,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.83,
|
||||
"y": -4.5
|
||||
},
|
||||
"Variance": {
|
||||
@@ -6381,8 +6381,8 @@
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"y": 0.03499998,
|
||||
"x": 3.52359,
|
||||
"y": -0.00500001,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
@@ -6514,7 +6514,7 @@
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"displayOrder": 5,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
@@ -6529,8 +6529,8 @@
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"y": 0.03499998,
|
||||
"x": 5.18743134,
|
||||
"y": -0.00500001,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
|
||||
250
map/map06.map
250
map/map06.map
@@ -47,7 +47,7 @@
|
||||
"FootholdsByLayer": {
|
||||
"1": [
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.13000011,
|
||||
"NextFootholdId": 2,
|
||||
"PreviousFootholdId": 27,
|
||||
"groupID": 1,
|
||||
@@ -62,15 +62,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 1,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.78,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -7.65000057,
|
||||
"y": -0.04000002
|
||||
"x": -7.64999962,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -93,15 +93,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 2,
|
||||
"StartPoint": {
|
||||
"x": -7.64999962,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -124,15 +124,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 3,
|
||||
"StartPoint": {
|
||||
"x": -6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -155,15 +155,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 4,
|
||||
"StartPoint": {
|
||||
"x": -5.84999943,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -186,15 +186,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 5,
|
||||
"StartPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -217,15 +217,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 6,
|
||||
"StartPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -248,15 +248,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 7,
|
||||
"StartPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -264,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.899999738,
|
||||
"Length": 0.9,
|
||||
"NextFootholdId": 9,
|
||||
"PreviousFootholdId": 7,
|
||||
"groupID": 1,
|
||||
@@ -279,15 +279,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 8,
|
||||
"StartPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"x": -1.34999979,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -310,15 +310,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 9,
|
||||
"StartPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -341,15 +341,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 10,
|
||||
"StartPoint": {
|
||||
"x": -0.45,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -372,15 +372,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 11,
|
||||
"StartPoint": {
|
||||
"x": 0.450000018,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -403,15 +403,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 12,
|
||||
"StartPoint": {
|
||||
"x": 1.34999979,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -434,15 +434,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 13,
|
||||
"StartPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"x": 2.24999976,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 3.15,
|
||||
"y": -0.04000002
|
||||
"x": 3.14999986,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -465,15 +465,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 14,
|
||||
"StartPoint": {
|
||||
"x": 3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.04999971,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -496,15 +496,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 15,
|
||||
"StartPoint": {
|
||||
"x": 4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -527,15 +527,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 16,
|
||||
"StartPoint": {
|
||||
"x": 4.95000029,
|
||||
"y": -0.04000002
|
||||
"x": 4.95,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"x": 5.84999943,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -558,15 +558,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 17,
|
||||
"StartPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -574,7 +574,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.12999964,
|
||||
"NextFootholdId": 19,
|
||||
"PreviousFootholdId": 17,
|
||||
"groupID": 1,
|
||||
@@ -589,15 +589,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 18,
|
||||
"StartPoint": {
|
||||
"x": 6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.87999964,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -605,7 +605,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.75,
|
||||
"NextFootholdId": 20,
|
||||
"PreviousFootholdId": 18,
|
||||
"groupID": 1,
|
||||
@@ -620,14 +620,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 19,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.87999964,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -0.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -651,14 +651,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 20,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -682,14 +682,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 21,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -713,14 +713,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 22,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
@@ -744,15 +744,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 23,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -2.70000029
|
||||
"x": 7.87999964,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -3.30000019
|
||||
"x": 7.87999964,
|
||||
"y": -3.3
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -775,14 +775,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 24,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -3.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -806,14 +806,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 25,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"Variance": {
|
||||
@@ -837,14 +837,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 26,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -4.5
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.87999964,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"Variance": {
|
||||
@@ -853,7 +853,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.75,
|
||||
"NextFootholdId": 1,
|
||||
"PreviousFootholdId": 28,
|
||||
"groupID": 1,
|
||||
@@ -868,15 +868,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 27,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -0.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.78,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -899,14 +899,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 28,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"Variance": {
|
||||
@@ -930,14 +930,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 29,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -961,14 +961,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 30,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -992,15 +992,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 31,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -3.30000019
|
||||
"x": -8.78,
|
||||
"y": -3.3
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -2.70000029
|
||||
"x": -8.78,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -1023,14 +1023,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 32,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -3.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1054,14 +1054,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 33,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1085,14 +1085,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001773-0000-4000-8000-000000001773",
|
||||
"Id": 34,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.78,
|
||||
"y": -4.5
|
||||
},
|
||||
"Variance": {
|
||||
@@ -6381,8 +6381,8 @@
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"y": 0.03499998,
|
||||
"x": 3.51769257,
|
||||
"y": -0.075,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
@@ -6514,7 +6514,7 @@
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"displayOrder": 5,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
@@ -6530,7 +6530,7 @@
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"y": 0.03499998,
|
||||
"y": -0.075,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
|
||||
252
map/map07.map
252
map/map07.map
@@ -47,7 +47,7 @@
|
||||
"FootholdsByLayer": {
|
||||
"1": [
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.250001,
|
||||
"NextFootholdId": 2,
|
||||
"PreviousFootholdId": 27,
|
||||
"groupID": 1,
|
||||
@@ -62,15 +62,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 1,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.900001,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -7.65000057,
|
||||
"y": -0.04000002
|
||||
"x": -7.64999962,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -93,15 +93,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 2,
|
||||
"StartPoint": {
|
||||
"x": -7.64999962,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -124,15 +124,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 3,
|
||||
"StartPoint": {
|
||||
"x": -6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -155,15 +155,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 4,
|
||||
"StartPoint": {
|
||||
"x": -5.84999943,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -186,15 +186,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 5,
|
||||
"StartPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -217,15 +217,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 6,
|
||||
"StartPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -248,15 +248,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 7,
|
||||
"StartPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -264,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.899999738,
|
||||
"Length": 0.9,
|
||||
"NextFootholdId": 9,
|
||||
"PreviousFootholdId": 7,
|
||||
"groupID": 1,
|
||||
@@ -279,15 +279,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 8,
|
||||
"StartPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"x": -1.34999979,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -310,15 +310,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 9,
|
||||
"StartPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -341,15 +341,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 10,
|
||||
"StartPoint": {
|
||||
"x": -0.45,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -372,15 +372,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 11,
|
||||
"StartPoint": {
|
||||
"x": 0.450000018,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -403,15 +403,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 12,
|
||||
"StartPoint": {
|
||||
"x": 1.34999979,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -434,15 +434,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 13,
|
||||
"StartPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"x": 2.24999976,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 3.15,
|
||||
"y": -0.04000002
|
||||
"x": 3.14999986,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -465,15 +465,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 14,
|
||||
"StartPoint": {
|
||||
"x": 3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.04999971,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -496,15 +496,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 15,
|
||||
"StartPoint": {
|
||||
"x": 4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -527,15 +527,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 16,
|
||||
"StartPoint": {
|
||||
"x": 4.95000029,
|
||||
"y": -0.04000002
|
||||
"x": 4.95,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"x": 5.84999943,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -558,15 +558,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 17,
|
||||
"StartPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -574,7 +574,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.24999952,
|
||||
"NextFootholdId": 19,
|
||||
"PreviousFootholdId": 17,
|
||||
"groupID": 1,
|
||||
@@ -589,15 +589,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 18,
|
||||
"StartPoint": {
|
||||
"x": 6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.99999952,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -605,7 +605,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.84,
|
||||
"NextFootholdId": 20,
|
||||
"PreviousFootholdId": 18,
|
||||
"groupID": 1,
|
||||
@@ -620,14 +620,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 19,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.99999952,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -0.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -651,14 +651,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 20,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -682,14 +682,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 21,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -713,14 +713,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 22,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
@@ -744,15 +744,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 23,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -2.70000029
|
||||
"x": 7.99999952,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -3.30000019
|
||||
"x": 7.99999952,
|
||||
"y": -3.3
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -775,14 +775,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 24,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -3.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -806,14 +806,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 25,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"Variance": {
|
||||
@@ -837,14 +837,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 26,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -4.5
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.99999952,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"Variance": {
|
||||
@@ -853,7 +853,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.84,
|
||||
"NextFootholdId": 1,
|
||||
"PreviousFootholdId": 28,
|
||||
"groupID": 1,
|
||||
@@ -868,15 +868,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 27,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -0.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.900001,
|
||||
"y": -0.0600000173
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -899,14 +899,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 28,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"Variance": {
|
||||
@@ -930,14 +930,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 29,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -961,14 +961,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 30,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -992,15 +992,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 31,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -3.30000019
|
||||
"x": -8.900001,
|
||||
"y": -3.3
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -2.70000029
|
||||
"x": -8.900001,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -1023,14 +1023,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 32,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -3.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1054,14 +1054,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 33,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1085,14 +1085,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00001b5b-0000-4000-8000-000000001b5b",
|
||||
"Id": 34,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.900001,
|
||||
"y": -4.5
|
||||
},
|
||||
"Variance": {
|
||||
@@ -6381,8 +6381,8 @@
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"y": 0.03499998,
|
||||
"x": 3.50589752,
|
||||
"y": 0.0149999857,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
@@ -6514,7 +6514,7 @@
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"displayOrder": 5,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
@@ -6529,8 +6529,8 @@
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"y": 0.03499998,
|
||||
"x": 5.48230743,
|
||||
"y": 0.0149999857,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
|
||||
516
map/map10.map
516
map/map10.map
@@ -47,7 +47,7 @@
|
||||
"FootholdsByLayer": {
|
||||
"1": [
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.22999954,
|
||||
"NextFootholdId": 2,
|
||||
"PreviousFootholdId": 27,
|
||||
"groupID": 1,
|
||||
@@ -62,15 +62,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 1,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.88,
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -7.65000057,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -93,15 +93,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 2,
|
||||
"StartPoint": {
|
||||
"x": -7.64999962,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -6.75,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -124,15 +124,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 3,
|
||||
"StartPoint": {
|
||||
"x": -6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -5.85,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -155,15 +155,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 4,
|
||||
"StartPoint": {
|
||||
"x": -5.84999943,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -186,15 +186,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 5,
|
||||
"StartPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -217,15 +217,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 6,
|
||||
"StartPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -248,15 +248,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 7,
|
||||
"StartPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -279,15 +279,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 8,
|
||||
"StartPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -310,15 +310,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 9,
|
||||
"StartPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -341,15 +341,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 10,
|
||||
"StartPoint": {
|
||||
"x": -0.45,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -372,15 +372,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 11,
|
||||
"StartPoint": {
|
||||
"x": 0.450000018,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 1.35,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -403,15 +403,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 12,
|
||||
"StartPoint": {
|
||||
"x": 1.34999979,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -434,15 +434,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 13,
|
||||
"StartPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 3.15,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -465,15 +465,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 14,
|
||||
"StartPoint": {
|
||||
"x": 3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.04999971,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -496,15 +496,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 15,
|
||||
"StartPoint": {
|
||||
"x": 4.05,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.95,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -527,15 +527,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 16,
|
||||
"StartPoint": {
|
||||
"x": 4.95000029,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -558,15 +558,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 17,
|
||||
"StartPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -574,7 +574,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.23,
|
||||
"NextFootholdId": 19,
|
||||
"PreviousFootholdId": 17,
|
||||
"groupID": 1,
|
||||
@@ -589,15 +589,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 18,
|
||||
"StartPoint": {
|
||||
"x": 6.75,
|
||||
"y": -0.04000002
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.98,
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -605,7 +605,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.9,
|
||||
"NextFootholdId": 20,
|
||||
"PreviousFootholdId": 18,
|
||||
"groupID": 1,
|
||||
@@ -620,14 +620,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 19,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.98,
|
||||
"y": 0
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -0.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -651,14 +651,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 20,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -682,14 +682,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 21,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -713,14 +713,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 22,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
@@ -744,14 +744,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 23,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -2.70000029
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"Variance": {
|
||||
@@ -775,14 +775,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 24,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -3.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -806,14 +806,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 25,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"Variance": {
|
||||
@@ -837,14 +837,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 26,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -4.5
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"Variance": {
|
||||
@@ -853,7 +853,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.9,
|
||||
"NextFootholdId": 1,
|
||||
"PreviousFootholdId": 28,
|
||||
"groupID": 1,
|
||||
@@ -868,15 +868,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 27,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -0.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.88,
|
||||
"y": 0
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -899,14 +899,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 28,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"Variance": {
|
||||
@@ -930,14 +930,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 29,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -961,14 +961,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 30,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -992,14 +992,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 31,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -2.70000029
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1023,14 +1023,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 32,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -3.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1054,14 +1054,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 33,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1085,14 +1085,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002713-0000-4000-8000-000000002713",
|
||||
"Id": 34,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -4.5
|
||||
},
|
||||
"Variance": {
|
||||
@@ -6355,154 +6355,6 @@
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00002715-0000-4000-8000-000000002715",
|
||||
"path": "/maps/map10/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map10/Monster1",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00002715-0000-4000-8000-000000002715",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"Scale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "3109357701ae41a4bcc7543f52f1f4c3",
|
||||
"hit": "ce0269079e884545b5bb6ea075e2a67f",
|
||||
"die": "a5e65650e00e47878cac1be7a5b999a0"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "3109357701ae41a4bcc7543f52f1f4c3",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.78,
|
||||
"y": 0.86
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.03999999,
|
||||
"y": 0.43
|
||||
},
|
||||
"CollisionGroup": {
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.KinematicbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SideviewbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"Enable": false,
|
||||
"InputSpeed": 0
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00002716-0000-4000-8000-000000002716",
|
||||
"path": "/maps/map10/Monster2",
|
||||
@@ -6650,6 +6502,154 @@
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00002715-0000-4000-8000-000000002715",
|
||||
"path": "/maps/map10/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map10/Monster1",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 5,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00002715-0000-4000-8000-000000002715",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"Scale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "3109357701ae41a4bcc7543f52f1f4c3",
|
||||
"hit": "ce0269079e884545b5bb6ea075e2a67f",
|
||||
"die": "a5e65650e00e47878cac1be7a5b999a0"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "3109357701ae41a4bcc7543f52f1f4c3",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.78,
|
||||
"y": 0.86
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.03999999,
|
||||
"y": 0.43
|
||||
},
|
||||
"CollisionGroup": {
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.KinematicbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SideviewbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"Enable": false,
|
||||
"InputSpeed": 0
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
250
map/map11.map
250
map/map11.map
@@ -47,7 +47,7 @@
|
||||
"FootholdsByLayer": {
|
||||
"1": [
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.2300005,
|
||||
"NextFootholdId": 2,
|
||||
"PreviousFootholdId": 27,
|
||||
"groupID": 1,
|
||||
@@ -62,15 +62,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 1,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.88,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -7.65000057,
|
||||
"y": -0.04000002
|
||||
"x": -7.64999962,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -93,15 +93,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 2,
|
||||
"StartPoint": {
|
||||
"x": -7.64999962,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -124,15 +124,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 3,
|
||||
"StartPoint": {
|
||||
"x": -6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -155,15 +155,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 4,
|
||||
"StartPoint": {
|
||||
"x": -5.84999943,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -186,15 +186,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 5,
|
||||
"StartPoint": {
|
||||
"x": -4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -217,15 +217,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 6,
|
||||
"StartPoint": {
|
||||
"x": -4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -248,15 +248,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 7,
|
||||
"StartPoint": {
|
||||
"x": -3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -264,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.899999738,
|
||||
"Length": 0.9,
|
||||
"NextFootholdId": 9,
|
||||
"PreviousFootholdId": 7,
|
||||
"groupID": 1,
|
||||
@@ -279,15 +279,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 8,
|
||||
"StartPoint": {
|
||||
"x": -2.24999976,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"x": -1.34999979,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -310,15 +310,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 9,
|
||||
"StartPoint": {
|
||||
"x": -1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -341,15 +341,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 10,
|
||||
"StartPoint": {
|
||||
"x": -0.45,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 0.449999958,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -372,15 +372,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 11,
|
||||
"StartPoint": {
|
||||
"x": 0.450000018,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 1.35,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -403,15 +403,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 12,
|
||||
"StartPoint": {
|
||||
"x": 1.34999979,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -434,15 +434,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 13,
|
||||
"StartPoint": {
|
||||
"x": 2.25,
|
||||
"y": -0.04000002
|
||||
"x": 2.24999976,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 3.15,
|
||||
"y": -0.04000002
|
||||
"x": 3.14999986,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -465,15 +465,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 14,
|
||||
"StartPoint": {
|
||||
"x": 3.14999986,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.04999971,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -496,15 +496,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 15,
|
||||
"StartPoint": {
|
||||
"x": 4.05,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 4.95,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -527,15 +527,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 16,
|
||||
"StartPoint": {
|
||||
"x": 4.95000029,
|
||||
"y": -0.04000002
|
||||
"x": 4.95,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"x": 5.84999943,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -558,15 +558,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 17,
|
||||
"StartPoint": {
|
||||
"x": 5.85,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 6.74999952,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -574,7 +574,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 1.27999973,
|
||||
"Length": 1.23,
|
||||
"NextFootholdId": 19,
|
||||
"PreviousFootholdId": 17,
|
||||
"groupID": 1,
|
||||
@@ -589,15 +589,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 18,
|
||||
"StartPoint": {
|
||||
"x": 6.75,
|
||||
"y": -0.04000002
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.98,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 1,
|
||||
@@ -605,7 +605,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.75,
|
||||
"NextFootholdId": 20,
|
||||
"PreviousFootholdId": 18,
|
||||
"groupID": 1,
|
||||
@@ -620,14 +620,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 19,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -0.04000002
|
||||
"x": 7.98,
|
||||
"y": -0.15
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -0.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -651,14 +651,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 20,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -682,14 +682,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 21,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -713,14 +713,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 22,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
@@ -744,15 +744,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 23,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"y": -2.70000029
|
||||
"x": 7.98,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"y": -3.30000019
|
||||
"x": 7.98,
|
||||
"y": -3.3
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -775,14 +775,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 24,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -3.9
|
||||
},
|
||||
"Variance": {
|
||||
@@ -806,14 +806,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 25,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"Variance": {
|
||||
@@ -837,14 +837,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 26,
|
||||
"StartPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -4.5
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": 8.03,
|
||||
"x": 7.98,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"Variance": {
|
||||
@@ -853,7 +853,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"Length": 0.859999955,
|
||||
"Length": 0.75,
|
||||
"NextFootholdId": 1,
|
||||
"PreviousFootholdId": 28,
|
||||
"groupID": 1,
|
||||
@@ -868,15 +868,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 27,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -0.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -0.04000002
|
||||
"x": -8.88,
|
||||
"y": -0.15
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -899,14 +899,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 28,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -0.9000001
|
||||
},
|
||||
"Variance": {
|
||||
@@ -930,14 +930,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 29,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -1.50000012
|
||||
},
|
||||
"Variance": {
|
||||
@@ -961,14 +961,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 30,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -2.7
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -2.10000014
|
||||
},
|
||||
"Variance": {
|
||||
@@ -992,15 +992,15 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 31,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"y": -3.30000019
|
||||
"x": -8.88,
|
||||
"y": -3.3
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"y": -2.70000029
|
||||
"x": -8.88,
|
||||
"y": -2.7
|
||||
},
|
||||
"Variance": {
|
||||
"x": 0,
|
||||
@@ -1023,14 +1023,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 32,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -3.9
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -3.30000019
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1054,14 +1054,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 33,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -4.50000048
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -3.90000033
|
||||
},
|
||||
"Variance": {
|
||||
@@ -1085,14 +1085,14 @@
|
||||
"isCustomFoothold": false,
|
||||
"inertiaOption": 0
|
||||
},
|
||||
"OwnerId": "c9a3018a-f6fa-4c4b-b91e-404ac5ce9858",
|
||||
"OwnerId": "00002afb-0000-4000-8000-000000002afb",
|
||||
"Id": 34,
|
||||
"StartPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -5.10000038
|
||||
},
|
||||
"EndPoint": {
|
||||
"x": -8.93,
|
||||
"x": -8.88,
|
||||
"y": -4.5
|
||||
},
|
||||
"Variance": {
|
||||
@@ -6381,8 +6381,8 @@
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"y": 0.03499998,
|
||||
"x": 3.50589752,
|
||||
"y": -0.075,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
@@ -6514,7 +6514,7 @@
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"displayOrder": 5,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
@@ -6530,7 +6530,7 @@
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"y": 0.03499998,
|
||||
"y": -0.075,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
199
tools/sim-balance.mjs
Normal file
199
tools/sim-balance.mjs
Normal file
@@ -0,0 +1,199 @@
|
||||
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
|
||||
// ⚠️ 전투 규칙은 tools/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것.
|
||||
// (데이터는 data/*.json 공유, 규칙 로직은 JS로 중복 재현)
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
export const PLAYER_HP = 80; // 데이터 미포함 placeholder (codeblock과 일치)
|
||||
export const ENERGY = 3;
|
||||
export const HAND_SIZE = 5;
|
||||
export const MAX_TURNS = 100;
|
||||
|
||||
export function mulberry32(seed) {
|
||||
let a = seed >>> 0;
|
||||
return function () {
|
||||
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
export function shuffle(arr, rng) {
|
||||
const a = arr.slice();
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rng() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// 방어 우선 차감 후 hp 적용 → { hp, block }
|
||||
export function applyDamage(hp, block, amount) {
|
||||
let dmg = amount;
|
||||
if (block > 0) {
|
||||
const absorbed = Math.min(block, dmg);
|
||||
block -= absorbed;
|
||||
dmg -= absorbed;
|
||||
}
|
||||
hp -= dmg;
|
||||
if (hp < 0) hp = 0;
|
||||
return { hp, block };
|
||||
}
|
||||
|
||||
export function loadData() {
|
||||
const cardsData = JSON.parse(readFileSync('data/cards.json', 'utf8'));
|
||||
const enemiesData = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
|
||||
const enemy = enemiesData.enemies[enemiesData.activeEnemy];
|
||||
if (!enemy) throw new Error(`activeEnemy 없음: ${enemiesData.activeEnemy}`);
|
||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDeck, enemy };
|
||||
}
|
||||
|
||||
// 손패에서 다음에 낼 카드의 인덱스 반환(-1=턴 종료). hand=카드 id 배열.
|
||||
export function chooseAction(hand, cards, energy, enemyHp, enemyBlock, enemyIntent) {
|
||||
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id].cost <= energy);
|
||||
const attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
|
||||
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
|
||||
const dmgEff = (x) => (cards[x.id].damage || 0) / cards[x.id].cost;
|
||||
const blkEff = (x) => (cards[x.id].block || 0) / cards[x.id].cost;
|
||||
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
|
||||
|
||||
// 1) 치사: 에너지 한도 내 효율순 공격 데미지 합 >= 적 유효 hp?
|
||||
let e = energy, lethalDmg = 0;
|
||||
for (const x of attacks.slice().sort((a, b) => dmgEff(b) - dmgEff(a))) {
|
||||
if (cards[x.id].cost <= e) { e -= cards[x.id].cost; lethalDmg += cards[x.id].damage || 0; }
|
||||
}
|
||||
if (attacks.length && lethalDmg >= enemyHp + enemyBlock) return bestBy(attacks, dmgEff).i;
|
||||
|
||||
// 2) 적 공격 의도면 방어 우선
|
||||
if (enemyIntent && enemyIntent.kind === 'Attack' && skills.length) return bestBy(skills, blkEff).i;
|
||||
|
||||
// 3) 공격 우선, 없으면 스킬, 없으면 종료
|
||||
if (attacks.length) return bestBy(attacks, dmgEff).i;
|
||||
if (skills.length) return bestBy(skills, blkEff).i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function bump(s, cost, dmg, blk) {
|
||||
s = s || { plays: 0, energy: 0, damage: 0, block: 0 };
|
||||
s.plays++; s.energy += cost; s.damage += dmg; s.block += blk;
|
||||
return s;
|
||||
}
|
||||
|
||||
// 단일 전투 시뮬. stats(선택): {cardId: {plays,energy,damage,block}} 누적.
|
||||
// 반환: { win, turns, playerHpRemaining, draw? }
|
||||
export function simulateCombat(data, rng, stats) {
|
||||
const { cards, starterDeck, enemy } = data;
|
||||
let drawPile = shuffle(starterDeck, rng);
|
||||
let discard = [];
|
||||
let hand = [];
|
||||
let pHp = PLAYER_HP, pBlock = 0;
|
||||
let eHp = enemy.maxHp, eBlock = 0, intentIdx = 0;
|
||||
let turns = 0;
|
||||
|
||||
function draw(n) {
|
||||
for (let k = 0; k < n; k++) {
|
||||
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
|
||||
if (drawPile.length === 0) break;
|
||||
hand.push(drawPile.pop());
|
||||
}
|
||||
}
|
||||
|
||||
while (turns < MAX_TURNS) {
|
||||
turns++;
|
||||
let energy = ENERGY; pBlock = 0; hand = []; draw(HAND_SIZE);
|
||||
while (true) {
|
||||
const intent = enemy.intents[intentIdx];
|
||||
const idx = chooseAction(hand, cards, energy, eHp, eBlock, intent);
|
||||
if (idx < 0) break;
|
||||
const id = hand[idx], c = cards[id];
|
||||
energy -= c.cost;
|
||||
if (c.kind === 'Attack') {
|
||||
const r = applyDamage(eHp, eBlock, c.damage || 0); eHp = r.hp; eBlock = r.block;
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, c.damage || 0, 0);
|
||||
} else {
|
||||
pBlock += c.block || 0;
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, 0, c.block || 0);
|
||||
}
|
||||
hand.splice(idx, 1); discard.push(id);
|
||||
if (eHp <= 0) return { win: true, turns, playerHpRemaining: pHp };
|
||||
}
|
||||
discard.push(...hand); hand = [];
|
||||
eBlock = 0;
|
||||
const intent = enemy.intents[intentIdx];
|
||||
if (intent.kind === 'Attack') { const r = applyDamage(pHp, pBlock, intent.value); pHp = r.hp; pBlock = r.block; }
|
||||
else if (intent.kind === 'Defend') { eBlock += intent.value; }
|
||||
intentIdx = (intentIdx + 1) % enemy.intents.length;
|
||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||
}
|
||||
return { win: false, turns, playerHpRemaining: pHp, draw: true };
|
||||
}
|
||||
|
||||
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }
|
||||
function median(a) {
|
||||
if (!a.length) return 0;
|
||||
const s = a.slice().sort((x, y) => x - y), m = Math.floor(s.length / 2);
|
||||
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
||||
}
|
||||
|
||||
export function runBatch(N, seed) {
|
||||
const data = loadData();
|
||||
const rng = mulberry32(seed);
|
||||
const cardStats = {};
|
||||
let wins = 0, draws = 0;
|
||||
const turnsArr = [], hpArr = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
const r = simulateCombat(data, rng, cardStats);
|
||||
if (r.draw) draws++;
|
||||
if (r.win) { wins++; hpArr.push(r.playerHpRemaining); }
|
||||
turnsArr.push(r.turns);
|
||||
}
|
||||
return {
|
||||
N, wins, draws, losses: N - wins - draws,
|
||||
winRate: wins / N,
|
||||
avgTurns: mean(turnsArr), medianTurns: median(turnsArr),
|
||||
avgHpOnWin: mean(hpArr),
|
||||
cardStats, cards: data.cards, enemy: data.enemy, seed,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatReport(r) {
|
||||
const L = [];
|
||||
L.push(`=== 밸런스 시뮬레이션 (적: ${r.enemy.name} HP ${r.enemy.maxHp}) ===`);
|
||||
L.push(`시뮬 ${r.N}회 (seed=${r.seed})`);
|
||||
L.push(`승률: ${(r.winRate * 100).toFixed(1)}% (승 ${r.wins} / 패 ${r.losses}${r.draws ? ` / 무 ${r.draws}` : ''})`);
|
||||
L.push(`평균 턴: ${r.avgTurns.toFixed(2)} 중앙값 턴: ${r.medianTurns}`);
|
||||
L.push(`승리 시 평균 잔여 HP: ${r.avgHpOnWin.toFixed(1)} / ${PLAYER_HP}`);
|
||||
if (r.draws) L.push(`⚠️ 무승부 ${r.draws}건 (턴 상한 ${MAX_TURNS} 초과)`);
|
||||
L.push('');
|
||||
L.push('카드별:');
|
||||
const rows = Object.entries(r.cardStats).map(([id, s]) => {
|
||||
const kind = r.cards[id].kind;
|
||||
const eff = kind === 'Attack' ? s.damage / s.energy : s.block / s.energy;
|
||||
return { id, name: r.cards[id].name, kind, plays: s.plays, eff };
|
||||
});
|
||||
for (const kind of ['Attack', 'Skill']) {
|
||||
const kr = rows.filter((x) => x.kind === kind);
|
||||
if (!kr.length) continue;
|
||||
const med = median(kr.map((x) => x.eff));
|
||||
const unit = kind === 'Attack' ? '뎀/E' : '블록/E';
|
||||
for (const x of kr) {
|
||||
const op = med > 0 && x.eff >= med * 1.5 ? ' ⚠️ OP 의심' : '';
|
||||
L.push(` ${x.name}(${x.id}): 사용 ${x.plays}, 효율 ${x.eff.toFixed(2)} ${unit}${op}`);
|
||||
}
|
||||
}
|
||||
const sorted = rows.slice().sort((a, b) => b.plays - a.plays);
|
||||
if (sorted.length) L.push(`최다 사용: ${sorted[0].name} / 최소 사용: ${sorted[sorted.length - 1].name}`);
|
||||
return L.join('\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let N = 2000, seed = 1;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--seed') seed = parseInt(args[++i], 10);
|
||||
else if (/^\d+$/.test(args[i])) N = parseInt(args[i], 10);
|
||||
}
|
||||
console.log(formatReport(runBatch(N, seed)));
|
||||
}
|
||||
|
||||
if (process.argv[1] && process.argv[1].endsWith('sim-balance.mjs')) main();
|
||||
80
tools/sim-balance.test.mjs
Normal file
80
tools/sim-balance.test.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
mulberry32, applyDamage, chooseAction, simulateCombat, runBatch,
|
||||
} from './sim-balance.mjs';
|
||||
|
||||
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
||||
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
||||
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
||||
assert.deepEqual(applyDamage(80, 12, 10), { hp: 80, block: 2 });
|
||||
assert.deepEqual(applyDamage(3, 0, 10), { hp: 0, block: 0 });
|
||||
});
|
||||
|
||||
test('mulberry32: 동일 시드 동일 수열', () => {
|
||||
const a = mulberry32(1), b = mulberry32(1);
|
||||
assert.equal(a(), b());
|
||||
assert.equal(a(), b());
|
||||
});
|
||||
|
||||
const CARDS = {
|
||||
Strike: { name: '타격', cost: 1, kind: 'Attack', damage: 6 },
|
||||
Defend: { name: '방어', cost: 1, kind: 'Skill', block: 5 },
|
||||
Bash: { name: '강타', cost: 2, kind: 'Attack', damage: 10 },
|
||||
};
|
||||
|
||||
test('chooseAction: 치사 가능하면 공격 선택', () => {
|
||||
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 5, 0, { kind: 'Attack', value: 10 });
|
||||
assert.equal(idx, 0);
|
||||
});
|
||||
|
||||
test('chooseAction: 치사 불가 + 적 공격 의도면 방어 선택', () => {
|
||||
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 40, 0, { kind: 'Attack', value: 10 });
|
||||
assert.equal(idx, 1);
|
||||
});
|
||||
|
||||
test('chooseAction: 적 방어 의도면 공격 우선', () => {
|
||||
const idx = chooseAction(['Defend', 'Strike'], CARDS, 3, 40, 0, { kind: 'Defend', value: 8 });
|
||||
assert.equal(idx, 1);
|
||||
});
|
||||
|
||||
test('chooseAction: 사용 가능 카드 없으면 -1', () => {
|
||||
const idx = chooseAction(['Bash'], CARDS, 1, 40, 0, { kind: 'Attack', value: 10 });
|
||||
assert.equal(idx, -1);
|
||||
});
|
||||
|
||||
const DATA = {
|
||||
cards: CARDS,
|
||||
starterDeck: ['Strike', 'Strike', 'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend', 'Defend', 'Bash'],
|
||||
enemy: {
|
||||
name: '슬라임', maxHp: 45, intents: [
|
||||
{ kind: 'Attack', value: 10 }, { kind: 'Attack', value: 6 }, { kind: 'Defend', value: 8 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
test('simulateCombat: 결정적 결과(동일 시드)', () => {
|
||||
const r1 = simulateCombat(DATA, mulberry32(1));
|
||||
const r2 = simulateCombat(DATA, mulberry32(1));
|
||||
assert.deepEqual(r1, r2);
|
||||
assert.equal(typeof r1.win, 'boolean');
|
||||
assert.ok(r1.turns >= 1);
|
||||
});
|
||||
|
||||
test('simulateCombat: 약한 적이면 대체로 승리', () => {
|
||||
let wins = 0;
|
||||
for (let i = 0; i < 50; i++) if (simulateCombat(DATA, mulberry32(i + 1)).win) wins++;
|
||||
assert.ok(wins >= 40, `예상 승리 다수, 실제 ${wins}/50`);
|
||||
});
|
||||
|
||||
test('runBatch: 집계 필드·승률 범위', () => {
|
||||
const r = runBatch(100, 1);
|
||||
assert.equal(r.N, 100);
|
||||
assert.ok(r.winRate >= 0 && r.winRate <= 1);
|
||||
assert.ok(r.avgTurns > 0);
|
||||
assert.ok(r.cardStats.Strike.plays > 0);
|
||||
});
|
||||
|
||||
test('runBatch: 동일 시드 동일 결과', () => {
|
||||
assert.deepEqual(runBatch(100, 7), runBatch(100, 7));
|
||||
});
|
||||
18093
ui/DefaultGroup.ui
18093
ui/DefaultGroup.ui
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user