Compare commits
300 Commits
8f08e67e4c
...
feature/ch
| Author | SHA1 | Date | |
|---|---|---|---|
| 48b43f23e6 | |||
| d1473ad9e2 | |||
| a5388da2cc | |||
| 8ca48eca60 | |||
| eeca77df35 | |||
| 40e351333e | |||
| 0a83dea2d8 | |||
| fbf4d8d02d | |||
| a141939675 | |||
| 42eb33b579 | |||
| 9f7713267c | |||
| bfa86f0f28 | |||
| c5bb8c18a9 | |||
| 420cce561c | |||
| d265c8f918 | |||
| 255781d969 | |||
| b904b29503 | |||
| 0435a76fc1 | |||
| d82e98f832 | |||
| eafd6747a7 | |||
| bc266b1885 | |||
| e6a397cc55 | |||
| fcc103227c | |||
| 44878bab9e | |||
| 064d81d424 | |||
| 62187db5dd | |||
| 1a5953050f | |||
| a902cb8bce | |||
| b23dc3868e | |||
| 98ca1668c8 | |||
| 654a49f3a2 | |||
| 3e4619ed2f | |||
| aa872afa7b | |||
| 1eb6622cf5 | |||
| 8309b25ec5 | |||
| 00903f2659 | |||
| f2c470f972 | |||
| 2e8a1ab869 | |||
| 4228f58b09 | |||
| 5e0eca6cdf | |||
| 4da934585c | |||
| 49069a16cf | |||
| bda35eefc7 | |||
| 44010e0fce | |||
| efa32d0a8f | |||
| 7c776864e2 | |||
| 72370aab23 | |||
| 5377112826 | |||
| 8a5b0d4f8d | |||
| 6c35d959ac | |||
| 67d21a9619 | |||
| b1d0af311a | |||
| 5b41eb78a4 | |||
| 3902c9b1ee | |||
| d1e51878c3 | |||
| cc945fce8b | |||
| 9966065409 | |||
| bc9bc78cef | |||
| 9cb5e1abff | |||
| 1fce0b284a | |||
| e269154d17 | |||
| b65d4af1eb | |||
| d5318ac86b | |||
| bd91c67483 | |||
| b43ee02014 | |||
| 6427d23f50 | |||
| b40c8d11d8 | |||
| f9e7bc3603 | |||
| 256433d3f3 | |||
| 05a06644cf | |||
| 709e6f8f99 | |||
| a88c1d344c | |||
| a24f3592c4 | |||
| 3db11f5d82 | |||
| 6e1f1cf990 | |||
| 304b2f3c2a | |||
| 15bc17b351 | |||
| 6f436ef3eb | |||
| cf193bf51a | |||
| 1e87be2cd6 | |||
| 6cc008e894 | |||
| 760b856576 | |||
| 91bbe7d200 | |||
| 989e3fe000 | |||
| 0e064cc1e9 | |||
| de1e69de7d | |||
| a683f186d4 | |||
| a309da2a99 | |||
| 82bf22d4cc | |||
| f36bc0d14e | |||
| 1f0a8099ee | |||
| a5f6a4509d | |||
| d3ae6c1c62 | |||
| 4d3f6fc0af | |||
| a2b8d6bfb9 | |||
| 8f233296af | |||
| 5f89d61a8b | |||
| 8879647b26 | |||
| 7ee323ea8b | |||
| 7cc311ee91 | |||
| 5cde11647f | |||
| 8296775e21 | |||
| d546d62755 | |||
| 8f58a90746 | |||
| 8fa1079548 | |||
| 9e16465218 | |||
| f67471435e | |||
| fd57e0d56d | |||
| afe995a895 | |||
| 6a6b64cbc5 | |||
| b2693be111 | |||
| 675616bf51 | |||
| 1e48fa35b3 | |||
| aaa68ebe07 | |||
| 35dfcbaffe | |||
| 9aa4721790 | |||
| e553ebe666 | |||
| a814bf2c4b | |||
| 9e162d6e2d | |||
| 8baa97bde8 | |||
| 66c1ac8ee1 | |||
| abd6d00052 | |||
| 2cd672b474 | |||
| 56d958fe19 | |||
| 7aed1943b7 | |||
| 9989a61675 | |||
| 2c28935d95 | |||
| b635cb3a63 | |||
| bcdf9457c8 | |||
| 89056e903d | |||
| fc0a96fcb7 | |||
| 7b6e181cb0 | |||
| e1d298f972 | |||
| 811c8ec2ac | |||
| 6b6037739f | |||
| 80c5daabbf | |||
| 6e8d1a88f5 | |||
| 13d1ccb771 | |||
| d0b8fbe091 | |||
| 2c9a1b351e | |||
| 74e3a70a19 | |||
| 15b342972d | |||
| 1925144f85 | |||
| 2a0ec0ef21 | |||
| 65ad2fe854 | |||
| d7e4e88182 | |||
| 52c03b208e | |||
| 76aaafcf1b | |||
| c69a17abe0 | |||
| 1abd9f7987 | |||
| 1624ef6f3b | |||
| 443aaf83d2 | |||
| 67e8b4c848 | |||
| 52d808eacd | |||
| b5a648fc23 | |||
| 62b2193f2e | |||
| 2a42c4a372 | |||
| 42f9143f73 | |||
| 239dd6caf3 | |||
| e4f7ff10d7 | |||
| 6b8db0b871 | |||
| 4c3be95c86 | |||
| 88a9136398 | |||
| 0a96a6955a | |||
| 00844857ee | |||
| 6df3fcf01c | |||
| 2bf5716fce | |||
| 12f3928ab4 | |||
| eb06663758 | |||
| 02aa73dda3 | |||
| 134353d374 | |||
| 013f946c6b | |||
| 8ff50b428d | |||
| 1de8fac893 | |||
| 248677759c | |||
| 2af67b3b2b | |||
| 7890f4c029 | |||
| 32a0b2437b | |||
| dd2a814eeb | |||
| 4a1c66c5fe | |||
| 869e4d88df | |||
| e876a8ce3a | |||
| f88e6ffeeb | |||
| 5921dfbeb8 | |||
| 4b7ad753a4 | |||
| c80020a464 | |||
| 858f9727dd | |||
| 1c2062b8cc | |||
| d4ec8757ce | |||
| 76cb2cd65e | |||
| d2aba643fa | |||
| d783cccea1 | |||
| c4ad8fc068 | |||
| 4ea9bfe14b | |||
| 18729a2047 | |||
| 50462f3d8a | |||
| 2f73910e47 | |||
| 659ae2a60f | |||
| b5facb7763 | |||
| 1762112296 | |||
| 389e7f36cf | |||
| 24e6aca305 | |||
| d1f894a802 | |||
| c5e28062eb | |||
| c7d795f839 | |||
| 2265bd7fa1 | |||
| fb5f0d6d5b | |||
| 569c1d5eb4 | |||
| 066ad6ddca | |||
| 1a15225ff6 | |||
| 33bc98c015 | |||
| 895e521724 | |||
| cd7ae102ca | |||
| d0353fb81f | |||
| 6eea4f9e17 | |||
| 903a06d233 | |||
| c614b10566 | |||
| 6feb252674 | |||
| 428bdc8a2e | |||
| 271a7991d1 | |||
| 59c699c04b | |||
| 0d517617a3 | |||
| d247115ad7 | |||
| 26c084d951 | |||
| 6349be63aa | |||
| 7167ece6a7 | |||
| 76e60d3350 | |||
| e241382d09 | |||
| f4b349532d | |||
| 683ea88271 | |||
| 516348c0ec | |||
| f211a79c82 | |||
| f33a5507db | |||
| 647516d0cd | |||
| f704d0f14e | |||
| f0569d9a53 | |||
| a5c7d96770 | |||
| 423407325d | |||
| ec45438b3c | |||
| 020be477e6 | |||
| 9eef5eb66e | |||
| 185e0f3a94 | |||
| de23829439 | |||
| 4ef3d1811d | |||
| b14b614d94 | |||
| 0cbcf4c70d | |||
| 1583f7ec26 | |||
| da5dd03183 | |||
| c0cbea42a2 | |||
| de6e12c765 | |||
| f38a3c98b1 | |||
| 62e76f7db2 | |||
| ff91680f18 | |||
| b18c44f0a5 | |||
| 124e49b938 | |||
| c9f82708c8 | |||
| f1d101f6a4 | |||
| 5dfbef4f0f | |||
| fbf5cfe19f | |||
| 9eeb12adf9 | |||
| 68c9333b59 | |||
| 8405395d84 | |||
| f310d2978e | |||
| 971539a72f | |||
| 861442e2c1 | |||
| cb4d72ead2 | |||
| 376511dfa9 | |||
| f42628c2e9 | |||
| ace489ed0f | |||
| 4c866f3cd9 | |||
| 5ebc781f81 | |||
| edbc717426 | |||
| 03b59eeafc | |||
| 0400291939 | |||
| 42ce7286f5 | |||
| 444d02367e | |||
| 15975d7f51 | |||
| e5960e150b | |||
| 266b7ddb0c | |||
| e9b6d9c6c0 | |||
| 3be5e85c94 | |||
| 911f45407c | |||
| f42e03a006 | |||
| e6994f92f7 | |||
| 929347c599 | |||
| 8789330a4e | |||
| 9550302137 | |||
| e8ed467cda | |||
| 9206018fbe | |||
| 788167b1ae | |||
| e5a75a83b5 | |||
| 724cd5a04d | |||
| bd02865f4f | |||
| 14449373e0 | |||
|
|
27818e92c7 | ||
|
|
1299d718e2 | ||
|
|
913b4f1721 | ||
|
|
a9926feea3 | ||
|
|
8eab9a75ac | ||
|
|
8c397cbc09 |
20
.claude/settings.json
Normal file
20
.claude/settings.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Read(./ui/*.ui)",
|
||||
"Read(./map/*.map)",
|
||||
"Read(./RootDesk/MyDesk/*.codeblock)",
|
||||
"Edit(./ui/*.ui)",
|
||||
"Edit(./map/*.map)",
|
||||
"Edit(./RootDesk/MyDesk/*.codeblock)",
|
||||
"Edit(./Global/common.gamelogic)",
|
||||
"Edit(./Global/SectorConfig.config)",
|
||||
"Write(./ui/*.ui)",
|
||||
"Write(./map/*.map)",
|
||||
"Write(./RootDesk/MyDesk/*.codeblock)",
|
||||
"Write(./Global/common.gamelogic)",
|
||||
"Write(./Global/SectorConfig.config)"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,8 +4,11 @@
|
||||
# Codex CLI 로컬 설정 — Authorization Bearer 토큰 포함
|
||||
.codex/
|
||||
.agents/
|
||||
# Claude Code 로컬 설정
|
||||
.claude/
|
||||
# Claude Code 로컬 설정 — 단, 팀 공유 하네스 설정(settings.json)은 커밋 (RULES.md 참조)
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
# 개인 스킬(superpowers) 브레인스토밍/계획 산출물 — 로컬 전용, 협업 공유 X (프로젝트 설계 문서 docs/*.md 는 추적 유지)
|
||||
docs/superpowers/
|
||||
|
||||
# === OS / 에디터 잡파일 ===
|
||||
Thumbs.db
|
||||
|
||||
7
CLAUDE.md
Normal file
7
CLAUDE.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# SlayMaple — CLAUDE.md
|
||||
|
||||
MapleStory Worlds 기반 Slay the Spire 풍 덱빌더. 게임 전체가 데이터(`data/*.json`) + 생성기(`tools/`) 단일 소스이고, `ui/DefaultGroup.ui`(~7.1MB)·codeblock·map 파일은 **생성 산출물**이다.
|
||||
|
||||
@RULES.md
|
||||
|
||||
위 RULES.md의 하네스 규칙(산출물 Read/Edit 금지·카운트 검증·gitea-pr.mjs PR 절차)을 항상 따른다. `.claude/settings.json`이 산출물에 대한 Read/Edit/Write를 도구 수준에서 차단한다.
|
||||
@@ -23,7 +23,6 @@
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.AIChaseComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
@@ -150,4 +149,4 @@
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 1.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
@@ -39,7 +39,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 1.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
@@ -48,7 +48,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 1.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
@@ -57,7 +57,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 1.0
|
||||
"Value": 1
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
@@ -118,7 +118,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
@@ -129,8 +129,8 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -185,7 +185,7 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"x": 0,
|
||||
"y": 0.35
|
||||
}
|
||||
},
|
||||
@@ -198,7 +198,7 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"x": 0,
|
||||
"y": 0.35
|
||||
}
|
||||
},
|
||||
@@ -218,7 +218,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 500.0
|
||||
"Value": 500
|
||||
},
|
||||
{
|
||||
"TargetType": "script.PlayerHit",
|
||||
@@ -254,7 +254,7 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"x": 0,
|
||||
"y": 0.35
|
||||
}
|
||||
},
|
||||
@@ -265,7 +265,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 1.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
@@ -274,7 +274,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 1.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.PlayerComponent",
|
||||
@@ -302,4 +302,4 @@
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.AIWanderComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
@@ -141,4 +140,4 @@
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,7 @@
|
||||
"map://map03",
|
||||
"map://map04",
|
||||
"map://map05",
|
||||
"map://map06",
|
||||
"map://map07",
|
||||
"map://map08",
|
||||
"map://map09",
|
||||
"map://map10",
|
||||
"map://map11"
|
||||
"map://lobby"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
{
|
||||
"@type": "script.SlayDeckController",
|
||||
"Enable": true,
|
||||
"Energy": 0,
|
||||
"MaxEnergy": 3,
|
||||
"Turn": 0,
|
||||
"TweenEventId": 0
|
||||
"Energy": 0.0,
|
||||
"MaxEnergy": 3.0,
|
||||
"Turn": 0.0,
|
||||
"TweenEventId": 0.0
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
175
README.md
175
README.md
@@ -1,7 +1,7 @@
|
||||
# SlayMaple
|
||||
|
||||
[MapleStory Worlds(MSW)](https://maplestoryworlds.nexon.com/) 기반으로 제작하는 **Slay the Spire 풍 덱빌더 로그라이크** 월드.
|
||||
턴제 카드 전투, 덱 구성, 보상 선택, 맵 노드 진행을 메이플 월드 위에서 구현하는 것을 목표로 합니다.
|
||||
로비 마을에서 NPC와 상호작용해 런을 시작하고, 턴제 카드 전투·덱 구성·보상 선택·절차 생성 맵 진행·전직·영혼 메타 성장을 메이플 월드 위에서 구현합니다.
|
||||
|
||||
> 이 저장소는 MSW **로컬 워크스페이스(Local Workspace)** 데이터를 git으로 형상관리하기 위한 것입니다.
|
||||
> 공동작업자는 이 저장소를 통해 월드 데이터를 주고받습니다. (클라우드 공동제작 모드 미사용)
|
||||
@@ -32,9 +32,10 @@ git push
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
받아온 뒤, 메이커에서 **로컬 워크스페이스를 다시 로드(reload)** 해야 새 codeblock/모델 파일이 에디터 상태로 반영됩니다.
|
||||
받아온 뒤, 메이커에서 **로컬 워크스페이스를 다시 로드(reload)** 해야 새 codeblock/모델/맵 파일이 에디터 상태로 반영됩니다.
|
||||
|
||||
> 💡 같은 파일을 동시에 수정하면 git 충돌이 날 수 있으니, **서로 다른 맵/codeblock/UI를 나눠서** 작업하는 것을 권장합니다.
|
||||
> ⚠️ git pull 후 reload를 빠뜨리면 메이커의 stale 상태가 디스크를 덮어쓸 수 있습니다. 재생성 후에도 reload → 빌드 콘솔 0 에러 확인.
|
||||
|
||||
---
|
||||
|
||||
@@ -42,71 +43,150 @@ git pull
|
||||
|
||||
```
|
||||
slaymaple/
|
||||
├── data/ # 게임 데이터 단일 소스 (생성기가 읽어 주입). 맵은 정적 데이터 없음(절차 생성)
|
||||
│ ├── cards.json # 카드 123장(클래스·2차전직별 + 저주 + 표창 토큰) + 클래스별 시작 덱
|
||||
│ ├── enemies.json # 적 12종(일반/정예/보스, 디버프 인텐트 포함)
|
||||
│ ├── potions.json # 물약 6종 + 드랍률·슬롯·상점가
|
||||
│ ├── relics.json # 유물 19종(StS 효과 × 메이플 장비) + 시작 유물 + 풀
|
||||
│ ├── cardframes.json # 커스텀 카드 프레임 3종(전사/마법사/도적 × normal/unique/legend) + 보상 등급 가중치
|
||||
│ └── camera.json # 맵별 카메라 설정값(줌·오프셋·고정 영역)
|
||||
├── Global/ # 월드 전역 설정 · 공용 모델 · 게임로직
|
||||
│ ├── common.gamelogic # SlayCardCatalog / SlayRunState / SlayCombatManager 부착 지점
|
||||
│ ├── Player.model # 플레이어 모델
|
||||
│ ├── *.model # 몬스터 등 공용 모델
|
||||
│ ├── common.gamelogic # SlayDeckController 부착 지점 (산출물)
|
||||
│ ├── DefaultPlayer.model # 플레이어 모델 (턴전투용 이동 정지 freeze 적용)
|
||||
│ ├── ChaseMonster.model · MoveMonster.model # 몬스터 공용 모델
|
||||
│ ├── SectorConfig.config # 섹터/맵 등록 (lobby + map01~05 = 6 entries)
|
||||
│ ├── WorldConfig.config # 월드 설정
|
||||
│ └── ...
|
||||
├── RootDesk/
|
||||
│ └── MyDesk/ # 작업용 책상 — codeblock(스크립트)·모델·타일셋
|
||||
│ ├── Monster.codeblock
|
||||
│ ├── MonsterAttack.codeblock
|
||||
│ ├── PlayerAttack.codeblock
|
||||
│ ├── PlayerHit.codeblock
|
||||
│ ├── UIPopup.codeblock
|
||||
│ ├── UIToast.codeblock
|
||||
│ └── RectTileData_Henesys.tileset
|
||||
├── map/
|
||||
│ └── map01.map # 메인 맵
|
||||
├── ui/ # UI 그룹 (Default / Popup / Toast)
|
||||
│ └── MyDesk/ # 작업용 책상 — codeblock(스크립트)·타일셋
|
||||
│ ├── SlayDeckController.codeblock # 게임 전체 컨트롤러 (★산출물, 직접 편집 금지)
|
||||
│ ├── Monster.codeblock · MonsterAttack.codeblock # 필드 액션 몬스터 (카드 전투와 별개)
|
||||
│ ├── PlayerAttack.codeblock · PlayerHit.codeblock · UIPopup.codeblock · UIToast.codeblock
|
||||
│ ├── CombatMonster.codeblock # 맵 몬스터 EnemyId 마커 + /common 자기등록
|
||||
│ ├── MapCamera.codeblock # 맵별 카메라 적용
|
||||
│ ├── PlayerLock.codeblock # 전투맵 플레이어 입력·이동 잠금
|
||||
│ ├── LobbyNpc.codeblock # 로비 NPC 상호작용(근접·클릭)
|
||||
│ └── LobbyMobility.codeblock # 로비 이동·공격 해제 + 카메라 추종
|
||||
├── map/ # 맵 6종 (산출물)
|
||||
│ ├── lobby.map # 로비 허브 맵 (마을 배경, NPC 4종, 전투 없음)
|
||||
│ └── map01.map ~ map05.map # 5막 전투/맵 노드 (공식 배경 + STS풍 우측 배치)
|
||||
├── tools/ # 결정적 생성기·도구 (주체별 폴더, 단일 소스)
|
||||
│ ├── deck/ # ★게임 전체 생성 — gen-slaydeck.mjs(오케스트레이터) + lib/(ui-helpers·data·codeblock 공유) + hud/(화면별 UI emit 15종) + cb/(codeblock 메서드 17종). 출력=DefaultGroup.ui·SlayDeckController·common. gen-cardhand.mjs(보조)
|
||||
│ ├── map/ # gen-maps.mjs(맵 배경/타일) · gen-lobby-map.mjs(로비 맵+NPC) · gen-map-encounters.mjs(노드별 몬스터 그룹) · rogue-map.mjs(절차 생성 JS 미러)+test
|
||||
│ ├── camera/ # gen-camera.mjs(맵별 고정 카메라 codeblock)
|
||||
│ ├── player/ # gen-player-lock.mjs(전투맵 입력 잠금) · freeze-turn-player.mjs(모델 이동 정지) · gen-lobby-npc.mjs(LobbyNpc·LobbyMobility codeblock)
|
||||
│ ├── monster/ # gen-combat-monster.mjs(EnemyId 마커) · freeze-turn-monsters.mjs(필드 AI 정지)
|
||||
│ ├── balance/ # sim-balance.mjs(전투 밸런스 몬테카를로 시뮬) · sim-balance.test.mjs
|
||||
│ ├── verify/ # count.mjs(산출물 카운트 검증) · diffcheck.mjs(워킹트리 vs ref 바이트동일 검증 — 리팩터·머지용)
|
||||
│ └── git/ # gitea-pr.mjs(UTF-8 안전 PR 생성/수정/머지 — RULES.md 참조)
|
||||
├── ui/ # UI 그룹 (DefaultGroup ~6.8MB 산출물 / PopupGroup / ToastGroup)
|
||||
├── docs/
|
||||
│ └── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서
|
||||
│ ├── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서
|
||||
│ └── ui-generation-structure.md # UI 생성 구조 문서
|
||||
│ └── (superpowers/ — 개인 스킬 브레인스토밍/계획 산출물: `.gitignore` 처리·로컬 전용, 저장소 미포함)
|
||||
├── RULES.md # 협업·AI 에이전트 하네스 규칙 (토큰 가드·검증·PR 절차)
|
||||
├── CLAUDE.md # Claude Code 자동 로드 (RULES.md 임포트)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
> ⚠️ **`map/*.map` · `ui/DefaultGroup.ui` · `*.codeblock` · `Global/*.gamelogic`는 생성 산출물**입니다 — 직접 편집하면 다음 재생성 때 사라집니다. 게임 변경은 `data/*.json` 또는 `tools/`의 생성기를 고친 뒤 재생성하세요(자세한 규칙은 [`RULES.md`](RULES.md)).
|
||||
> `.mcp.json`, `.codex/` 는 **Authorization 토큰이 포함**되어 있어 git에서 제외됩니다(`.gitignore`). 각자 로컬에서 직접 구성하세요.
|
||||
> `docs/superpowers/`(개인 스킬 브레인스토밍·계획 산출물)도 `.gitignore` 처리되어 **로컬 전용**입니다 — 프로젝트 설계 문서(`docs/*.md`)만 형상관리합니다.
|
||||
|
||||
---
|
||||
|
||||
## 게임 프레임워크 현황
|
||||
|
||||
`Global/common.gamelogic`의 `/common` 엔티티에 부착된 세 컴포넌트가 전투의 핵심입니다.
|
||||
**StS2풍 덱빌더 로그라이크가 end-to-end로 완성**됐고, 이제 **로비 마을을 기점으로 반복 런**이 돕니다:
|
||||
|
||||
| 컴포넌트 | 역할 |
|
||||
|---|---|
|
||||
| `SlayCardCatalog` | 카드 데이터, 시작 덱 구성, 보상 풀, 카드 복제 정의 |
|
||||
| `SlayRunState` | HP·골드·층수·덱·유물·카드 보상 등 런(run) 영속 데이터 관리 |
|
||||
| `SlayCombatManager` | 턴 진행, 드로우/버림/소멸 더미, 에너지, 적 의도, 방어도, 데미지, 승패 처리 |
|
||||
|
||||
### 프로토타입 흐름
|
||||
1. `SlayRunState`가 HP 80 · 10장 시작 덱으로 새 런 시작
|
||||
2. `SlayCombatManager`가 데모 전투 자동 시작
|
||||
3. 매 플레이어 턴: 에너지 3 회복, 방어도 초기화, 적 의도 갱신, 5장 드로우
|
||||
4. 카드 사용 시 에너지 소모 → 데미지/방어/드로우/에너지/상태이상 적용 → 버림 또는 소멸
|
||||
5. 턴 종료 시 손패 버림, 적 의도 실행, 상태이상 처리, 다음 턴 시작
|
||||
6. 전투 승리 시 잔여 HP 저장, 골드 15 지급, 카드 보상 3종 생성
|
||||
|
||||
### 유용한 스크립트 호출
|
||||
`/common` 엔티티에 붙은 스크립트에서:
|
||||
```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")
|
||||
```
|
||||
로비 맵(NPC 4종) → 모험가 NPC → 캐릭터 선택(전사/도적/마법사) → 절차 생성 맵(5막)
|
||||
→ 전투/엘리트/상점/휴식/유물 방 → 보상·전직·덱 성장 → 보스 → 다음 막
|
||||
→ 런 클리어(승천 해금) → 로비 복귀(영혼 정산) → 다음 런 …
|
||||
```
|
||||
|
||||
게임 전체는 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작하며, 모든 산출물(`ui/DefaultGroup.ui` · `SlayDeckController.codeblock` · `common.gamelogic`)은 **`tools/deck/gen-slaydeck.mjs` 단일 소스에서 생성**됩니다(결정적 출력, 직접 편집 금지 — `RULES.md` 참조). 게임 데이터는 **`data/*.json`** 가 단일 소스, 맵 구조는 **런타임 절차 생성**(`GenerateMap` Lua ↔ `tools/map/rogue-map.mjs` JS 미러).
|
||||
|
||||
### 구현된 기능 (배포 퀄리티 P1~P15 + 이후 개선, PR #34~#69)
|
||||
|
||||
| 영역 | 내용 |
|
||||
|---|---|
|
||||
| **로비 마을** | 전용 물리 맵 `lobby.map`(마을 배경). **NPC 4종 월드 엔티티** — 모험가(런 시작)·사서(카드 도감)·상인(영혼 상점)·안내원(게시판). 근접 시 머리 위 마크 + `↑`키 **또는 직접 클릭**으로 상호작용. **이동·공격 모션은 로비 맵에서만** 풀림(전투맵은 잠금), 카메라는 로비에서 **플레이어 추종**(전투맵은 고정) |
|
||||
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택(**캐릭터 이미지 카드**, 선택 시 금색 테두리), 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**2차 전직**] — 각 클래스 3종(전사→파이터/페이지/스피어맨, 법사→위자드불독/위자드썬콜/클레릭, 도적→Shiv/Poison/Trickster). 전용 카드는 해당 클래스 풀만 획득 |
|
||||
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬=위로 스윕). 카드 **123장** — kind **Attack/Skill/Power/Status**. 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거)·**소멸(exhaust)**·**가시(thorns)**·**민첩(dex)**·**표창 토큰**(손패에 Shiv 생성) |
|
||||
| **버프/디버프** | StS 표준 — **힘**(+N 영구)·**약화**(주는 피해 −25%)·**취약**(받는 피해 +50%)·**독**(매 행동 틱). 양방향(적 디버프 인텐트 포함), 인텐트는 최종 예상치 표시 |
|
||||
| **전투 연출** | 공격 이펙트·**몬스터 데미지 팝업(자릿수 스킨)**·드래그 타깃 마커·적 개별 차례·**공격/피격/독뎀 모션**(아바타 상태 전이·몬스터 hit 클립·런지/넉백) |
|
||||
| **절차 생성 맵** | 막 시작마다 **경로 생성**(런마다 다름, **가로 진행**). 층 규칙: 1~2층 전투만 → 3층~ 상점/휴식 → 4층~ 엘리트/**유물 방** → 보스 수렴. **노드맵 UI**: 타입별 공식 메이플 아이콘 노드(전투=버섯·엘리트=골렘·보스=발록·상점=돈주머니·휴식=모닥불·보물=상자) + scenic 배경 + 우하단 범례. 노드 타입별 **몬스터 랜덤 구성**(일반 1~3 / 엘리트 / 보스) + intent 랜덤 행동 |
|
||||
| **유물 19종 / 물약 6종** | 유물: StS 효과 × 메이플 장비 외형, TopBar 아이콘 + 마우스오버 툴팁, 8종 훅. 물약: 승리 40% 드랍·상점·슬롯 메뉴. 보물 방=상자 연출 → 유물+메소 |
|
||||
| **카드 프레임·등급** | 커스텀 프레임 3종(전사/마법사/도적 × normal/unique/legend), 카드 5개 사이트 통합 레이아웃. 보상 등급 가중 추첨 70/25/5 |
|
||||
| **영혼(Soul) 메타 성장** | 승천과 별개의 영구 강화 화폐. 2차 전직 상태로 보스 클리어 시 적립 → 로비 영혼 상점 4종 해금(시작 메소 +60·HP +15·덱 정제·시작 유물 +1). **UserDataStorage 영구 저장** |
|
||||
| **승천(Ascension)** | A1~A10 누적 모디파이어(적 강화·시작 HP 감소·보상 감소). UserDataStorage 유저별 영구 저장, 런 클리어 시 다음 단계 해금 |
|
||||
| **멀티 act** | **5막** 진행(보스 클리어→다음 막 텔레포트, 맵·인카운터 변경, 적 스케일 `1+(막-1)*0.45`), 5막 클리어 시 런 종료 |
|
||||
| **경제** | 화폐 표기 **메소**(코인 아이콘), 카드/유물/물약 메소 가격. 내부 식별자는 Gold 유지 |
|
||||
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트 |
|
||||
|
||||
> ⚠️ 수치(적 스탯·경제·승천 배율)는 1차 조정 상태입니다. 정밀 밸런싱은 `sim-balance.mjs`로 검증하며 진행합니다.
|
||||
> ℹ️ 도적(Silent) 카드 88장은 효과·프레임은 적용됐으나 **카드 아이콘(image/fx) 미할당** 상태입니다(전사·마법사 카드는 실 스킬 아이콘 적용 완료).
|
||||
|
||||
### 유용한 스크립트 호출
|
||||
`/common` 엔티티(또는 Play Test 컨텍스트)에서:
|
||||
```lua
|
||||
local c = _EntityService:GetEntityByPath("/common").SlayDeckController
|
||||
-- 로비
|
||||
c:OnLobbyNpcInteract("run") -- 모험가(런 시작) / "codex"(도감) / "shop"(영혼상점) / "board"(게시판)
|
||||
c:ShowLobby() -- 로비 맵 복귀 + 상태 초기화
|
||||
-- 런
|
||||
c:SelectClass("warrior") -- "warrior" / "bandit" / "magician"
|
||||
c:StartNewGame() -- 캐릭터 선택 → 런 시작(map01 텔레포트)
|
||||
c:PickNode("r1c2") -- 맵 노드 선택(절차 생성 그리드 id) / "boss"
|
||||
c:PlayCard(1) -- 손패 slot 카드 사용
|
||||
c:EndPlayerTurn() -- 턴 종료 → 적 턴 → 다음 턴
|
||||
c:PickReward(1) -- 보상 카드 1택(0=건너뛰기)
|
||||
c:BuyCard(1) / c:BuyRelic() / c:BuyPotion() -- 상점 구매(메소)
|
||||
c:SetJob("fighter") -- 전직 (보스 보상 선택 화면)
|
||||
c:AdjustAscension(1) -- 메뉴에서 승천 단계 +1
|
||||
```
|
||||
|
||||
밸런스 검증: `node tools/balance/sim-balance.mjs [N] [--seed S]` · 테스트: `node --test tools/balance/sim-balance.test.mjs tools/map/rogue-map.test.mjs`.
|
||||
상세 설계는 [`docs/slaymaple_basic_framework.md`](docs/slaymaple_basic_framework.md) 참조.
|
||||
|
||||
### 산출물 재생성
|
||||
```bash
|
||||
node tools/deck/gen-slaydeck.mjs # 게임 전체(UI·컨트롤러·common·맵 인카운터)
|
||||
node tools/map/gen-maps.mjs # map01~05 배경/타일
|
||||
node tools/map/gen-lobby-map.mjs # 로비 맵 + NPC 배치
|
||||
node tools/player/gen-lobby-npc.mjs # 로비 codeblock(LobbyNpc·LobbyMobility)
|
||||
node tools/camera/gen-camera.mjs # 맵별 카메라
|
||||
node tools/player/gen-player-lock.mjs # 전투맵 입력 잠금
|
||||
node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
|
||||
```
|
||||
> 산출물 검증은 내용 출력 없이 카운트만: `node tools/verify/count.mjs <ui|cb|common> <regex>...` (자세한 가드는 [`RULES.md`](RULES.md)).
|
||||
|
||||
---
|
||||
|
||||
## 다음 구현 단계
|
||||
- [ ] HP·방어도·에너지·적 의도·손패 5버튼을 렌더링하는 전투 UI
|
||||
- [ ] 전투/엘리트/상점/휴식/이벤트/보스 노드를 가진 맵 노드 UI
|
||||
- [ ] `OnCombatStart` / `OnCardPlayed` / `OnTurnStart` / `OnCombatReward` 훅을 가진 유물 시스템
|
||||
- [ ] 적 행동 패턴을 데이터로 정의 (현재 단순 의도 패턴 → 무브셋)
|
||||
- [ ] 런 루프 완성 후 저장/불러오기
|
||||
## 아키텍처 메모
|
||||
|
||||
현재 게임 전체 로직이 `SlayDeckController` 단일 codeblock에 모여 있습니다. 초기 설계의 3분할(`SlayCardCatalog`/`SlayRunState`/`SlayCombatManager`)은 **기능적으로 모두 구현**됐으나 아직 한 컴포넌트 안에 있습니다. 맵 NPC·카메라·입력 잠금 등 **맵 단위 동작은 별도 codeblock**(LobbyNpc/LobbyMobility/MapCamera/PlayerLock/CombatMonster)으로 분리해 각 맵 루트/엔티티에 부착합니다. 카드/적/맵/유물/프레임/카메라 데이터는 `data/*.json`로 외부화돼 있습니다.
|
||||
|
||||
**생성기 모듈화 (진행 중, PR #70~#72)**: 위 *출력*(단일 `SlayDeckController` 컴포넌트)은 그대로지만 **생성기**는 모듈화됐습니다 — `tools/deck/gen-slaydeck.mjs`(오케스트레이터)가 `lib/`(공유 헬퍼·데이터·codeblock 헬퍼)·`hud/`(화면별 UI emit 15종)·`cb/`(codeblock 메서드 17종)를 조합합니다(출력 바이트동일 순수 리팩터 — `tools/verify/diffcheck.mjs`로 검증, 의존 단방향 orchestrator→{hud,cb}→lib). 특정 화면 UI는 `hud/<name>.mjs`, 특정 메서드는 `cb/<name>.mjs`만 고치면 됩니다. 더해 **캐릭터 선택 화면을 메이커 저작 stock으로 이관**(레이아웃=메이커 시각 편집·재생성에 안 덮임, 이미지·선택은 컨트롤러가 경로로 런타임 주입)하는 **하이브리드 UI 파일럿**이 진행 중입니다(자세한 규칙은 `RULES.md` §1).
|
||||
|
||||
> ⚠️ **전투 규칙과 맵 생성은 Lua(gen-slaydeck 내장)와 JS 미러(sim-balance/rogue-map)로 이중 구현**입니다. 한쪽을 고치면 반드시 다른 쪽도 동기화하고 테스트하세요(`RULES.md` §6).
|
||||
|
||||
---
|
||||
|
||||
## 향후 개선 계획 (후속 후보)
|
||||
- [x] 전투 루프 · 런 루프 · 절차 생성 맵 · 상점/휴식/유물 방 · 유물 19종 · 물약 · 버프/디버프 · Power · 전직(전사/법사/도적 2차) · 승천+개인 저장 · 전투 모션 · 커스텀 프레임 · **반복 런·로비 맵·NPC·영혼·메소·카메라 추종 (P1~P15 완료)**
|
||||
- [x] **노드맵 UI 강화(아이콘 노드+배경+범례) · 캐릭터 선택 이미지 · exhaust/dex/thorns·표창 토큰 카드 (PR #58~#69)**
|
||||
- [~] **생성기 모듈화(`lib`/`hud`/`cb`, 출력 바이트동일) + 캐릭터 선택 메이커 저작 stock 파일럿 (PR #70~#72 — 리뷰·플레이테스트 대기)**
|
||||
- [ ] **도적 카드 아이콘** — Silent 88장에 실 스킬 아이콘(image/fx) 할당, 2차 전직 설명 한글화
|
||||
- [ ] **런 이어하기** — 진행 중 런 직렬화 저장(UserDataStorage 확장, 메뉴 "이어하기" 활성화)
|
||||
- [ ] **카드 제거/업그레이드** — 상점 카드 제거 슬롯, 휴식 노드에서 카드 강화
|
||||
- [ ] **이벤트 노드(?)** — 랜덤 텍스트 이벤트(선택지·리스크/리워드)
|
||||
- [ ] **3차 전직** — 후반 막 보상으로 확장
|
||||
- [ ] **궁수 등 추가 클래스** — 캐릭터 선택 슬롯 확장
|
||||
- [ ] **정밀 밸런싱** — 첫 인카운터 승률 완화·직업별 카드 효율 튜닝(`sim-balance.mjs` 리포트 기반)
|
||||
- [ ] **상점 보장 규칙** — 막당 상점 최소 1회 등장
|
||||
- [ ] **연출 보강** — 사운드(타격·획득), 맵 화면에 유물/물약 표시
|
||||
|
||||
---
|
||||
|
||||
@@ -117,4 +197,5 @@ self.Entity.SlayCombatManager:StartCombat("elite")
|
||||
```
|
||||
2. MSW Maker에서 이 폴더를 **로컬 워크스페이스 경로**로 지정해 월드 열기
|
||||
3. `.mcp.json` / `.codex/` 는 git에 없으므로, 본인 토큰으로 직접 생성 (MCP·Codex 사용 시)
|
||||
4. 작업 전 항상 `git pull`, 작업 후 `git add/commit/push`
|
||||
4. 작업 전 항상 `git pull` + 메이커 reload, 작업 후 `git add/commit/push`
|
||||
5. **AI 에이전트(Claude Code 등)로 작업한다면 [`RULES.md`](RULES.md) 필독** — 생성 산출물 접근 금지(토큰 가드)·검증 절차·PR 도구(`tools/git/gitea-pr.mjs`) 규칙. Claude Code는 `CLAUDE.md`가 자동 적용
|
||||
|
||||
88
RULES.md
Normal file
88
RULES.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# RULES.md — SlayMaple 하네스 엔지니어링 규칙
|
||||
|
||||
AI 에이전트(Claude Code 등)와 협업자가 이 저장소에서 **토큰을 낭비하지 않고 안전하게** 작업하기 위한 공용 규칙.
|
||||
Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된다. 다른 도구(Codex 등)를 쓰면 세션 시작 시 이 파일을 읽혀라.
|
||||
|
||||
---
|
||||
|
||||
## 1. 생성 산출물은 읽지도, 고치지도 않는다 (가장 중요)
|
||||
|
||||
이 저장소의 큰 파일들은 전부 **생성기 산출물**이다. 직접 읽으면 토큰이 증발하고, 직접 고치면 다음 재생성 때 사라진다.
|
||||
|
||||
| 산출물 (절대 Read/Edit 금지) | 크기 | 단일 소스 (여기만 편집) | 재생성 명령 |
|
||||
|---|---|---|---|
|
||||
| `ui/DefaultGroup.ui` | **~7.1MB** | `data/*.json` + `tools/deck/`(`gen-slaydeck.mjs`+`lib/`+`hud/`) | `node tools/deck/gen-slaydeck.mjs` |
|
||||
| `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | 〃 | 〃 |
|
||||
| `Global/common.gamelogic` | ~1KB | 〃 | 〃 |
|
||||
| `map/map01.map`~`map05.map`, `map/lobby.map` | 각 ~210KB | `tools/map/`·`tools/monster/`·`tools/camera/`·`tools/player/` (↓ 보조 생성기) | 해당 생성기 |
|
||||
| `RootDesk/MyDesk/CombatMonster.codeblock` | ~2KB | `tools/monster/gen-combat-monster.mjs` | `node tools/monster/gen-combat-monster.mjs` |
|
||||
| `RootDesk/MyDesk/PlayerLock.codeblock` | ~2KB | `tools/player/gen-player-lock.mjs` | `node tools/player/gen-player-lock.mjs` |
|
||||
| `RootDesk/MyDesk/MapCamera.codeblock` | ~2KB | `tools/camera/gen-camera.mjs` (값: `data/camera.json`) | `node tools/camera/gen-camera.mjs` |
|
||||
| `RootDesk/MyDesk/LobbyNpc.codeblock`·`LobbyMobility.codeblock` | 각 ~2-3KB | `tools/player/gen-lobby-npc.mjs` | `node tools/player/gen-lobby-npc.mjs` |
|
||||
| `Global/SectorConfig.config` | ~1KB | `tools/map/gen-maps.mjs`·`gen-lobby-map.mjs` (패치) | 해당 생성기 |
|
||||
|
||||
- `.claude/settings.json`의 permissions.deny가 위 파일의 Read/Edit/Write 도구 사용을 차단한다 (이 저장소를 열면 자동 적용). deny는 **glob** — `ui/*.ui`·`map/*.map`·`RootDesk/MyDesk/*.codeblock`·`Global/common.gamelogic`·`Global/SectorConfig.config`. 따라서 **메이커 저작 codeblock/UI**(`Monster`·`MonsterAttack`·`PlayerAttack`·`PlayerHit`·`UIPopup`·`UIToast`.codeblock, `ui/PopupGroup.ui`·`ui/ToastGroup.ui`)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발.
|
||||
- 게임 로직·UI 수정 = **`tools/deck/gen-slaydeck.mjs`(오케스트레이터 + codeblock Lua) 또는 `data/*.json`(데이터)을 수정** → 재생성 → 산출물은 통째로 커밋.
|
||||
- **UI emit은 HUD별 모듈** `tools/deck/hud/*.mjs`(shop·combat·map·deckall·soulshop 등 15종 — **charselect는 제외: Phase 2에서 메이커 저작 stock으로 이관**, 아래), **codeblock 메서드(Lua)는 기능별 모듈** `tools/deck/cb/*.mjs`(boot·state·combat·hand·deckview·items·map·shop 등 17종, 메서드 161개를 연속런으로). **공유분**: UI 헬퍼·상수·데이터·lua 테이블 = `tools/deck/lib/ui-helpers.mjs`·`tools/deck/lib/data.mjs`, method/prop/codeblock 헬퍼·writeCodeblocks 상수 = `tools/deck/lib/codeblock.mjs`. 특정 화면 UI 수정은 `hud/<name>.mjs`, 특정 메서드 수정은 `cb/<name>.mjs`만(의존: orchestrator→{hud,cb}→lib 단방향). prop 103개는 오케스트레이터 writeCodeblocks에 유지. **cb 모듈은 원본 메서드 순서 보존이 바이트동일 조건** — 새 메서드는 해당 기능 모듈의 알맞은 위치에 추가하고 writeCodeblocks의 spread 순서 유지.
|
||||
- 리팩터 시 **출력 바이트-동일 검증**: `node tools/deck/gen-slaydeck.mjs` 후 `node tools/verify/diffcheck.mjs [ref]`(워킹트리 vs ref(기본 HEAD) 줄바꿈 정규화 비교 — 산출물 경로를 명령줄에 노출 안 해 deny 회피). 산출물 ` M`은 보통 autocrlf churn이니 `git checkout --`로 복원.
|
||||
- **하이브리드(메이커 저작) 화면 — charselect 파일럿(Phase 2)**: `CharacterSelectHud`는 `GENERATED_UI_SECTIONS`에서 빠져 **생성기가 안 만들고 안 덮음** = `ui/DefaultGroup.ui`의 charselect 엔티티는 **메이커에서 시각 편집하는 stock**. 컨트롤러(`cb/charselect.mjs`)가 경로(`CharacterSelectHud/{Warrior,Thief,Mage}Button/Art` 등 — 메이커가 이 경로 유지 필수)로 이미지(`self.ClassPortraits`, `data/characters.json` 시드)·선택테두리·상태를 **런타임 주입**. 즉 레이아웃=메이커, 내용=컨트롤러. charselect 레이아웃 수정은 `hud/`가 아니라 **메이커에서**.
|
||||
- **머지 충돌(gen-slaydeck.mjs)**: 다른 브랜치가 단일체를 수정해 충돌나면, 그쪽 버전(`git checkout --theirs tools/deck/gen-slaydeck.mjs`)을 취해 **콘텐츠 마커 기반으로 재모듈화**(라인인덱스 X — 줄 추가에 안전·export 이름 자동 파생·`const x=[]` 직전 전문 상수 walk-back 포함) 후 `node tools/verify/diffcheck.mjs origin/main`으로 ui·codeblock 바이트-동일 확인(손실 0 증명). codeblock 메서드·patchCommon은 오케스트레이터 잔류라 그쪽 변경은 자동 보존됨.
|
||||
- **보조 생성기**(각자 자기 산출물의 단일 소스 — 위 표의 메인 `gen-slaydeck.mjs` 외):
|
||||
- `tools/camera/gen-camera.mjs` → `MapCamera.codeblock` + map01~05 카메라 부착 (값 `data/camera.json`)
|
||||
- `tools/map/gen-maps.mjs` → `map02~05` + `Global/SectorConfig.config` (map01 템플릿 클론)
|
||||
- `tools/map/gen-lobby-map.mjs` → `map/lobby.map` + `SectorConfig.config`
|
||||
- `tools/map/gen-map-encounters.mjs` → map01~05 노드 타입별 몬스터 그룹 재구성
|
||||
- `tools/monster/gen-combat-monster.mjs` → `CombatMonster.codeblock` + map01~05 부착
|
||||
- `tools/monster/freeze-turn-monsters.mjs` → 몬스터 `.model`·맵 AI 컴포넌트 제거
|
||||
- `tools/player/gen-player-lock.mjs` → `PlayerLock.codeblock` + map01~05 부착
|
||||
- `tools/player/gen-lobby-npc.mjs` → `LobbyNpc.codeblock`·`LobbyMobility.codeblock`
|
||||
- `tools/player/freeze-turn-player.mjs` → `Global/DefaultPlayer.model` 이동 0 고정
|
||||
- `tools/deck/gen-cardhand.mjs` → `DefaultGroup.ui` 카드핸드 보조 패처
|
||||
|
||||
## 2. 산출물 검증은 카운트로, 내용 출력 금지
|
||||
|
||||
재생성 결과 확인이 필요하면 **본문을 출력하지 말고** 존재/개수만 확인한다:
|
||||
|
||||
```bash
|
||||
node tools/deck/gen-slaydeck.mjs # 성공 메시지 1줄
|
||||
grep -c "TreasureHud" ui/DefaultGroup.ui # 개수만
|
||||
grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
```
|
||||
|
||||
- ⚠️ codeblock은 **한 줄이 수만 자**(JSON 직렬화)다. `grep`(내용 출력)·`sed -n` 광역 범위 출력은 한 줄만 걸려도 토큰 폭발 → `grep -c`/`grep -l`/`grep -o '짧은패턴'`만 사용.
|
||||
- Claude Code의 Grep 도구를 산출물에 쓸 때는 `output_mode: count` 또는 `files_with_matches`만. content 모드 금지.
|
||||
- 진짜 내용 확인이 필요하면 좁은 `grep -o` 또는 `python`으로 슬라이스해서 **수 줄 이내**로.
|
||||
|
||||
## 3. 탐색 규칙
|
||||
|
||||
- 코드 탐색은 `tools/`·`data/`·`docs/`만 대상으로. 저장소 전체 grep은 산출물이 걸리므로 경로를 지정한다.
|
||||
- `git add -A` 전 `git status --short`로 산출물 외 의도치 않은 변경이 없는지 확인 (산출물 diff는 보지 않는다 — 생성기가 결정적이므로 소스 리뷰로 충분).
|
||||
- 게임 동작 확인은 메이커 플레이테스트(스크린샷·로그)로 한다. 산출물 정독으로 동작을 추론하지 않는다.
|
||||
|
||||
## 4. Git/PR 절차
|
||||
|
||||
- 브랜치 → 커밋(기능 단위) → push → **PR은 반드시 `node tools/git/gitea-pr.mjs`로** (인라인 `curl -d` 한글 본문은 Windows에서 CP949로 깨짐 — PR #34~41 사고).
|
||||
- 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create <spec.json>` / `merge <번호>`.
|
||||
- PR 제목과 본문은 한국어로 작성한다.
|
||||
- 산출물 재생성 커밋은 소스 변경 커밋과 분리하거나, 메시지에 "산출물 재생성"을 명시.
|
||||
|
||||
## 5. 메이커(MSW) 연동 주의
|
||||
|
||||
- git pull 후 메이커에서 **로컬 워크스페이스 reload** 필수 (안 하면 메이커의 stale 상태가 디스크를 덮어씀).
|
||||
- 재생성 후 메이커가 켜져 있으면 refresh → 빌드 콘솔 0 에러 확인.
|
||||
- 카드/유물/물약 이미지는 **공식 maplestory 리소스 RUID**만 (계정 업로드 리소스는 로컬 워크스페이스에서 흰 박스).
|
||||
|
||||
## 6. 밸런스·맵 규칙 동기화
|
||||
|
||||
전투 규칙과 맵 생성은 Lua(생성기 내장)와 JS가 **이중 구현**이다. 한쪽을 고치면 반드시 다른 쪽도:
|
||||
|
||||
| 영역 | Lua (gen-slaydeck.mjs 내) | JS 미러 | 테스트 |
|
||||
|---|---|---|---|
|
||||
| 전투 규칙 | PlayCard·CalcPlayerAttack 등 | `tools/balance/sim-balance.mjs` | `node --test tools/balance/sim-balance.test.mjs` |
|
||||
| 맵 생성 | GenerateMap | `tools/map/rogue-map.mjs` | `node --test tools/map/rogue-map.test.mjs` |
|
||||
|
||||
## 7. UI 숫자 표기
|
||||
|
||||
- UI 텍스트에서는 정수값인 숫자에 `.0`을 붙이지 않는다. `1.0/1.0`이 아니라 `1/1`처럼 표시한다.
|
||||
- 생성기 내 Lua UI 코드에서 number 또는 숫자 문자열을 텍스트에 붙일 때는 `FormatNumber` 같은 포맷 헬퍼를 우선 사용한다.
|
||||
- 소수부가 플레이어에게 의미 있을 때만 소수점 표기를 유지한다.
|
||||
74
RootDesk/MyDesk/CombatMonster.codeblock
Normal file
74
RootDesk/MyDesk/CombatMonster.codeblock
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "codeblock://combatmonster",
|
||||
"ContentType": "x-mod/codeblock",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"CoreVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2
|
||||
},
|
||||
"ScriptVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0
|
||||
},
|
||||
"Description": "",
|
||||
"Id": "CombatMonster",
|
||||
"Language": 1,
|
||||
"Name": "CombatMonster",
|
||||
"Type": 1,
|
||||
"Source": 0,
|
||||
"Target": null,
|
||||
"Properties": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyId"
|
||||
},
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"combat\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "Group"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RegTries"
|
||||
}
|
||||
],
|
||||
"Methods": [
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.RegTries = 0\nlocal eventId = 0\nlocal function reg()\n\tself.RegTries = self.RegTries + 1\n\tlocal c = _EntityService:GetEntityByPath(\"/common\")\n\tif c ~= nil and c.SlayDeckController ~= nil then\n\t\tlocal mapName = \"\"\n\t\tif self.Entity.CurrentMapName ~= nil then\n\t\t\tmapName = self.Entity.CurrentMapName\n\t\tend\n\t\tc.SlayDeckController:RegisterMonster(self.Entity, self.EnemyId, self.Group, mapName)\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.RegTries > 50 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(reg, 0.1)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "OnBeginPlay"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
60
RootDesk/MyDesk/LobbyMobility.codeblock
Normal file
60
RootDesk/MyDesk/LobbyMobility.codeblock
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "codeblock://lobbymobility",
|
||||
"ContentType": "x-mod/codeblock",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"CoreVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2
|
||||
},
|
||||
"ScriptVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0
|
||||
},
|
||||
"Description": "",
|
||||
"Id": "LobbyMobility",
|
||||
"Language": 1,
|
||||
"Name": "LobbyMobility",
|
||||
"Type": 1,
|
||||
"Source": 0,
|
||||
"Target": null,
|
||||
"Properties": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "Tries"
|
||||
}
|
||||
],
|
||||
"Methods": [
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.Tries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.Tries = self.Tries + 1\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil and lp.PlayerControllerComponent ~= nil then\n\t\tlocal pc = lp.PlayerControllerComponent\n\t\tpc.Enable = true\n\t\tpc.FixedLookAt = 0\n\t\tlocal rb = lp.RigidbodyComponent\n\t\tif rb ~= nil then rb.WalkAcceleration = 0.7 end\n\t\tlocal mv = lp.MovementComponent\n\t\tif mv ~= nil then\n\t\t\tmv.InputSpeed = 1.4\n\t\t\tmv.JumpForce = 1.23\n\t\tend\n\t\tlocal cam = lp.CameraComponent\n\t\tif cam == nil then cam = _CameraService:GetCurrentCameraComponent() end\n\t\tif cam ~= nil then\n\t\t\tcam.ZoomRatio = 90\n\t\t\tcam.ConfineCameraArea = false\n\t\t\tcam.ScreenOffset = Vector2(0.5, 0.5)\n\t\t\tcam.CameraOffset = Vector2(0, 0)\n\t\tend\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.Tries > 50 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "OnBeginPlay"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
89
RootDesk/MyDesk/LobbyNpc.codeblock
Normal file
89
RootDesk/MyDesk/LobbyNpc.codeblock
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "codeblock://lobbynpc",
|
||||
"ContentType": "x-mod/codeblock",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"CoreVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2
|
||||
},
|
||||
"ScriptVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0
|
||||
},
|
||||
"Description": "",
|
||||
"Id": "LobbyNpc",
|
||||
"Language": 1,
|
||||
"Name": "LobbyNpc",
|
||||
"Type": 1,
|
||||
"Source": 0,
|
||||
"Target": null,
|
||||
"Properties": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "NpcId"
|
||||
},
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "MarkName"
|
||||
},
|
||||
{
|
||||
"Type": "boolean",
|
||||
"DefaultValue": "false",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "InRange"
|
||||
}
|
||||
],
|
||||
"Methods": [
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.InRange = false\nlocal mark = _EntityService:GetEntityByPath(\"/maps/lobby/\" .. self.MarkName)\nif mark ~= nil then mark:SetVisible(false) end\nself.Entity:ConnectEvent(TouchEvent, function(e)\n\tself:Interact()\nend)\n_InputService:ConnectEvent(KeyDownEvent, function(e)\n\tif self.InRange and e.key == KeyboardKey.UpArrow then\n\t\tself:Interact()\n\tend\nend)\nlocal eventId = 0\nlocal function tick()\n\tlocal lp = _UserService.LocalPlayer\n\tif lp == nil then return end\n\tif mark == nil then mark = _EntityService:GetEntityByPath(\"/maps/lobby/\" .. self.MarkName) end\n\tlocal a = lp.TransformComponent.WorldPosition\n\tlocal b = self.Entity.TransformComponent.WorldPosition\n\tlocal d = Vector2.Distance(Vector2(a.x, a.y), Vector2(b.x, b.y))\n\tlocal near = d < 1.2\n\tif near ~= self.InRange then\n\t\tself.InRange = near\n\t\tif mark ~= nil then mark:SetVisible(near) end\n\tend\nend\neventId = _TimerService:SetTimerRepeat(tick, 0.15)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "OnBeginPlay"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local c = _EntityService:GetEntityByPath(\"/common\")\nif c ~= nil and c.SlayDeckController ~= nil then\n\tc.SlayDeckController:OnLobbyNpcInteract(self.NpcId)\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "Interact"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
60
RootDesk/MyDesk/MapCamera.codeblock
Normal file
60
RootDesk/MyDesk/MapCamera.codeblock
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "codeblock://mapcamera",
|
||||
"ContentType": "x-mod/codeblock",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"CoreVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2
|
||||
},
|
||||
"ScriptVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0
|
||||
},
|
||||
"Description": "",
|
||||
"Id": "MapCamera",
|
||||
"Language": 1,
|
||||
"Name": "MapCamera",
|
||||
"Type": 1,
|
||||
"Source": 0,
|
||||
"Target": null,
|
||||
"Properties": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "CamTries"
|
||||
}
|
||||
],
|
||||
"Methods": [
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.CamTries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.CamTries = self.CamTries + 1\n\tlocal cam = nil\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil then\n\t\tcam = lp.CameraComponent\n\tend\n\tif cam == nil then\n\t\tcam = _CameraService:GetCurrentCameraComponent()\n\tend\n\tif cam ~= nil then\n\t\tcam.ZoomRatio = 90\n\t\tcam.ScreenOffset = Vector2(0.5, 0.655)\n\t\tcam.ConfineCameraArea = true\n\t\tcam.CameraOffset = Vector2(1.5, -0.83)\n\tend\n\tif cam ~= nil then\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.CamTries > 30 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "OnBeginPlay"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.AIWanderComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
@@ -57,10 +56,10 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODQuaternion, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"w": 1.0
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -186,8 +185,8 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -199,8 +198,8 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -219,7 +218,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 1.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
@@ -228,7 +227,7 @@
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0.0
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
@@ -239,24 +238,6 @@
|
||||
},
|
||||
"Value": true
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.AIWanderComponent",
|
||||
"Name": "IsLegacy",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": false
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.AIWanderComponent",
|
||||
"Name": "Enable",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": true
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateComponent",
|
||||
"Name": "IsLegacy",
|
||||
@@ -376,8 +357,8 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -389,8 +370,8 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -402,8 +383,8 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -424,8 +405,8 @@
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -460,4 +441,4 @@
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local monster = self.Entity.Monster\nif not monster then\n\treturn\nend\n\nself.Shape = BoxShape(Vector2.zero, Vector2.one, 0)\n\n-- sprite 사이즈를 가져와 공격 영역으로 사용한다\n_ResourceService:PreloadAsync({self.Entity.SpriteRendererComponent.SpriteRUID}, function()\n\tlocal clip = _ResourceService:LoadAnimationClipAndWait(self.Entity.SpriteRendererComponent.SpriteRUID)\n\tlocal firstFrameSprite = clip.Frames[1].FrameSprite\n\tlocal firstSpriteSizeInPixel = Vector2(firstFrameSprite.Width, firstFrameSprite.Height)\n\tlocal ppu = firstFrameSprite.PixelPerUnit\n\n\tself.SpriteSize = firstSpriteSizeInPixel / ppu\n\tself.PositionOffset = (firstSpriteSizeInPixel / 2 - firstFrameSprite.PivotPixel:ToVector2()) / ppu\n\t\n\t_TimerService:SetTimerRepeat(function() \n\t\tif monster.IsDead == false then\n\t\t\tself:AttackNear()\n\t\tend\n\tend, self.AttackInterval)\nend)",
|
||||
"Code": "local monster = self.Entity.Monster\nif not monster then\n\treturn\nend\n\nself.Shape = BoxShape(Vector2.zero, Vector2.one, 0)\n\n-- sprite 사이즈를 가져와 공격 영역으로 사용한다\n_ResourceService:PreloadAsync({self.Entity.SpriteRendererComponent.SpriteRUID}, function()\n\tif _ResourceService:GetTypeAndWait(self.Entity.SpriteRendererComponent.SpriteRUID) ~= ResourceType.AnimationClip then\n\t\treturn\n\tend\n\tlocal clip = _ResourceService:LoadAnimationClipAndWait(self.Entity.SpriteRendererComponent.SpriteRUID)\n\tif clip == nil then\n\t\treturn\n\tend\n\tlocal firstFrameSprite = clip.Frames[1].FrameSprite\n\tlocal firstSpriteSizeInPixel = Vector2(firstFrameSprite.Width, firstFrameSprite.Height)\n\tlocal ppu = firstFrameSprite.PixelPerUnit\n\n\tself.SpriteSize = firstSpriteSizeInPixel / ppu\n\tself.PositionOffset = (firstSpriteSizeInPixel / 2 - firstFrameSprite.PivotPixel:ToVector2()) / ppu\n\t\n\t_TimerService:SetTimerRepeat(function() \n\t\tif monster.IsDead == false then\n\t\t\tself:AttackNear()\n\t\tend\n\tend, self.AttackInterval)\nend)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 1,
|
||||
"Attributes": [],
|
||||
|
||||
60
RootDesk/MyDesk/PlayerLock.codeblock
Normal file
60
RootDesk/MyDesk/PlayerLock.codeblock
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "codeblock://playerlock",
|
||||
"ContentType": "x-mod/codeblock",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"CoreVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2
|
||||
},
|
||||
"ScriptVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0
|
||||
},
|
||||
"Description": "",
|
||||
"Id": "PlayerLock",
|
||||
"Language": 1,
|
||||
"Name": "PlayerLock",
|
||||
"Type": 1,
|
||||
"Source": 0,
|
||||
"Target": null,
|
||||
"Properties": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "LockTries"
|
||||
}
|
||||
],
|
||||
"Methods": [
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.LockTries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.LockTries = self.LockTries + 1\n\tlocal pc = nil\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil then\n\t\tpc = lp.PlayerControllerComponent\n\tend\n\tif pc ~= nil then\n\t\tpc.LookDirectionX = 1\n\t\tpc.FixedLookAt = true\n\t\tpc.Enable = false\n\tend\n\tif lp ~= nil then\n\t\tif lp.RigidbodyComponent ~= nil then lp.RigidbodyComponent.WalkAcceleration = 0 end\n\t\tif lp.MovementComponent ~= nil then lp.MovementComponent.InputSpeed = 0; lp.MovementComponent.JumpForce = 0 end\n\tend\n\tif pc ~= nil then\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.LockTries > 30 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "OnBeginPlay"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
23
RootDesk/MyDesk/archmage(fire_poison).sprite
Normal file
23
RootDesk/MyDesk/archmage(fire_poison).sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://2d659478140c4b1c8f37febbb61bdaa0",
|
||||
"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/2d659478140c4b1c8f37febbb61bdaa0/639171361295360405",
|
||||
"upload_hash": "13E6D3B629261148095059F7C1D8EDC012C7A60422FC769ECB574A7C5A75759E",
|
||||
"name": "archmage(fire_poison)",
|
||||
"resource_guid": "2d659478140c4b1c8f37febbb61bdaa0",
|
||||
"resource_version": "6a3022013e53f03801a4ac58"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/archmage(thun_cold).sprite
Normal file
23
RootDesk/MyDesk/archmage(thun_cold).sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://ddd0a729328f452b9ff0802ab1f6f579",
|
||||
"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/ddd0a729328f452b9ff0802ab1f6f579/639171361295458274",
|
||||
"upload_hash": "7B874B7774FA37E570B16E369112EC467EEA5EC1395A32E4DF01FB6165062E67",
|
||||
"name": "archmage(thun_cold)",
|
||||
"resource_guid": "ddd0a729328f452b9ff0802ab1f6f579",
|
||||
"resource_version": "6a3022012c6a274be88a0819"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/bandit.sprite
Normal file
23
RootDesk/MyDesk/bandit.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://efa920e58d31426486ef974106e7dc8b",
|
||||
"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/efa920e58d31426486ef974106e7dc8b/639171361295547945",
|
||||
"upload_hash": "C4B0469A46B70C2356DC8B0F99D36FD9480BFDA5832E251960DD1FF30C201B7F",
|
||||
"name": "bandit",
|
||||
"resource_guid": "efa920e58d31426486ef974106e7dc8b",
|
||||
"resource_version": "6a302201a81bed5f59770e23"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/bandit_legend.sprite
Normal file
23
RootDesk/MyDesk/bandit_legend.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://c357d2daf31a489d95b8fa47e50dd879",
|
||||
"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/c357d2daf31a489d95b8fa47e50dd879/639168711224334184",
|
||||
"upload_hash": "A1639991C7A8CB1025C97E6BE93F618088971DC609F03106C50D8B6EA145F6A2",
|
||||
"name": "bandit_legend",
|
||||
"resource_guid": "c357d2daf31a489d95b8fa47e50dd879",
|
||||
"resource_version": "6a2c16d2cc7e89479f128ffb"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/bandit_normal.sprite
Normal file
23
RootDesk/MyDesk/bandit_normal.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://9487b06867bc46269ed1d855420f457f",
|
||||
"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/9487b06867bc46269ed1d855420f457f/639168711224307886",
|
||||
"upload_hash": "FD9F2140D16EFC7A77F0B09CA230702CA6CA25E3178AAF6ACD63EF07D5C2C83E",
|
||||
"name": "bandit_normal",
|
||||
"resource_guid": "9487b06867bc46269ed1d855420f457f",
|
||||
"resource_version": "6a2c16d23d5de2eb0c7d16a2"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/bandit_unique.sprite
Normal file
23
RootDesk/MyDesk/bandit_unique.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://b3081fb2fb1445fa90b12b01481a78ef",
|
||||
"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/b3081fb2fb1445fa90b12b01481a78ef/639168711224396185",
|
||||
"upload_hash": "1F5218270148F873D060223A617DFC184AEEE61344D26C85705E40EECC086D5E",
|
||||
"name": "bandit_unique",
|
||||
"resource_guid": "b3081fb2fb1445fa90b12b01481a78ef",
|
||||
"resource_version": "6a2c16d21a7908d59b5dc059"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/bowmaster.sprite
Normal file
23
RootDesk/MyDesk/bowmaster.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://93e615d645e948f5a76656bfdd9dce15",
|
||||
"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/93e615d645e948f5a76656bfdd9dce15/639171361295501823",
|
||||
"upload_hash": "A5A1263A9E1F5D58B0F985AC58DE7DDE1EA13E0CC5CEE56FC34C3FD792A8D39E",
|
||||
"name": "bowmaster",
|
||||
"resource_guid": "93e615d645e948f5a76656bfdd9dce15",
|
||||
"resource_version": "6a302201a0766b148f66ec2c"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/cleric.sprite
Normal file
23
RootDesk/MyDesk/cleric.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://3c752ffcd4984dcb9f04baab06544f02",
|
||||
"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/3c752ffcd4984dcb9f04baab06544f02/639171361295496567",
|
||||
"upload_hash": "ED1ABC3DBCB04FCBD365BAD0408C8CA1D2458FB337DBDEB4A7EC5DF1BAC32496",
|
||||
"name": "cleric",
|
||||
"resource_guid": "3c752ffcd4984dcb9f04baab06544f02",
|
||||
"resource_version": "6a302201d03493c632770e41"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/darkknight.sprite
Normal file
23
RootDesk/MyDesk/darkknight.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://e207e6839a4a4bd0aab681bd296a609a",
|
||||
"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/e207e6839a4a4bd0aab681bd296a609a/639171361295401732",
|
||||
"upload_hash": "D5931943781C46611D43595594234BFB133F8BCCAA920C13BCA7E2F8AC9C09D0",
|
||||
"name": "darkknight",
|
||||
"resource_guid": "e207e6839a4a4bd0aab681bd296a609a",
|
||||
"resource_version": "6a302201644d4c175c75d435"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/hero.sprite
Normal file
23
RootDesk/MyDesk/hero.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://0efbf37bb7414aea82b257781068372b",
|
||||
"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/0efbf37bb7414aea82b257781068372b/639171361295431877",
|
||||
"upload_hash": "DE22318162E1F93E7B90A0B6A59BE31FC76DEEC122914979915D6D575A4D99B5",
|
||||
"name": "hero",
|
||||
"resource_guid": "0efbf37bb7414aea82b257781068372b",
|
||||
"resource_version": "6a302201c377d9630d82c463"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/hunter.sprite
Normal file
23
RootDesk/MyDesk/hunter.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://fd460e6ee38a40e3b6b05580d00773b6",
|
||||
"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/fd460e6ee38a40e3b6b05580d00773b6/639171361295408991",
|
||||
"upload_hash": "B832DE85217EA6544BA4477F0DBA5D2148E4E392A170944336E0DA74E8D41961",
|
||||
"name": "hunter",
|
||||
"resource_guid": "fd460e6ee38a40e3b6b05580d00773b6",
|
||||
"resource_version": "6a3022013d5de2eb0c7d2a51"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/mage.sprite
Normal file
23
RootDesk/MyDesk/mage.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://3b9ea1f066a744bb859df47fef817277",
|
||||
"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/3b9ea1f066a744bb859df47fef817277/639171361295599801",
|
||||
"upload_hash": "770C9D83241F8CE2C0EA8B742887D8351A580E2DE15A6380380653855A974F14",
|
||||
"name": "mage",
|
||||
"resource_guid": "3b9ea1f066a744bb859df47fef817277",
|
||||
"resource_version": "6a302201cc7e89479f12a1ac"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/mage_legend.sprite
Normal file
23
RootDesk/MyDesk/mage_legend.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://cff71f2e472041ce80c6fbd296f42e2d",
|
||||
"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/cff71f2e472041ce80c6fbd296f42e2d/639168711224422293",
|
||||
"upload_hash": "040CA63C5D3D52E1661F3A008D229A182B1A7C09D919BB74E23D1305B1AA56A7",
|
||||
"name": "mage_legend",
|
||||
"resource_guid": "cff71f2e472041ce80c6fbd296f42e2d",
|
||||
"resource_version": "6a2c16d2a0766b148f66d799"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/mage_normal.sprite
Normal file
23
RootDesk/MyDesk/mage_normal.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://d788d09f6f50467ebc67f01dec45f9e2",
|
||||
"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/d788d09f6f50467ebc67f01dec45f9e2/639168711224258598",
|
||||
"upload_hash": "5D91E6E333564F15A76554AE07402DC0ED39ABC02439F65C2CCB818C71FB6994",
|
||||
"name": "mage_normal",
|
||||
"resource_guid": "d788d09f6f50467ebc67f01dec45f9e2",
|
||||
"resource_version": "6a2c16d2e75b0d4ccdfcee8b"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/mage_unique.sprite
Normal file
23
RootDesk/MyDesk/mage_unique.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://f5def2e8022b4e59a17d3c16414034fe",
|
||||
"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/f5def2e8022b4e59a17d3c16414034fe/639168711224271729",
|
||||
"upload_hash": "B07075BE5F13B1D7BFB4AE36F4C47D97A10A3CC7EF763F02DD2A11C2AFC61E67",
|
||||
"name": "mage_unique",
|
||||
"resource_guid": "f5def2e8022b4e59a17d3c16414034fe",
|
||||
"resource_version": "6a2c16d2e75b0d4ccdfcee8a"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/nightlord.sprite
Normal file
23
RootDesk/MyDesk/nightlord.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://994c64290e6d4248bd60aba03a595f72",
|
||||
"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/994c64290e6d4248bd60aba03a595f72/639171361295422460",
|
||||
"upload_hash": "A0B7146F6D8E9D72CEACDB7E21A2CD6EEBD4135554D3965B4E1C06BC98FC1211",
|
||||
"name": "nightlord",
|
||||
"resource_guid": "994c64290e6d4248bd60aba03a595f72",
|
||||
"resource_version": "6a30220139613d284615a1e1"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/palladin.sprite
Normal file
23
RootDesk/MyDesk/palladin.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://415d423954764b659574fe829f9aff52",
|
||||
"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/415d423954764b659574fe829f9aff52/639171361329450040",
|
||||
"upload_hash": "EB94BA457C18E04D87964201DD50042695A3C7F93651CBC8ED9509BDDE07F331",
|
||||
"name": "palladin",
|
||||
"resource_guid": "415d423954764b659574fe829f9aff52",
|
||||
"resource_version": "6a302205a0766b148f66ec2d"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/pirate.sprite
Normal file
23
RootDesk/MyDesk/pirate.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://94d6417c55da48e9861964c405991219",
|
||||
"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/94d6417c55da48e9861964c405991219/639171361329410075",
|
||||
"upload_hash": "EBEC4B43CDBB1759C0BC92C051252A7DF84E6B89C9C56ECBFEFF543A0E260889",
|
||||
"name": "pirate",
|
||||
"resource_guid": "94d6417c55da48e9861964c405991219",
|
||||
"resource_version": "6a3022052c6a274be88a081a"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/shadower.sprite
Normal file
23
RootDesk/MyDesk/shadower.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://06c1060586e3457f897f2c596eb5cd71",
|
||||
"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/06c1060586e3457f897f2c596eb5cd71/639171361329387468",
|
||||
"upload_hash": "42EE2E63508762E86391486107A23322E11038070F48851D03FE91255CF41596",
|
||||
"name": "shadower",
|
||||
"resource_guid": "06c1060586e3457f897f2c596eb5cd71",
|
||||
"resource_version": "6a302205a81bed5f59770e24"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/singung.sprite
Normal file
23
RootDesk/MyDesk/singung.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://b2e099f2e5334705af122e3f88840ba7",
|
||||
"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/b2e099f2e5334705af122e3f88840ba7/639171361329408374",
|
||||
"upload_hash": "2AB66279D07E64A17CD2AD05BB03F732632805B0076278EC94F51C0227341CC5",
|
||||
"name": "singung",
|
||||
"resource_guid": "b2e099f2e5334705af122e3f88840ba7",
|
||||
"resource_version": "6a302204c377d9630d82c464"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/warior_legend.sprite
Normal file
23
RootDesk/MyDesk/warior_legend.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://6d741a60c60743cb98ee740a1e2dbfed",
|
||||
"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/6d741a60c60743cb98ee740a1e2dbfed/639168711258121433",
|
||||
"upload_hash": "80E2D8FFC8E56ECE4694BED5A387413F49633841DD37B7AA9FA9AC713962602C",
|
||||
"name": "warior_legend",
|
||||
"resource_guid": "6d741a60c60743cb98ee740a1e2dbfed",
|
||||
"resource_version": "6a2c16d59c3c6c308bfd4bf8"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/warior_normal.sprite
Normal file
23
RootDesk/MyDesk/warior_normal.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://4bb57ef88ef449fdaf958f6cf37fe44b",
|
||||
"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/4bb57ef88ef449fdaf958f6cf37fe44b/639168711258172386",
|
||||
"upload_hash": "2832F3B6C4C9530DE69DF061E590FD314D8646BE6BDB65931AFBE68D38DBB0ED",
|
||||
"name": "warior_normal",
|
||||
"resource_guid": "4bb57ef88ef449fdaf958f6cf37fe44b",
|
||||
"resource_version": "6a2c16d53d5de2eb0c7d16a3"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/warior_unique.sprite
Normal file
23
RootDesk/MyDesk/warior_unique.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://4f71c124c8bc4e13b5e9fad392995f68",
|
||||
"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/4f71c124c8bc4e13b5e9fad392995f68/639168711257524633",
|
||||
"upload_hash": "F27B2D713455FB18C10E924D381C4582E5E039D5DCCA9CECD2FB0D8E9A4C8135",
|
||||
"name": "warior_unique",
|
||||
"resource_guid": "4f71c124c8bc4e13b5e9fad392995f68",
|
||||
"resource_version": "6a2c16d5a43134e1b8a34b65"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/warrior.sprite
Normal file
23
RootDesk/MyDesk/warrior.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://28c88fdc5ab44f34a8b3fc1e19d4ce78",
|
||||
"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/28c88fdc5ab44f34a8b3fc1e19d4ce78/639171361330139251",
|
||||
"upload_hash": "2929F452FBB26215631886FFB430EE6035D55EB42B1770E880C1B0A34D97BDA0",
|
||||
"name": "warrior",
|
||||
"resource_guid": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
|
||||
"resource_version": "6a30220539613d284615a1e2"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
data/camera.json
Normal file
8
data/camera.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"zoomRatio": 90,
|
||||
"screenOffsetX": 0.5,
|
||||
"screenOffsetY": 0.655,
|
||||
"confineCameraArea": true,
|
||||
"cameraOffsetX": 1.5,
|
||||
"cameraOffsetY": -0.83
|
||||
}
|
||||
39
data/cardframes.json
Normal file
39
data/cardframes.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"frames": {
|
||||
"warrior": {
|
||||
"normal": "4bb57ef88ef449fdaf958f6cf37fe44b",
|
||||
"unique": "4f71c124c8bc4e13b5e9fad392995f68",
|
||||
"legend": "6d741a60c60743cb98ee740a1e2dbfed"
|
||||
},
|
||||
"magician": {
|
||||
"normal": "d788d09f6f50467ebc67f01dec45f9e2",
|
||||
"unique": "f5def2e8022b4e59a17d3c16414034fe",
|
||||
"legend": "cff71f2e472041ce80c6fbd296f42e2d"
|
||||
},
|
||||
"bandit": {
|
||||
"normal": "9487b06867bc46269ed1d855420f457f",
|
||||
"unique": "b3081fb2fb1445fa90b12b01481a78ef",
|
||||
"legend": "c357d2daf31a489d95b8fa47e50dd879"
|
||||
}
|
||||
},
|
||||
"classToFrame": {
|
||||
"warrior": "warrior",
|
||||
"fighter": "warrior",
|
||||
"page": "warrior",
|
||||
"spearman": "warrior",
|
||||
"magician": "magician",
|
||||
"firepoison": "magician",
|
||||
"icelightning": "magician",
|
||||
"cleric": "magician",
|
||||
"bandit": "bandit",
|
||||
"curse": "bandit",
|
||||
"shiv": "bandit",
|
||||
"poisoner": "bandit",
|
||||
"trickster": "bandit"
|
||||
},
|
||||
"rewardWeights": {
|
||||
"normal": 70,
|
||||
"unique": 25,
|
||||
"legend": 5
|
||||
}
|
||||
}
|
||||
1284
data/cards.json
Normal file
1284
data/cards.json
Normal file
File diff suppressed because it is too large
Load Diff
7
data/characters.json
Normal file
7
data/characters.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"portraits": {
|
||||
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
|
||||
"magician": "3b9ea1f066a744bb859df47fef817277",
|
||||
"bandit": "efa920e58d31426486ef974106e7dc8b"
|
||||
}
|
||||
}
|
||||
126
data/enemies.json
Normal file
126
data/enemies.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"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 },
|
||||
{ "kind": "Debuff", "effect": "weak", "value": 1 }
|
||||
]
|
||||
},
|
||||
"slime_boss": {
|
||||
"name": "슬라임 킹",
|
||||
"maxHp": 120,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 18 },
|
||||
{ "kind": "Defend", "value": 12 },
|
||||
{ "kind": "Debuff", "effect": "vuln", "value": 2 },
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "Attack", "value": 22 }
|
||||
]
|
||||
},
|
||||
"orange_mushroom": {
|
||||
"name": "주황버섯",
|
||||
"maxHp": 16,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 5 },
|
||||
{ "kind": "Attack", "value": 5 },
|
||||
{ "kind": "Defend", "value": 4 },
|
||||
{ "kind": "Attack", "value": 8 }
|
||||
]
|
||||
},
|
||||
"blue_mushroom": {
|
||||
"name": "파란버섯",
|
||||
"maxHp": 22,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 4 },
|
||||
{ "kind": "Attack", "value": 4 },
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "AddCard", "card": "Wound", "count": 1 }
|
||||
]
|
||||
},
|
||||
"pig": {
|
||||
"name": "돼지",
|
||||
"maxHp": 18,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Defend", "value": 5 }
|
||||
]
|
||||
},
|
||||
"green_mushroom": {
|
||||
"name": "초록버섯",
|
||||
"maxHp": 20,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 7 },
|
||||
{ "kind": "Defend", "value": 3 },
|
||||
{ "kind": "Attack", "value": 9 }
|
||||
]
|
||||
},
|
||||
"red_snail": {
|
||||
"name": "빨간 달팽이",
|
||||
"maxHp": 14,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 5 },
|
||||
{ "kind": "Defend", "value": 6 },
|
||||
{ "kind": "Attack", "value": 7 }
|
||||
]
|
||||
},
|
||||
"stump": {
|
||||
"name": "나무토막",
|
||||
"maxHp": 19,
|
||||
"intents": [
|
||||
{ "kind": "Defend", "value": 5 },
|
||||
{ "kind": "Attack", "value": 8 },
|
||||
{ "kind": "Attack", "value": 6 }
|
||||
]
|
||||
},
|
||||
"mushmom": {
|
||||
"name": "머쉬맘",
|
||||
"maxHp": 75,
|
||||
"intents": [
|
||||
{ "kind": "Defend", "value": 10 },
|
||||
{ "kind": "Debuff", "effect": "weak", "value": 2 },
|
||||
{ "kind": "Attack", "value": 16 },
|
||||
{ "kind": "Attack", "value": 9 },
|
||||
{ "kind": "Defend", "value": 6 },
|
||||
{ "kind": "AddCard", "card": "Burn", "count": 1 }
|
||||
]
|
||||
},
|
||||
"modified_snail": {
|
||||
"name": "변형된 달팽이",
|
||||
"maxHp": 60,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 12 },
|
||||
{ "kind": "Defend", "value": 8 },
|
||||
{ "kind": "Attack", "value": 7 },
|
||||
{ "kind": "Attack", "value": 14 },
|
||||
{ "kind": "Debuff", "effect": "weak", "value": 1 }
|
||||
]
|
||||
},
|
||||
"king_slime": {
|
||||
"name": "킹 슬라임",
|
||||
"maxHp": 130,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 18 },
|
||||
{ "kind": "Defend", "value": 14 },
|
||||
{ "kind": "Debuff", "effect": "vuln", "value": 2 },
|
||||
{ "kind": "Attack", "value": 12 },
|
||||
{ "kind": "Attack", "value": 24 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"activeEnemy": "slime",
|
||||
"simEncounter": ["orange_mushroom", "orange_mushroom", "blue_mushroom"]
|
||||
}
|
||||
11
data/nodeicons.json
Normal file
11
data/nodeicons.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"icons": {
|
||||
"combat": "f98db6823e894a4f90308d61f75894ac",
|
||||
"elite": "793ed8a757534b89a82f460747d2df24",
|
||||
"boss": "423056cdbbc04f4da131b9721c404d96",
|
||||
"shop": "da37e1fac55d455b9ade08569f09f798",
|
||||
"rest": "b86c1b0568bd45f3ae4a4b97e1b4a594",
|
||||
"treasure": "f8a6d58e20f54e2ca899485055df1ce4"
|
||||
},
|
||||
"background": "ef89906dd9844fcbaafc0b2313812eca"
|
||||
}
|
||||
14
data/potions.json
Normal file
14
data/potions.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"potions": {
|
||||
"redPotion": { "name": "빨간 포션", "desc": "HP 20 회복", "effect": "heal", "value": 20, "icon": "393e2a0d8da544899eaa8b22c97f832b" },
|
||||
"firebomb": { "name": "화염병", "desc": "적에게 피해 20", "effect": "damage", "value": 20, "icon": "7ddb464c2574456289a4eb72ce86f193" },
|
||||
"warriorElixir": { "name": "전사의 물약", "desc": "힘 +2", "effect": "strength", "value": 2, "icon": "7cfbd410581e4073815daaf5f3e6c72f" },
|
||||
"guardPotion": { "name": "수호의 물약", "desc": "방어도 +12", "effect": "block", "value": 12, "icon": "8f8402dfa0f746e18bf606ed74302c0a" },
|
||||
"manaElixir": { "name": "마나 엘릭서", "desc": "에너지 +2", "effect": "energy", "value": 2, "icon": "ec2778c366f6477ab0f8e7f06bcd73f4" },
|
||||
"cursedVial": { "name": "저주의 병", "desc": "적에게 약화 3", "effect": "weak", "value": 3, "icon": "a9a2763fdb6849dcba3028c737487680" }
|
||||
},
|
||||
"dropChance": 0.4,
|
||||
"baseSlots": 3,
|
||||
"beltSlots": 5,
|
||||
"shopPrice": 20
|
||||
}
|
||||
30
data/relics.json
Normal file
30
data/relics.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"relics": {
|
||||
"ironHeart": { "name": "강철 심장", "desc": "전투 시작 시 방어도 +6", "hook": "combatStart", "effect": "block", "value": 6, "icon": "e555b3a62f3c49dbb2c53784e6bd481f" },
|
||||
"energyCore": { "name": "에너지 코어", "desc": "턴 시작 시 에너지 +1", "hook": "turnStart", "effect": "energy", "value": 1, "icon": "a41014f28b47434ab9f49ef104523862" },
|
||||
"vampire": { "name": "흡혈 송곳니", "desc": "공격 카드 사용 시 HP +1", "hook": "cardPlayed", "effect": "healOnAttack", "value": 1, "icon": "ed64cde7e6c44b9e99502847e54f04e9" },
|
||||
"goldIdol": { "name": "황금 우상", "desc": "전투 승리 시 메소 +10", "hook": "combatReward", "effect": "gold", "value": 10, "icon": "03bb05c92b8f45edb0f3dad2e118fd5a" },
|
||||
"potionBelt": { "name": "장인의 벨트", "desc": "물약 슬롯이 5칸으로 늘어난다", "hook": "passive", "effect": "potionSlots", "value": 5, "icon": "36725b4566ac40d4902e2ab2113c2096" },
|
||||
"burningBlood": { "name": "자쿰의 투구", "desc": "전투 승리 시 HP 6 회복", "hook": "combatEnd", "effect": "healOnWin", "value": 6, "icon": "07f994825ce34131b419d43e890c878d" },
|
||||
"vajra": { "name": "미스릴 해머", "desc": "전투 시작 시 힘 +1", "hook": "combatStart", "effect": "strength", "value": 1, "icon": "59d2579d46dc41d590a9e6b141ad458b" },
|
||||
"anchor": { "name": "메이플 실드", "desc": "첫 턴 방어도 +10", "hook": "combatStart", "effect": "block", "value": 10, "icon": "6349413e08cc49848862591863d056a0" },
|
||||
"bagOfPrep": { "name": "모험가의 배낭", "desc": "첫 턴 드로우 +2", "hook": "combatStart", "effect": "draw", "value": 2, "icon": "77b240cb8af245b4801a714380267ae9" },
|
||||
"bloodVial": { "name": "피의 목걸이", "desc": "전투 시작 시 HP 2 회복", "hook": "combatStart", "effect": "heal", "value": 2, "icon": "c782e949506a42c49eb139c7e65527d7" },
|
||||
"bronzeScales": { "name": "브론즈 체인메일", "desc": "피격 시 공격자에게 3 반사", "hook": "onPlayerDamaged", "effect": "thorns", "value": 3, "icon": "87272346b145412391622cf803f888d1" },
|
||||
"strawberry": { "name": "건강의 반지", "desc": "획득 시 최대 HP +7", "hook": "passive", "effect": "maxHp", "value": 7, "icon": "58f643e29c354c2783a5ce9a72ec155c" },
|
||||
"penNib": { "name": "황금 깃펜", "desc": "10번째 공격마다 피해 2배", "hook": "attackCalc", "effect": "penNib", "value": 10, "icon": "4d38d721cc064d14b31b9e9a92754139" },
|
||||
"boot": { "name": "브론즈 부츠", "desc": "5 미만 공격 피해가 5로", "hook": "attackCalc", "effect": "boot", "value": 5, "icon": "d572b3aa4dac4162aa0d9e551b055dce" },
|
||||
"akabeko": { "name": "황소 투구", "desc": "전투 첫 공격 피해 +8", "hook": "attackCalc", "effect": "akabeko", "value": 8, "icon": "eb3330a6e2274eff958639f8792119d3" },
|
||||
"centennialPuzzle": { "name": "백년의 부적", "desc": "전투 첫 피격 시 드로우 3", "hook": "onPlayerDamaged", "effect": "firstLossDraw", "value": 3, "icon": "cfe5ed6556b944fc83ab58b774bb2b73" },
|
||||
"meatOnBone": { "name": "고기 망치", "desc": "승리 시 HP 50% 이하면 12 회복", "hook": "combatEnd", "effect": "healIfLow", "value": 12, "icon": "a93e8e87f184411c98c96b877d9f8b10" },
|
||||
"selfFormingClay": { "name": "점토 갑옷", "desc": "피해를 받으면 다음 턴 방어 +3", "hook": "onPlayerDamaged", "effect": "clayBlock", "value": 3, "icon": "bb446793c5204d5db7d33563fe79f648" },
|
||||
"championBelt": { "name": "챔피언 벨트", "desc": "취약 부여 시 약화 1 추가", "hook": "cardDebuff", "effect": "vulnAddsWeak", "value": 1, "icon": "7ca8c63026034113a561d6adf679fed2" }
|
||||
},
|
||||
"startingRelic": "ironHeart",
|
||||
"relicPool": [
|
||||
"energyCore", "vampire", "goldIdol",
|
||||
"potionBelt", "burningBlood", "vajra", "anchor", "bagOfPrep", "bloodVial",
|
||||
"bronzeScales", "strawberry", "penNib", "boot", "akabeko",
|
||||
"centennialPuzzle", "meatOnBone", "selfFormingClay", "championBelt"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
# 하단 카드 손패 UI 목업 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:** 전투 화면 하단에 카드 5장이 수평 일렬로 보이는 정적(static) 손패 UI 목업을 `ui/DefaultGroup.ui`에 추가한다.
|
||||
|
||||
**Architecture:** 카드 데이터 테이블 + MSW UI 엔티티 템플릿으로 21개 엔티티(컨테이너 1 + 카드 5 + 카드별 텍스트 3×5=15)를 생성하는 일회성 Node 스크립트(`tools/gen-cardhand.mjs`)를 만든다. 스크립트는 기존 엔티티를 변경하지 않고 `ContentProto.Entities` 배열 끝에 새 엔티티 JSON 텍스트만 삽입한다(텍스트 splice, 전체 재직렬화 없음). Maker에서 reload 후 Play 모드 스크린샷으로 시각 검증한다.
|
||||
|
||||
**Tech Stack:** MSW Maker `.ui`(JSON) 엔티티, Node.js(ESM, 표준 라이브러리만), MSW Maker MCP(`maker_refresh_workspace`/`maker_play`/`maker_screenshot`/`maker_stop`).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `tools/gen-cardhand.mjs` — 카드 손패 엔티티 생성기. 카드 데이터 + 컴포넌트 빌더(transform/sprite/text) + entity 빌더로 21개 엔티티를 만들고 `ui/DefaultGroup.ui`에 삽입. 멱등(이미 CardHand 있으면 무변경).
|
||||
- Modify: `ui/DefaultGroup.ui` — 스크립트가 `ContentProto.Entities` 끝에 CardHand 계층을 추가(기존 엔티티 불변).
|
||||
|
||||
좌표 공식(기존 `Button_Attack`로 검증 완료):
|
||||
- `OffsetMin = pos - pivot*size`, `OffsetMax = pos + (1-pivot)*size`
|
||||
- `Position.x = anchor.x*parentW - parentW/2 + pos.x` (y도 동일, parentH 사용)
|
||||
- 여기서 `pos`(=anchoredPosition)는 pivot 지점의 앵커 기준 오프셋, `parentW/H`는 **직속 부모**의 크기.
|
||||
|
||||
배치 요약:
|
||||
- CardHand: 부모 DefaultGroup(1920×1080), anchor(0.5,0), pivot(0.5,0), size 1020×280, pos(0,30)
|
||||
- Card i(0..4): 부모 CardHand(1020×280), anchor(0.5,0.5), pivot(0.5,0.5), size 180×250, pos((-2+i)*200, 0)
|
||||
- Cost: 부모 Card(180×250), anchor(0,1), pivot(0.5,0.5), size 50×50, pos(32,-32)
|
||||
- Name: anchor(0.5,1), size 160×50, pos(0,-70)
|
||||
- Desc: anchor(0.5,0), size 160×80, pos(0,55)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 생성 스크립트 작성
|
||||
|
||||
**Files:**
|
||||
- Create: `tools/gen-cardhand.mjs`
|
||||
|
||||
- [ ] **Step 1: 스크립트 파일 작성**
|
||||
|
||||
`tools/gen-cardhand.mjs`에 아래 내용을 그대로 작성한다.
|
||||
|
||||
```js
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const FILE = 'ui/DefaultGroup.ui';
|
||||
|
||||
// ---- card data ----
|
||||
const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1.0 };
|
||||
const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1.0 };
|
||||
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 },
|
||||
];
|
||||
const CARD_BG_RUID = 'cd0560c4fc7f3b14994b90a502f00a21'; // 기존 버튼 스프라이트 재사용
|
||||
const CARD_W = 180, CARD_H = 250;
|
||||
|
||||
// ---- guid helper (deterministic, hex-safe) ----
|
||||
const guid = (n) =>
|
||||
`cad0${n.toString(16).padStart(2, '0')}-0000-4000-8000-${n.toString(16).padStart(12, '0')}`;
|
||||
|
||||
// ---- component builders ----
|
||||
function transform({ parentW, parentH, anchor, pivot, size, pos }) {
|
||||
const offMin = { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y };
|
||||
const offMax = { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y };
|
||||
const position = {
|
||||
x: anchor.x * parentW - parentW / 2 + pos.x,
|
||||
y: anchor.y * parentH - parentH / 2 + pos.y,
|
||||
z: 0.0,
|
||||
};
|
||||
return {
|
||||
'@type': 'MOD.Core.UITransformComponent',
|
||||
ActivePlatform: 255,
|
||||
AlignmentOption: 0,
|
||||
AnchorsMax: { x: anchor.x, y: anchor.y },
|
||||
AnchorsMin: { x: anchor.x, y: anchor.y },
|
||||
MobileOnly: false,
|
||||
OffsetMax: offMax,
|
||||
OffsetMin: offMin,
|
||||
Pivot: { x: pivot.x, y: pivot.y },
|
||||
RectSize: { x: size.x, y: size.y },
|
||||
UIMode: 1,
|
||||
UIScale: { x: 1.0, y: 1.0, z: 1.0 },
|
||||
UIVersion: 2,
|
||||
anchoredPosition: { x: pos.x, y: pos.y },
|
||||
Position: position,
|
||||
QuaternionRotation: { x: 0.0, y: 0.0, z: 0.0, w: 1.0 },
|
||||
Scale: { x: 1.0, y: 1.0, z: 1.0 },
|
||||
Enable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function sprite({ dataId = '', color, type = 1, raycast = true }) {
|
||||
return {
|
||||
'@type': 'MOD.Core.SpriteGUIRendererComponent',
|
||||
AnimClipPlayType: 0,
|
||||
EndFrameIndex: 2147483647,
|
||||
ImageRUID: { DataId: dataId },
|
||||
LocalPosition: { x: 0.0, y: 0.0 },
|
||||
LocalScale: { x: 1.0, y: 1.0 },
|
||||
OverrideSorting: false,
|
||||
PlayRate: 1.0,
|
||||
PreserveSprite: 0,
|
||||
StartFrameIndex: 0,
|
||||
Color: color,
|
||||
DropShadow: false,
|
||||
DropShadowAngle: 30.0,
|
||||
DropShadowColor: { r: 0.0, g: 0.0, b: 0.0, a: 0.72 },
|
||||
DropShadowDistance: 32.0,
|
||||
FillAmount: 1.0,
|
||||
FillCenter: true,
|
||||
FillClockWise: true,
|
||||
FillMethod: 0,
|
||||
FillOrigin: 0,
|
||||
FlipX: false,
|
||||
FlipY: false,
|
||||
FrameColumn: 1,
|
||||
FrameRate: 0,
|
||||
FrameRow: 1,
|
||||
Outline: false,
|
||||
OutlineColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
||||
OutlineWidth: 3.0,
|
||||
RaycastTarget: raycast,
|
||||
Type: type,
|
||||
Enable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function text({ value, fontSize, bold, alignment = 4 }) {
|
||||
return {
|
||||
'@type': 'MOD.Core.TextComponent',
|
||||
Alignment: alignment,
|
||||
Bold: bold,
|
||||
DropShadow: false,
|
||||
DropShadowAngle: 30.0,
|
||||
DropShadowColor: { r: 0.0, g: 0.0, b: 0.0, a: 0.72 },
|
||||
DropShadowDistance: 32.0,
|
||||
Font: 0,
|
||||
FontColor: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
|
||||
FontSize: fontSize,
|
||||
MaxSize: fontSize,
|
||||
MinSize: 8,
|
||||
OutlineColor: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 },
|
||||
OutlineDistance: { x: 1.0, y: -1.0 },
|
||||
OutlineWidth: 1.0,
|
||||
Overflow: 0,
|
||||
OverrideSorting: false,
|
||||
Padding: { left: 0, right: 0, top: 0, bottom: 0 },
|
||||
SizeFit: false,
|
||||
Text: value,
|
||||
UseOutLine: true,
|
||||
Enable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function entity({ id, path, modelId, entryId, componentNames, components, displayOrder }) {
|
||||
const parts = path.split('/');
|
||||
const name = parts[parts.length - 1];
|
||||
const slashes = '/'.repeat(parts.length - 1);
|
||||
return {
|
||||
id,
|
||||
path,
|
||||
componentNames,
|
||||
jsonString: {
|
||||
name,
|
||||
path,
|
||||
nameEditable: true,
|
||||
enable: true,
|
||||
visible: true,
|
||||
localize: true,
|
||||
displayOrder,
|
||||
pathConstraints: slashes,
|
||||
revision: 1,
|
||||
origin: {
|
||||
type: 'Model',
|
||||
entry_id: entryId,
|
||||
sub_entity_id: null,
|
||||
root_entity_id: null,
|
||||
replaced_model_id: null,
|
||||
},
|
||||
modelId,
|
||||
'@components': components,
|
||||
'@version': 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---- build entities ----
|
||||
const TRANSPARENT = { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
const ents = [];
|
||||
let g = 0;
|
||||
|
||||
// CardHand container
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: '/ui/DefaultGroup/CardHand',
|
||||
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 }, pivot: { x: 0.5, y: 0 }, size: { x: 1020, y: 280 }, pos: { x: 0, y: 30 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
],
|
||||
}));
|
||||
|
||||
cards.forEach((c, i) => {
|
||||
const cardPath = `/ui/DefaultGroup/CardHand/Card${i + 1}`;
|
||||
// card background
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 1020, parentH: 280, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: (-2 + i) * 200, y: 0 } }),
|
||||
sprite({ dataId: CARD_BG_RUID, color: c.tint, type: 0, raycast: true }),
|
||||
],
|
||||
}));
|
||||
// cost (top-left)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Cost`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0, y: 1 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 50, y: 50 }, pos: { x: 32, y: -32 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.cost, fontSize: 34, bold: true }),
|
||||
],
|
||||
}));
|
||||
// name (upper-center)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Name`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 1 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 50 }, pos: { x: 0, y: -70 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.name, fontSize: 28, bold: true }),
|
||||
],
|
||||
}));
|
||||
// desc (lower-center)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Desc`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 80 }, pos: { x: 0, y: 55 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.desc, fontSize: 22, bold: false }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
// ---- splice into file ----
|
||||
let txt = readFileSync(FILE, 'utf8');
|
||||
|
||||
if (txt.includes('/ui/DefaultGroup/CardHand')) {
|
||||
console.log('CardHand already present — no changes made.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const matches = txt.match(/\n {4}\]/g); // Entities 닫는 대괄호(4-space indent)는 파일 내 유일
|
||||
if (!matches || matches.length !== 1) {
|
||||
console.error(`Expected exactly one Entities closing bracket, found ${matches ? matches.length : 0}. Aborting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const blocks = ents
|
||||
.map((e) => JSON.stringify(e, null, 2).split('\n').map((l) => ' ' + l).join('\n'))
|
||||
.join(',\n');
|
||||
|
||||
txt = txt.replace('\n ]', ',\n' + blocks + '\n ]');
|
||||
|
||||
JSON.parse(txt); // 유효성 검증 (실패 시 throw)
|
||||
|
||||
writeFileSync(FILE, txt, 'utf8');
|
||||
console.log(`Inserted ${ents.length} CardHand entities.`);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git add tools/gen-cardhand.mjs
|
||||
git commit -m "하단 카드 손패 엔티티 생성 스크립트 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 스크립트 실행 및 결과 검증
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/DefaultGroup.ui` (스크립트가 수정)
|
||||
|
||||
- [ ] **Step 1: 스크립트 실행**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node tools/gen-cardhand.mjs
|
||||
```
|
||||
|
||||
Expected 출력:
|
||||
```
|
||||
Inserted 21 CardHand entities.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: JSON 유효성 + 엔티티 수 검증**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const j=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const c=j.ContentProto.Entities.filter(e=>e.path.includes('CardHand'));console.log('count:',c.length);console.log(c.map(e=>e.path).join('\n'))"
|
||||
```
|
||||
|
||||
Expected: `count: 21` 그리고 경로 목록에 `/ui/DefaultGroup/CardHand`, `.../Card1`~`.../Card5`, 각 카드의 `/Cost`,`/Name`,`/Desc`가 모두 나타남.
|
||||
|
||||
- [ ] **Step 3: 멱등성 확인 (재실행 시 무변경)**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node tools/gen-cardhand.mjs
|
||||
```
|
||||
|
||||
Expected 출력:
|
||||
```
|
||||
CardHand already present — no changes made.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 기존 엔티티 불변 확인**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git diff ui/DefaultGroup.ui | findstr /R "^-"
|
||||
```
|
||||
|
||||
Expected: 삭제(`-`)된 줄이 **마지막 엔티티 뒤 `]` 직전 한 줄 외에는 없음** — 즉 기존 엔티티 내용은 그대로이고 끝에만 추가됨. (삭제 라인은 splice 지점의 ` ]` 한 줄뿐이어야 함)
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Maker 시각 검증
|
||||
|
||||
**Files:** (없음 — 검증 전용)
|
||||
|
||||
- [ ] **Step 1: 워크스페이스 reload**
|
||||
|
||||
MCP 도구 `maker_refresh_workspace` 호출 (edit 모드여야 함). Expected: `status: ok`.
|
||||
|
||||
- [ ] **Step 2: Play 모드 진입**
|
||||
|
||||
MCP 도구 `maker_play` 호출. (UI는 edit 캔버스가 아닌 Play 렌더에서 보임)
|
||||
|
||||
- [ ] **Step 3: 스크린샷 촬영 및 확인**
|
||||
|
||||
MCP 도구 `maker_screenshot` 호출 후 반환된 path를 Read로 열어 확인.
|
||||
Expected: 화면 **하단 중앙에 카드 5장이 수평 일렬**로 보이고, 각 카드에 코스트(1/2)·이름(타격/방어/강타)·설명(피해6/방어도5/피해10)이 표시되며, 공격 카드는 붉은톤·방어 카드는 푸른톤.
|
||||
|
||||
문제가 보이면(위치 어긋남/텍스트 안 보임/색 이상) 수치를 조정해 Task 1의 스크립트 파라미터를 고치고, `ui/DefaultGroup.ui`의 CardHand 블록을 되돌린 뒤(아래 명령) Task 2부터 재실행한다.
|
||||
|
||||
되돌리기:
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git checkout ui/DefaultGroup.ui
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Play 모드 종료**
|
||||
|
||||
MCP 도구 `maker_stop` 호출.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 최종 커밋
|
||||
|
||||
**Files:**
|
||||
- `ui/DefaultGroup.ui`
|
||||
|
||||
- [ ] **Step 1: 변경 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git add ui/DefaultGroup.ui
|
||||
git commit -m "전투 화면 하단에 카드 손패 5장 목업 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검증 요약
|
||||
|
||||
- 스크립트 단위 검증: `node tools/gen-cardhand.mjs` → 21개 삽입, 재실행 시 멱등
|
||||
- 데이터 검증: `JSON.parse` 성공 + CardHand 경로 21개 + 기존 엔티티 불변(diff)
|
||||
- 시각 검증: Maker Play 스크린샷에서 하단 5장 카드 렌더 확인
|
||||
@@ -1,235 +0,0 @@
|
||||
# 카드 슬롯 이미지 적용 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:** 하단 손패 5번 슬롯(강타)의 외형을 완성형 카드 이미지 `invincible belief.png`("리부트 프로토콜")로 교체한다.
|
||||
|
||||
**Architecture:** PNG를 MSW 계정 sprite 리소스로 업로드해 RUID를 발급받고, 그 RUID를 생성기 `gen-cardhand.mjs`의 5번 카드 데이터에 `image` 필드로 넣는다. 생성기는 `image`가 있는 카드를 단색 배경 대신 해당 RUID 스프라이트(흰색 틴트, 180×270)로 만들고 텍스트 자식을 생성하지 않는다. 재생성 후 Maker reload→play 스크린샷으로 검증한다.
|
||||
|
||||
**Tech Stack:** MSW 에셋 MCP(`asset_create_account_resource_storage_item`, 2단계 업로드), curl PUT, Node.js 생성기, msw-maker-mcp(reload/play/screenshot).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `tools/gen-cardhand.mjs` — 카드 빌드 루프에 `image` 분기 추가, 5번 카드에 RUID 부여.
|
||||
- Modify: `ui/DefaultGroup.ui` — 생성기가 5번 카드를 이미지 스프라이트로 재생성.
|
||||
- 외부: MSW 계정 리소스 스토리지에 PNG 업로드(저장소엔 RUID만 들어감).
|
||||
|
||||
원본 이미지: `C:\Users\jaeoh\Desktop\workspace\source\images\maple\invincible belief.png` (세로 약 2:3, 완성형 카드).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 이미지 업로드 및 RUID 확보 (컨트롤러/MCP 실행)
|
||||
|
||||
**Files:** 없음 (외부 리소스 생성)
|
||||
|
||||
- [ ] **Step 1: 파일 크기 확인**
|
||||
|
||||
```bash
|
||||
node -e "console.log(require('fs').statSync('C:/Users/jaeoh/Desktop/workspace/source/images/maple/invincible belief.png').size)"
|
||||
```
|
||||
출력된 바이트 수를 `<BYTES>`로 사용한다.
|
||||
|
||||
- [ ] **Step 2: 업로드 1단계 — presigned URL 발급**
|
||||
|
||||
MCP `asset_create_account_resource_storage_item` 호출:
|
||||
- `category`: `sprite`
|
||||
- `subcategory`: `etc`
|
||||
- `name`: `slaymaple_card_reboot_protocol`
|
||||
- `description`: `SlayMaple 손패 카드 이미지 (리부트 프로토콜)`
|
||||
- `contentLength`: `<BYTES>`
|
||||
- `fileUrl`: 생략
|
||||
|
||||
응답에서 `presignedUrl`을 `<PRESIGNED_URL>`로 확보.
|
||||
|
||||
- [ ] **Step 3: 파일 PUT 업로드**
|
||||
|
||||
```bash
|
||||
curl.exe -X PUT --data-binary "@C:/Users/jaeoh/Desktop/workspace/source/images/maple/invincible belief.png" "<PRESIGNED_URL>"
|
||||
```
|
||||
Expected: HTTP 200 (출력 없음 또는 빈 본문). 오류 시 응답 본문 확인.
|
||||
|
||||
- [ ] **Step 4: 업로드 2단계 — 리소스 생성 완료**
|
||||
|
||||
MCP `asset_create_account_resource_storage_item` 다시 호출, 이번엔 동일 파라미터 + `fileUrl`: `<PRESIGNED_URL>`.
|
||||
응답에서 발급된 리소스 **RUID(GUID/DataId)** 를 `<RUID>`로 확보.
|
||||
|
||||
- [ ] **Step 5: RUID 검증**
|
||||
|
||||
MCP `asset_list_account_resources` (`category`: `sprite`, `subcategory`: `etc`, `searchWord`: `reboot`) 호출 → 방금 만든 리소스가 목록에 있고 RUID가 일치하는지 확인. `<RUID>`를 기록해 Task 2에서 사용.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 생성기에 image 분기 추가
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-cardhand.mjs`
|
||||
|
||||
- [ ] **Step 1: 5번 카드 데이터에 image 필드 추가**
|
||||
|
||||
`cards` 배열의 마지막 원소(강타)를 다음으로 교체 (`<RUID>`는 Task 1에서 확보한 실제 값):
|
||||
|
||||
```js
|
||||
{ name: '강타', cost: '2', desc: '피해 10', tint: ATTACK, image: '<RUID>' },
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 카드 빌드 루프를 image 분기로 교체**
|
||||
|
||||
`cards.forEach((c, i) => { ... });` 블록 전체(현재 카드 배경 + cost/name/desc 생성)를 다음으로 교체:
|
||||
|
||||
```js
|
||||
cards.forEach((c, i) => {
|
||||
const cardPath = `/ui/DefaultGroup/CardHand/Card${i + 1}`;
|
||||
const cardH = c.image ? 270 : CARD_H;
|
||||
const cardSprite = c.image
|
||||
? sprite({ dataId: c.image, color: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, type: 0, raycast: true })
|
||||
: sprite({ color: c.tint, type: 1, raycast: true });
|
||||
// card background (or full image)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 1020, parentH: 280, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: cardH }, pos: { x: (i - (cards.length - 1) / 2) * CARD_SPACING, y: 0 }, align: ALIGN_CENTER }),
|
||||
cardSprite,
|
||||
],
|
||||
}));
|
||||
// 이미지 카드는 텍스트 오버레이를 만들지 않는다 (이미지에 이미 포함)
|
||||
if (c.image) return;
|
||||
// cost (top-left)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Cost`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 50, y: 50 }, pos: { x: -60, y: 95 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.cost, fontSize: 34, bold: true }),
|
||||
],
|
||||
}));
|
||||
// name (upper-center)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Name`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 50 }, pos: { x: 0, y: 50 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.name, fontSize: 28, bold: true }),
|
||||
],
|
||||
}));
|
||||
// desc (lower-center)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Desc`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 80 }, pos: { x: 0, y: -80 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.desc, fontSize: 22, bold: false }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 스크립트 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git add tools/gen-cardhand.mjs
|
||||
git commit -m "카드 손패 생성기: image 필드 지원 (5번 카드 이미지 적용)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 재생성 및 데이터 검증
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/DefaultGroup.ui`
|
||||
|
||||
- [ ] **Step 1: 카드 없는 베이스로 되돌린 뒤 재생성**
|
||||
|
||||
직전 카드 커밋(`c9c761d`) 이전 베이스에서 ui를 받아 재생성한다. (생성기는 CardHand가 이미 있으면 no-op이므로 베이스가 필요)
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git checkout 2c39066 -- ui/DefaultGroup.ui
|
||||
node tools/gen-cardhand.mjs
|
||||
```
|
||||
Expected: `Inserted 18 CardHand entities.` (컨테이너 1 + 카드 5 + 텍스트 12 = 18)
|
||||
|
||||
- [ ] **Step 2: 5번 카드 = 이미지, 텍스트 없음 / 나머지 4장 텍스트 유지 검증**
|
||||
|
||||
```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;const card5=E.find(e=>e.path==='/ui/DefaultGroup/CardHand/Card5');const sp=card5.jsonString['@components'][1];const tr=card5.jsonString['@components'][0];console.log('card5 image:', sp.ImageRUID.DataId);console.log('card5 height:', tr.RectSize.y);console.log('card5 has text children:', E.some(e=>e.path.startsWith('/ui/DefaultGroup/CardHand/Card5/')));console.log('card1 has text children:', E.some(e=>e.path.startsWith('/ui/DefaultGroup/CardHand/Card1/')));console.log('total CardHand entities:', E.filter(e=>e.path.includes('CardHand')).length)"
|
||||
```
|
||||
Expected:
|
||||
- `card5 image:` 가 `<RUID>` 와 일치
|
||||
- `card5 height: 270`
|
||||
- `card5 has text children: false`
|
||||
- `card1 has text children: true`
|
||||
- `total CardHand entities: 18`
|
||||
|
||||
- [ ] **Step 3: JSON 유효성 + 기존(우리 외) 엔티티 불변**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));console.log('JSON ok')"
|
||||
```
|
||||
Expected: `JSON ok`. (Button_Attack/Jump/UIJoystick/UIChat 4개 기본 엔티티는 splice가 끝에만 추가하므로 불변)
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Maker 시각 검증 (컨트롤러 실행)
|
||||
|
||||
**Files:** 없음
|
||||
|
||||
- [ ] **Step 1: reload** — msw-maker-mcp `maker_refresh_workspace` (edit 모드). Expected `status: ok`.
|
||||
- [ ] **Step 2: play** — `maker_play`.
|
||||
- [ ] **Step 3: 로드 확인** — `maker_execute_script` (context client):
|
||||
```lua
|
||||
local c5 = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card5")
|
||||
log("CARD5="..tostring(c5 ~= nil))
|
||||
if c5 ~= nil then local r = c5.SpriteGUIRendererComponent; log("CARD5_IMG="..tostring(r.ImageRUID.DataId)) end
|
||||
```
|
||||
→ `maker_logs(kind=normal)` 에서 `CARD5=true`, `CARD5_IMG=<RUID>` 확인. (이미지 미로드 시 `maker_logs(kind=build)` 도 확인)
|
||||
- [ ] **Step 4: screenshot** — `maker_screenshot` 후 Read로 열어 5번 자리에 "리부트 프로토콜" 카드 이미지가 왜곡 없이 표시되는지 확인. 나머지 4장은 단색 목업 유지.
|
||||
- [ ] **Step 5: stop** — `maker_stop`.
|
||||
|
||||
문제 시(이미지 안 보임/깨짐): subcategory를 `item`으로 바꿔 재업로드하거나, 스프라이트 Type/PreserveSprite를 조정. ui 되돌리기: `git checkout ui/DefaultGroup.ui` 후 Task 3부터 재실행.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 최종 커밋
|
||||
|
||||
**Files:**
|
||||
- `ui/DefaultGroup.ui`
|
||||
|
||||
- [ ] **Step 1: 디스크 무결성 후 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git add ui/DefaultGroup.ui
|
||||
git commit -m "5번 카드 슬롯에 리부트 프로토콜 이미지 카드 적용"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검증 요약
|
||||
- RUID 발급/검증 (asset_list)
|
||||
- 생성기: `Inserted 18`, 5번=이미지·텍스트없음·270, 나머지 텍스트 유지, JSON 유효
|
||||
- Maker: Lua로 Card5 이미지 RUID 확인 + 스크린샷 시각 확인
|
||||
@@ -1,217 +0,0 @@
|
||||
# 맵 개선(다양한 몬스터 + 타일셋 + StS2 배치) 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:** map02~map11에 공식 맵에서 수확한 다양한 몬스터 2종(기존 4종 미사용)을 StS2 우측 배치로, 맵마다 다른 타일셋으로 재생성한다.
|
||||
|
||||
**Architecture:** 공식 맵 import로 몬스터 변형 `{sprite,stand,hit,die}`과 타일셋 RUID를 수확(배경 수확과 동일 기법) → `tools/gen-maps.mjs`의 `MONSTER_VARIANTS`/`TILESETS`에 반영 → 몬스터 선택을 "서로 다른 2종 + 정적 베이스 + StS2 우측 고정위치"로, TileSetRUID를 맵별로 교체 → map02~map11 재생성. map02 스파이크로 렌더 검증 후 확대.
|
||||
|
||||
**Tech Stack:** Node.js, MSW `.map` JSON, msw-maker-mcp(import/save/play/screenshot/execute_script), msw-mcp.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Modify: `tools/gen-maps.mjs` — `MONSTER_VARIANTS`/`TILESETS` 데이터 + 몬스터 선택/배치 로직 + TileSetRUID 교체.
|
||||
- Modify(재생성): `map/map02.map`~`map11.map`.
|
||||
|
||||
기준 사실:
|
||||
- 몬스터 엔티티: `SpriteRendererComponent.SpriteRUID` + `StateAnimationComponent.ActionSheet{stand,hit,die}`. 정적 베이스로 쓸 템플릿은 path에 `Static` 포함(StaticMonsterTemplate, 배회 안 함).
|
||||
- 타일: 맵의 `/TileMap` 엔티티 `TileMapComponent.TileSetRUID.DataId`. map01 기본 `9dfea3808bbd49a5877d8624df21b1c7`.
|
||||
- 배경: 기존 `BACKGROUNDS` 10종 유지.
|
||||
- import는 현재 맵(map02, 재생성 가능)을 교체 → save → 파일에서 추출.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 몬스터 변형 + 타일셋 수확 (컨트롤러/MCP, 스파이크 포함)
|
||||
|
||||
**목표:** `MONSTER_VARIANTS`(≥12종 `{sprite,stand,hit,die}`) + `TILESETS`(10종 RUID) 확정.
|
||||
|
||||
- [ ] **Step 1: 몬스터 엔티티 구조 스파이크**
|
||||
|
||||
몬스터가 있는 공식 **필드맵** 1개를 import(`maker_import_maplestory_map`) → `maker_save` → `map/map02.map`에서 `script.Monster`를 포함하는 엔티티를 찾아 `SpriteRendererComponent.SpriteRUID` + `StateAnimationComponent.ActionSheet`(stand/hit/die)가 존재하는지 확인.
|
||||
- 존재 → 그 형태로 변형 추출.
|
||||
- 부재(구조 다름) → 폴백: `SpriteRUID`만 추출하고 `ActionSheet`는 map01 템플릿 유지(생성기에서 변형에 stand/hit/die가 없으면 ActionSheet 미변경하도록 처리).
|
||||
|
||||
필드맵 후보 id는 `maker_list_maplestory_maps`로 탐색(영문/지역명). 몬스터가 있는 사냥/필드맵을 고른다.
|
||||
|
||||
- [ ] **Step 2: 변형 ≥12종 수확**
|
||||
|
||||
필드맵 여러 개를 import→save→추출 반복. 각 맵의 몬스터 엔티티들에서 `{sprite, stand, hit, die}`를 모아 **중복 sprite 제거**해 ≥12종 확보. map01의 4종 sprite(`8ef238e0…`,`6c7130f5…`,`3e76c89a…`,`6d381bea…`,`c96c11f9…`)는 **제외**.
|
||||
|
||||
- [ ] **Step 3: 타일셋 10종 수확**
|
||||
|
||||
import한 맵들의 `TileMapComponent.TileSetRUID.DataId`를 수집해 **distinct 10종**(map01의 `9dfea380…` 제외). (배경 수확 때처럼 import 1회로 타일셋+몬스터 동시 수확 가능)
|
||||
|
||||
- [ ] **Step 4: 결과 정리**
|
||||
|
||||
`MONSTER_VARIANTS = [{sprite,stand,hit,die}, ...]`(≥12)와 `TILESETS = [ruid, ...]`(10)를 Task 2에 넘길 형태로 기록. (코드 변경 없음; 데이터 산출)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 생성기 로직·데이터 갱신
|
||||
|
||||
**Files:** Modify `tools/gen-maps.mjs`
|
||||
|
||||
- [ ] **Step 1: TILESETS 상수 추가**
|
||||
|
||||
`BACKGROUNDS = [...]` 정의 바로 아래에 추가(값은 Task 1 결과):
|
||||
|
||||
```js
|
||||
// 공식 맵에서 수확한 타일셋 RUID 10종 (맵마다 다르게). map01 기본(9dfea380…) 제외.
|
||||
const TILESETS = [
|
||||
// Task 1에서 수확한 10개 RUID
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 2: MONSTER_VARIANTS 채우기**
|
||||
|
||||
기존 `const MONSTER_VARIANTS = [];` 를 Task 1에서 수확한 ≥12종으로 교체:
|
||||
|
||||
```js
|
||||
// 공식 맵에서 수확한 몬스터 변형 (기존 map01 4종 미사용).
|
||||
const MONSTER_VARIANTS = [
|
||||
// { sprite: '...', stand: '...', hit: '...', die: '...' }, ... (≥12종)
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 몬스터 배치 로직 교체 (서로 다른 2종 + StS2 + 정적 베이스)**
|
||||
|
||||
`buildMap` 안의 몬스터 추가 루프(`const ents = ...` 이후 `for (let i = 0; i < 2; i++) { ... }` 블록 전체)를 다음으로 교체:
|
||||
|
||||
```js
|
||||
const ents = map.ContentProto.Entities.filter((e) => !isMonster(e));
|
||||
// 정적 베이스(StS2 위치 고정 — 배회 방지). 변형이 sprite/animation을 덮어쓰므로 외형은 베이스와 무관.
|
||||
const base = monsterTemplates.find((e) => (e.path || '').includes('Static')) || monsterTemplates[0];
|
||||
// 서로 다른 변형 2종 선택 (맵 내 중복 금지)
|
||||
const vi = Math.floor(rand() * MONSTER_VARIANTS.length);
|
||||
const vj = (vi + 1 + Math.floor(rand() * (MONSTER_VARIANTS.length - 1))) % MONSTER_VARIANTS.length;
|
||||
const chosen = [MONSTER_VARIANTS[vi], MONSTER_VARIANTS[vj]];
|
||||
const STS2_X = [3.5, 5.5]; // 화면 우측 전투 포메이션
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const m = JSON.parse(JSON.stringify(base));
|
||||
m.jsonString.name = `Monster${i + 1}`;
|
||||
m.path = `/maps/map${tag}/Monster${i + 1}`;
|
||||
m.jsonString.path = m.path;
|
||||
const tr = compOf(m, 'MOD.Core.TransformComponent');
|
||||
if (tr) tr.Position.x = STS2_X[i];
|
||||
const v = chosen[i];
|
||||
const sp = compOf(m, 'MOD.Core.SpriteRendererComponent');
|
||||
if (sp) sp.SpriteRUID = v.sprite;
|
||||
const sa = compOf(m, 'MOD.Core.StateAnimationComponent');
|
||||
if (sa && v.stand) sa.ActionSheet = { stand: v.stand, hit: v.hit, die: v.die };
|
||||
ents.push(m);
|
||||
}
|
||||
```
|
||||
|
||||
(`v.stand`가 없으면 ActionSheet를 유지 → 폴백 호환)
|
||||
|
||||
- [ ] **Step 4: TileSetRUID 교체 추가**
|
||||
|
||||
`buildMap`의 경로/배경 설정 루프 `for (const e of ents) { ... }` 안, 배경 설정 블록 다음에 추가:
|
||||
|
||||
```js
|
||||
if ((e.path || '').endsWith('/TileMap')) {
|
||||
const tm = compOf(e, 'MOD.Core.TileMapComponent');
|
||||
if (tm && TILESETS.length > 0) tm.TileSetRUID = { DataId: TILESETS[(nn - 2) % TILESETS.length] };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 구문 확인 + 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node --check tools/gen-maps.mjs
|
||||
git add tools/gen-maps.mjs
|
||||
git commit -m "맵 생성기: 수확한 다양한 몬스터 2종(StS2 배치) + 맵별 타일셋 교체"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: map02 스파이크 — 재생성 + Maker 검증
|
||||
|
||||
**Files:** Modify `map/map02.map`
|
||||
|
||||
- [ ] **Step 1: map02 재생성**
|
||||
|
||||
수확 import로 오염된 map02를 깨끗이 재생성:
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git checkout map/map01.map # 혹시 모를 보호(템플릿). map01은 변경 대상 아님
|
||||
node tools/gen-maps.mjs 2
|
||||
```
|
||||
Expected: `Generated: map02`
|
||||
|
||||
- [ ] **Step 2: 데이터 검증**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const j=JSON.parse(require('fs').readFileSync('map/map02.map','utf8'));const E=j.ContentProto.Entities;const ms=E.filter(e=>(e.componentNames||'').includes('script.Monster'));const old=['8ef238e0d0ca4bb783aca526cff35d11','6c7130f51a654803a1c39cbe30e2f427','3e76c89ae8e7477ca871f5bbcd6f6f29','6d381bea1bcb4504b518a1fbfa0904ac','c96c11f9a3f845a4b6a27d9ca10ab103'];const sprs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.SpriteRendererComponent').SpriteRUID);const xs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.TransformComponent').Position.x);const tm=E.find(e=>(e.path||'').endsWith('/TileMap')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.TileMapComponent').TileSetRUID.DataId;console.log('monsters:',ms.length);console.log('sprites:',sprs.join(','));console.log('distinct sprites:',new Set(sprs).size===2);console.log('no old sprite:',sprs.every(s=>!old.includes(s)));console.log('positions x:',xs.join(','));console.log('tileset:',tm,'changed:',tm!=='9dfea3808bbd49a5877d8624df21b1c7')"
|
||||
```
|
||||
Expected: `monsters: 2`, 2개 sprite distinct, `no old sprite: true`, positions x = `3.5,5.5`, tileset이 `9dfea380…`이 아님(교체됨).
|
||||
|
||||
- [ ] **Step 3: Maker 렌더 검증 (컨트롤러)**
|
||||
|
||||
1. `maker_refresh_workspace`
|
||||
2. map02가 활성인지 확인(`maker_get_current_map`). 아니면 사용자에게 map02 열기 요청.
|
||||
3. `maker_play` → `maker_screenshot` → Read로 확인: 몬스터 2마리가 **수확된(기존과 다른) 외형**으로 **우측에** 보이고, **타일 텍스처가 바뀌었는지**.
|
||||
4. `maker_execute_script`(client)로 확인:
|
||||
```lua
|
||||
local m1=_EntityService:GetEntityByPath("/maps/map02/Monster1")
|
||||
local m2=_EntityService:GetEntityByPath("/maps/map02/Monster2")
|
||||
if m1 then log("M1 spr="..tostring(m1.SpriteRendererComponent.SpriteRUID).." x="..tostring(m1.TransformComponent.Position.x)) end
|
||||
if m2 then log("M2 spr="..tostring(m2.SpriteRendererComponent.SpriteRUID).." x="..tostring(m2.TransformComponent.Position.x)) end
|
||||
```
|
||||
→ `maker_logs(normal)`로 sprite/x 확인.
|
||||
5. `maker_stop`.
|
||||
|
||||
- [ ] **Step 4: 게이트 판정**
|
||||
|
||||
- 몬스터 외형 변경 + 우측 배치 + 타일 변경 정상 → Task 4.
|
||||
- 몬스터가 흰박스/안 보임 → 변형 sprite/animation 로드 문제 → Task 1 폴백(SpriteRUID만, ActionSheet 유지) 적용 후 재생성.
|
||||
- 타일이 깨져 보임 → 해당 타일셋 제외하거나 호환 타일셋으로 교체(`TILESETS` 조정) 후 재생성.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 전체 재생성 + 검증
|
||||
|
||||
**Files:** Modify `map/map02.map`~`map11.map`, `Global/SectorConfig.config`
|
||||
|
||||
- [ ] **Step 1: 전체 재생성**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node tools/gen-maps.mjs
|
||||
```
|
||||
Expected: `Generated: map02 … map11`, `SectorConfig entries: 11`.
|
||||
|
||||
- [ ] **Step 2: 전체 데이터 검증**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const fs=require('fs');const old=['8ef238e0d0ca4bb783aca526cff35d11','6c7130f51a654803a1c39cbe30e2f427','3e76c89ae8e7477ca871f5bbcd6f6f29','6d381bea1bcb4504b518a1fbfa0904ac','c96c11f9a3f845a4b6a27d9ca10ab103'];let ids=new Set(),dup=false,ts=new Set(),bad=false;for(let n=2;n<=11;n++){const t=String(n).padStart(2,'0');const j=JSON.parse(fs.readFileSync('map/map'+t+'.map','utf8'));const E=j.ContentProto.Entities;const ms=E.filter(e=>(e.componentNames||'').includes('script.Monster'));if(ms.length!==2)throw new Error('monsters '+t);const sprs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.SpriteRendererComponent').SpriteRUID);if(new Set(sprs).size!==2)bad=true;if(sprs.some(s=>old.includes(s)))bad=true;ts.add(E.find(e=>(e.path||'').endsWith('/TileMap')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.TileMapComponent').TileSetRUID.DataId);for(const e of E){if(ids.has(e.id))dup=true;ids.add(e.id);}}console.log('cross-map id dup:',dup);console.log('any old/dup-in-map sprite:',bad);console.log('distinct tilesets:',ts.size)"
|
||||
```
|
||||
Expected: `cross-map id dup: false`, `any old/dup-in-map sprite: false`, `distinct tilesets: 10`.
|
||||
|
||||
- [ ] **Step 3: Maker 표본 검증 (컨트롤러)**
|
||||
|
||||
`maker_refresh_workspace` 후 표본 맵(map05, map09)을 각각 열어(사용자 협조) `maker_play`→`maker_screenshot`로 몬스터 외형·타일이 맵마다 다른지 확인. `maker_stop`.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 최종 커밋
|
||||
|
||||
- [ ] **Step 1: 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git add tools/gen-maps.mjs Global/SectorConfig.config map/map02.map map/map03.map map/map04.map map/map05.map map/map06.map map/map07.map map/map08.map map/map09.map map/map10.map map/map11.map
|
||||
git commit -m "맵 10개: 다양한 몬스터 2종(StS2 우측 배치) + 맵별 타일셋 적용"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검증 요약
|
||||
- 수확: 몬스터 변형 ≥12 / 타일셋 10 (스파이크로 구조 확인)
|
||||
- map02 스파이크: 데이터(2 distinct sprite·old 미사용·x=3.5/5.5·타일셋 교체) + Maker 렌더
|
||||
- 전체: cross-map id 무중복, old sprite 미사용, 타일셋 10 distinct
|
||||
- Maker 표본 시각 확인
|
||||
@@ -1,273 +0,0 @@
|
||||
# 맵 10개 생성 (랜덤 배경 + 몬스터 2마리) 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:** `map01`을 템플릿으로 독립 맵 10개(`map02`~`map11`)를 생성하고, 맵마다 다른 공식 배경 + 랜덤 위치 몬스터 2마리를 배치한다.
|
||||
|
||||
**Architecture:** Node 생성기 `tools/gen-maps.mjs`가 `map/map01.map`을 JSON으로 읽어 맵마다 deep-clone → 경로/EntryKey/name 치환, 전 엔티티 GUID 재발급(자기참조 보정), `Background.TemplateRUID` 교체, 몬스터 2마리 배치 → `map/mapNN.map`(JSON.stringify)로 기록. `SectorConfig.config`에 등록. 몬스터 다양화(A)는 `MONSTER_VARIANTS` 데이터로 주입하며, map02 스파이크로 렌더 검증 후 10개로 확대(실패 시 B=기존 몬스터 폴백).
|
||||
|
||||
**Tech Stack:** Node.js(ESM, 표준 라이브러리), MSW `.map`(JSON 엔티티), msw-mcp 에셋 검색, msw-maker-mcp reload/play/screenshot/execute_script.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Create: `tools/gen-maps.mjs` — 맵 생성기 (템플릿 클론·GUID 재발급·배경/몬스터 주입·SectorConfig 갱신).
|
||||
- Create: `map/map02.map` ~ `map/map11.map` — 생성 결과.
|
||||
- Modify: `Global/SectorConfig.config` — `entries`에 map02~map11 추가.
|
||||
|
||||
배경 RUID 풀(공식 라이브러리, 확보 완료, 10개):
|
||||
`79c95db9fdbb4c4796771733d069e3e2`, `1d4a335a5416401f8e289d78a03fd0c3`, `731a9cd1cce045e19d50fdcdc9a20be9`, `695805b1809243fd9376e2bba113ebde`, `454804df4c7e4701997ec8a8de088597`, `01992685f5d147b3a5c18fabf584807f`, `c861e9cb2d0b4d91be5d4d6aedf796b1`, `ee2e13a352d64611906760c1b722df67`, `8e89019c54d14aed875e54f13fa14109`, `fa936edd365f47e4b5622c19b1a80a0c`
|
||||
|
||||
맵 구조(map01): 엔티티 `/maps/map01`(Map+Foothold), `/Background`(BackgroundComponent.TemplateRUID), `/MapleMapLayer`, `/TileMap`, `/SpawnLocation`, 몬스터들(componentNames에 `script.Monster` 포함: StaticMonsterTemplate/MoveMonsterTemplate/ChaseMonsterTemplate/monster-43). 엔티티 id는 대시 GUID(8-4-4-4-12), 리소스 RUID는 대시 없는 32hex.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 라이브러리 몬스터 변형 후보 조사 (컨트롤러/MCP, 타임박스)
|
||||
|
||||
**목표:** 완결된 라이브러리 몬스터 변형(스프라이트 + stand/hit/die 액션 RUID 세트)을 ≥3종 확보 시도. 액션 그룹핑/이름을 얻을 수 없으면 **B 폴백**(빈 변형)으로 결정.
|
||||
|
||||
- [ ] **Step 1: 라이브러리 몬스터 리소스 조사**
|
||||
|
||||
MCP `asset_search_resources`로 `cat="animationclip"`/`"sprite"`, `source="maplestory"`, `query`로 몬스터 후보를 찾고, 가능하면 `detail=true` 및 메타데이터로 action(stand/hit/die) 식별을 시도한다.
|
||||
|
||||
- [ ] **Step 2: 변형 세트 확정 또는 폴백 결정**
|
||||
|
||||
각 변형을 `{ sprite, stand, hit, die }`(RUID) 형태로 ≥3개 확보하면 → 그 배열을 Task 2의 `MONSTER_VARIANTS`로 사용.
|
||||
액션 식별이 불가하거나 불확실하면 → `MONSTER_VARIANTS = []`로 두고 **B 폴백**(기존 템플릿 몬스터 그대로 사용)으로 진행한다. 결정 결과를 한 줄로 기록(`log` 또는 보고).
|
||||
|
||||
> 이 태스크의 산출물은 "MONSTER_VARIANTS 배열(또는 빈 배열) + 결정 사유" 한 가지다. 코드 변경 없음.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 생성기 작성
|
||||
|
||||
**Files:** Create `tools/gen-maps.mjs`
|
||||
|
||||
- [ ] **Step 1: 스크립트 작성**
|
||||
|
||||
`tools/gen-maps.mjs`에 아래를 그대로 작성한다. `MONSTER_VARIANTS`는 Task 1 결과로 채우거나 빈 배열로 둔다(빈 배열 = B 폴백).
|
||||
|
||||
```js
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const TEMPLATE = 'map/map01.map';
|
||||
const SECTOR = 'Global/SectorConfig.config';
|
||||
const MAP_NUMBERS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
|
||||
// 공식 라이브러리 배경 RUID 풀 (맵마다 1개씩, 서로 다르게)
|
||||
const BACKGROUNDS = [
|
||||
'79c95db9fdbb4c4796771733d069e3e2', '1d4a335a5416401f8e289d78a03fd0c3',
|
||||
'731a9cd1cce045e19d50fdcdc9a20be9', '695805b1809243fd9376e2bba113ebde',
|
||||
'454804df4c7e4701997ec8a8de088597', '01992685f5d147b3a5c18fabf584807f',
|
||||
'c861e9cb2d0b4d91be5d4d6aedf796b1', 'ee2e13a352d64611906760c1b722df67',
|
||||
'8e89019c54d14aed875e54f13fa14109', 'fa936edd365f47e4b5622c19b1a80a0c',
|
||||
];
|
||||
|
||||
// Task 1 결과. 비어 있으면 기존 템플릿 몬스터를 그대로 사용(B 폴백).
|
||||
// 각 항목: { sprite, stand, hit, die } (모두 RUID 문자열)
|
||||
const MONSTER_VARIANTS = [];
|
||||
|
||||
// 결정론적 시드 RNG (맵 번호 기반)
|
||||
function rng(seed) {
|
||||
let s = seed >>> 0;
|
||||
return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
|
||||
}
|
||||
|
||||
// 결정론적 대시 GUID (맵번호, 인덱스)
|
||||
function mapGuid(nn, idx) {
|
||||
const n = (nn * 1000 + idx) >>> 0;
|
||||
const h8 = n.toString(16).padStart(8, '0');
|
||||
const h12 = n.toString(16).padStart(12, '0');
|
||||
return `${h8}-0000-4000-8000-${h12}`;
|
||||
}
|
||||
|
||||
const isMonster = (e) => (e.componentNames || '').includes('script.Monster');
|
||||
const compOf = (e, type) => e.jsonString['@components'].find((c) => c['@type'] === type);
|
||||
|
||||
const template = JSON.parse(readFileSync(TEMPLATE, 'utf8'));
|
||||
const monsterTemplates = template.ContentProto.Entities.filter(isMonster);
|
||||
if (monsterTemplates.length === 0) throw new Error('템플릿에서 몬스터 엔티티를 못 찾음');
|
||||
|
||||
function buildMap(nn) {
|
||||
const tag = String(nn).padStart(2, '0');
|
||||
const rand = rng(nn * 7919);
|
||||
const map = JSON.parse(JSON.stringify(template)); // deep clone
|
||||
map.EntryKey = `map://map${tag}`;
|
||||
|
||||
// 비-몬스터 엔티티만 유지
|
||||
const ents = map.ContentProto.Entities.filter((e) => !isMonster(e));
|
||||
|
||||
// 몬스터 2마리 추가 (템플릿 몬스터 복제)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const src = monsterTemplates[Math.floor(rand() * monsterTemplates.length)];
|
||||
const m = JSON.parse(JSON.stringify(src));
|
||||
m.jsonString.name = `Monster${i + 1}`;
|
||||
m.path = `/maps/map${tag}/Monster${i + 1}`;
|
||||
m.jsonString.path = m.path;
|
||||
const tr = compOf(m, 'MOD.Core.TransformComponent');
|
||||
if (tr) tr.Position.x = Math.round((rand() * 8 - 4) * 100) / 100; // -4..4 바닥 위
|
||||
if (MONSTER_VARIANTS.length > 0) {
|
||||
const v = MONSTER_VARIANTS[Math.floor(rand() * MONSTER_VARIANTS.length)];
|
||||
const sp = compOf(m, 'MOD.Core.SpriteRendererComponent');
|
||||
if (sp) sp.SpriteRUID = v.sprite;
|
||||
const sa = compOf(m, 'MOD.Core.StateAnimationComponent');
|
||||
if (sa) sa.ActionSheet = { stand: v.stand, hit: v.hit, die: v.die };
|
||||
}
|
||||
ents.push(m);
|
||||
}
|
||||
|
||||
// 경로/이름 치환 + 배경 설정
|
||||
for (const e of ents) {
|
||||
if (typeof e.path === 'string') e.path = e.path.replace('/maps/map01', `/maps/map${tag}`);
|
||||
if (e.jsonString) {
|
||||
if (typeof e.jsonString.path === 'string') e.jsonString.path = e.jsonString.path.replace('/maps/map01', `/maps/map${tag}`);
|
||||
if (e.jsonString.name === 'map01') e.jsonString.name = `map${tag}`;
|
||||
}
|
||||
if ((e.path || '').endsWith('/Background')) {
|
||||
const bg = compOf(e, 'MOD.Core.BackgroundComponent');
|
||||
if (bg) bg.TemplateRUID = BACKGROUNDS[(nn - 2) % BACKGROUNDS.length];
|
||||
}
|
||||
}
|
||||
|
||||
// GUID 재발급 (자기참조 root/sub_entity_id 보정)
|
||||
ents.forEach((e, idx) => {
|
||||
const oldId = e.id;
|
||||
const newId = mapGuid(nn, idx);
|
||||
e.id = newId;
|
||||
const o = e.jsonString && e.jsonString.origin;
|
||||
if (o) {
|
||||
if (o.root_entity_id === oldId) o.root_entity_id = newId;
|
||||
if (o.sub_entity_id === oldId) o.sub_entity_id = newId;
|
||||
}
|
||||
});
|
||||
|
||||
map.ContentProto.Entities = ents;
|
||||
writeFileSync(`map/map${tag}.map`, JSON.stringify(map, null, 2), 'utf8');
|
||||
return `map${tag}`;
|
||||
}
|
||||
|
||||
// 인자: 생성할 맵 번호(미지정 시 전체). 예: node tools/gen-maps.mjs 2
|
||||
const arg = process.argv[2];
|
||||
const targets = arg ? [Number(arg)] : MAP_NUMBERS;
|
||||
const made = targets.map(buildMap);
|
||||
console.log('Generated:', made.join(', '));
|
||||
|
||||
// SectorConfig 등록 (전체 생성 시에만, 중복 방지)
|
||||
if (!arg) {
|
||||
const sector = JSON.parse(readFileSync(SECTOR, 'utf8'));
|
||||
const entries = sector.ContentProto.Json.Sectors[0].entries;
|
||||
for (const nn of MAP_NUMBERS) {
|
||||
const key = `map://map${String(nn).padStart(2, '0')}`;
|
||||
if (!entries.includes(key)) entries.push(key);
|
||||
}
|
||||
writeFileSync(SECTOR, JSON.stringify(sector, null, 2), 'utf8');
|
||||
console.log('SectorConfig entries:', entries.length);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 구문 확인**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node --check tools/gen-maps.mjs
|
||||
```
|
||||
Expected: 출력 없음(exit 0).
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-maps.mjs
|
||||
git commit -m "맵 생성기 추가 (map01 템플릿 복제·배경/몬스터 주입)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: map02 스파이크 — 생성 + Maker 렌더 검증
|
||||
|
||||
**Files:** Create `map/map02.map`
|
||||
|
||||
- [ ] **Step 1: map02만 생성**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node tools/gen-maps.mjs 2
|
||||
```
|
||||
Expected: `Generated: map02`
|
||||
|
||||
- [ ] **Step 2: 데이터 검증**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const j=JSON.parse(require('fs').readFileSync('map/map02.map','utf8'));const E=j.ContentProto.Entities;console.log('EntryKey:',j.EntryKey);const ids=E.map(e=>e.id);console.log('unique ids:', new Set(ids).size===ids.length);console.log('monsters:', E.filter(e=>(e.componentNames||'').includes('script.Monster')).length);const bg=E.find(e=>(e.path||'').endsWith('/Background'));console.log('bg RUID:', bg.jsonString['@components'].find(c=>c['@type']==='MOD.Core.BackgroundComponent').TemplateRUID);console.log('paths ok:', E.every(e=>!(e.path||'').includes('/maps/map01')))"
|
||||
```
|
||||
Expected: `EntryKey: map://map02`, `unique ids: true`, `monsters: 2`, `bg RUID:` 가 배경 풀의 첫 값(`79c95db9...`), `paths ok: true`.
|
||||
|
||||
- [ ] **Step 3: Maker에서 map02 열어 렌더 검증 (컨트롤러)**
|
||||
|
||||
1. `maker_refresh_workspace` (edit)
|
||||
2. Maker에서 map02를 활성 맵으로 연다(에디터에서 map02 더블클릭). MCP로 직접 맵 전환이 안 되면, 사용자에게 "Maker에서 map02 열기"를 요청한다.
|
||||
3. `maker_play` → `maker_screenshot` → Read로 확인: **배경이 map01과 다른 배경으로 표시**되고 **몬스터 2마리가 보이는지**.
|
||||
4. `maker_execute_script`(client)로 몬스터 로드 확인:
|
||||
```lua
|
||||
local m1 = _EntityService:GetEntityByPath("/maps/map02/Monster1")
|
||||
local m2 = _EntityService:GetEntityByPath("/maps/map02/Monster2")
|
||||
log("M1="..tostring(m1~=nil).." M2="..tostring(m2~=nil))
|
||||
```
|
||||
→ `maker_logs(normal)`에서 `M1=true M2=true` 확인.
|
||||
5. `maker_stop`.
|
||||
|
||||
- [ ] **Step 4: 게이트 판정**
|
||||
|
||||
- 배경·몬스터 정상 → 그대로 진행(Task 4).
|
||||
- 배경이 흰/검 박스이거나 몬스터 안 보임:
|
||||
- 배경 문제: 배경 RUID 풀이 로컬 워크스페이스에서 로드 안 됨 → 사용자와 상의(공식 배경 로드 가능 여부). 우선 다른 배경 RUID로 교체 시도.
|
||||
- 몬스터 변형(A) 문제(MONSTER_VARIANTS 사용 중일 때만): `MONSTER_VARIANTS = []`로 비우고(B 폴백) Step 1부터 재실행.
|
||||
- ui 되돌리기 필요 시: `git checkout map/map02.map` 후 재생성.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 나머지 맵 생성 + SectorConfig 등록
|
||||
|
||||
**Files:** Create `map/map03.map`~`map/map11.map`, Modify `Global/SectorConfig.config`
|
||||
|
||||
- [ ] **Step 1: 전체 생성**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node tools/gen-maps.mjs
|
||||
```
|
||||
Expected: `Generated: map02, map03, ... map11` 와 `SectorConfig entries: 11`
|
||||
|
||||
- [ ] **Step 2: 전체 데이터 검증**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
node -e "const fs=require('fs');let allIds=new Set(),dup=false,bgs=new Set();for(let n=2;n<=11;n++){const t=String(n).padStart(2,'0');const j=JSON.parse(fs.readFileSync('map/map'+t+'.map','utf8'));const E=j.ContentProto.Entities;if(j.EntryKey!=='map://map'+t)throw new Error('EntryKey '+t);if(E.filter(e=>(e.componentNames||'').includes('script.Monster')).length!==2)throw new Error('monsters '+t);for(const e of E){if(allIds.has(e.id))dup=true;allIds.add(e.id);}bgs.add(E.find(e=>(e.path||'').endsWith('/Background')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.BackgroundComponent').TemplateRUID);}const sec=JSON.parse(fs.readFileSync('Global/SectorConfig.config','utf8'));console.log('cross-map id dup:',dup);console.log('distinct backgrounds:',bgs.size);console.log('sector entries:',sec.ContentProto.Json.Sectors[0].entries.length)"
|
||||
```
|
||||
Expected: `cross-map id dup: false`, `distinct backgrounds: 10`, `sector entries: 11`.
|
||||
|
||||
- [ ] **Step 3: Maker 표본 검증 (컨트롤러)**
|
||||
|
||||
`maker_refresh_workspace` 후, 표본 맵 2~3개(map05, map08, map11)를 각각 열어 `maker_play`→`maker_screenshot`로 배경이 서로 다르고 몬스터 2마리가 보이는지 확인. 맵 전환이 MCP로 안 되면 사용자에게 해당 맵 열기를 요청. 확인 후 `maker_stop`.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 최종 커밋
|
||||
|
||||
**Files:** `map/map02.map`~`map/map11.map`, `Global/SectorConfig.config`
|
||||
|
||||
- [ ] **Step 1: 커밋**
|
||||
|
||||
```bash
|
||||
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
|
||||
git add map/map02.map map/map03.map map/map04.map map/map05.map map/map06.map map/map07.map map/map08.map map/map09.map map/map10.map map/map11.map Global/SectorConfig.config
|
||||
git commit -m "맵 10개(map02~map11) 생성: 랜덤 배경 + 몬스터 2마리, sector 등록"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검증 요약
|
||||
- 생성기 `node --check` 통과
|
||||
- map02 스파이크: 데이터(고유 id/2몬스터/배경) + Maker 렌더(배경 상이·몬스터 2)로 A/B 게이트 판정
|
||||
- 전체: cross-map id 중복 없음, 배경 10종 distinct, sector 11개
|
||||
- Maker 표본 맵 시각 확인
|
||||
@@ -1,256 +0,0 @@
|
||||
# 덱 컨트롤러 코드리뷰 수정 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미사용·타일셋교체) 검증 후 포함
|
||||
@@ -1,96 +0,0 @@
|
||||
# 하단 카드 손패 UI 목업 설계 (Slay the Spire 2 스타일)
|
||||
|
||||
- 날짜: 2026-06-06
|
||||
- 브랜치: feature/sts2-combat-layout
|
||||
- 대상 파일: `ui/DefaultGroup.ui`
|
||||
|
||||
## 목표
|
||||
|
||||
전투 화면 **하단에 카드 5장이 수평 일렬로 보이는** 시각 결과를 만든다.
|
||||
Slay the Spire 2 처럼 손패가 화면 아래쪽에 펼쳐진 느낌을 정적(static) 목업으로 구현한다.
|
||||
|
||||
## 범위
|
||||
|
||||
### 포함
|
||||
- `DefaultGroup.ui`에 카드 손패 UI 엔티티 추가
|
||||
- 카드 5장을 하단 중앙에 수평 일렬 배치
|
||||
- 각 카드는 "풀 카드 면": 에너지 코스트(좌상단) + 카드 이름(상단 중앙) + 설명(하단)
|
||||
- 샘플 손패 내용으로 채움 (정적)
|
||||
|
||||
### 명시적 제외 (YAGNI)
|
||||
- 클릭/터치 동작, 에너지 소모, 실제 카드 사용 로직
|
||||
- `SlayCombatManager` / `SlayCardCatalog` / `SlayRunState` 등 전투 로직 구현
|
||||
- 부채꼴(fan) 회전·곡선 배치
|
||||
- 드래그 앤 드롭, 호버 확대, 애니메이션
|
||||
- 데이터 기반 동적 손패 (드로우/버림에 따른 카드 수 변화)
|
||||
|
||||
이들은 이후 "데이터 연동" 단계에서 별도 스펙으로 다룬다.
|
||||
|
||||
## 구현 방식
|
||||
|
||||
`DefaultGroup.ui`의 `ContentProto.Entities` 배열에 신규 엔티티를 직접 추가한다.
|
||||
기존 MSW UI 엔티티 패턴(`uisprite`, `uitext`)을 그대로 따르고, 각 엔티티에 새 UUID를 부여한다.
|
||||
|
||||
이유:
|
||||
- 이 프로젝트는 로컬 워크스페이스 + git 방식이므로 `.ui` 파일 직접 편집이 형상관리와 일치
|
||||
- 결정론적·재현 가능하며 diff로 변경 내역이 명확히 남음
|
||||
- 작업 후 Maker에서 워크스페이스 reload로 반영
|
||||
|
||||
(대안 — Maker MCP 라이브 조작, 런타임 Lua 생성 — 은 재현성/범위 측면에서 부적합하여 제외)
|
||||
|
||||
## 엔티티 구조
|
||||
|
||||
```
|
||||
/ui/DefaultGroup/CardHand 컨테이너 (uiempty, 하단 중앙 앵커)
|
||||
├ Card1 (uisprite, 카드 면)
|
||||
│ ├ Cost (uitext, 좌상단 코스트)
|
||||
│ ├ Name (uitext, 상단 중앙 이름)
|
||||
│ └ Desc (uitext, 하단 설명)
|
||||
├ Card2 … Card5 (Card1과 동일한 하위 구조)
|
||||
```
|
||||
|
||||
## 배치 수치
|
||||
|
||||
- 좌표 공간: `DefaultGroup` 기준 1920 × 1080 (기존 UITransform과 동일)
|
||||
- `CardHand` 컨테이너: 하단 중앙 앵커 (AnchorsMin/Max = {0.5, 0}), RectSize 약 1020 × 300, 바닥에서 위로 약 30px 띄움
|
||||
- 카드 크기: 180 × 250 (폭 × 높이)
|
||||
- 카드 간격: 20px
|
||||
- 5장 총폭: `5 × 180 + 4 × 20 = 980px` → CardHand 내부에서 중앙 정렬
|
||||
- 카드 i의 x 중심 (컨테이너 중앙 기준): `(-2 + i) × 200` (i = 0..4) → -400, -200, 0, 200, 400
|
||||
|
||||
### 카드 내부 텍스트 배치 (카드 로컬 좌표, 180×250 기준)
|
||||
- Cost: 좌상단, RectSize 약 48×48, 카드 좌상단 모서리 부근
|
||||
- Name: 상단 중앙, FontSize 약 28
|
||||
- Desc: 하단, FontSize 약 22
|
||||
|
||||
## 비주얼
|
||||
|
||||
- 카드 면 배경: 기존 버튼 스프라이트 RUID `cd0560c4fc7f3b14994b90a502f00a21` 재사용
|
||||
- 카드 타입별 색 틴트 (SpriteGUIRendererComponent.Color):
|
||||
- 공격 카드(타격/강타): 붉은톤 (예: r 0.9, g 0.55, b 0.5)
|
||||
- 방어 카드(방어): 푸른톤 (예: r 0.55, g 0.7, b 0.95)
|
||||
- 텍스트 컴포넌트는 기존 `PopupMessage` TextComponent 스키마를 템플릿으로 사용 (FontColor, OutlineColor 등 기본값 유지)
|
||||
|
||||
## 샘플 손패 5장
|
||||
|
||||
| # | 이름 | 코스트 | 설명 | 타입 틴트 |
|
||||
|---|------|--------|----------|-----------|
|
||||
| 1 | 타격 | ① | 피해 6 | 공격(붉은) |
|
||||
| 2 | 타격 | ① | 피해 6 | 공격(붉은) |
|
||||
| 3 | 방어 | ① | 방어도 5 | 방어(푸른) |
|
||||
| 4 | 방어 | ① | 방어도 5 | 방어(푸른) |
|
||||
| 5 | 강타 | ② | 피해 10 | 공격(붉은) |
|
||||
|
||||
(StS 시작덱 느낌의 대표 손패. 코스트 표기는 텍스트 "1"/"2"로 입력)
|
||||
|
||||
## 검증
|
||||
|
||||
1. 파일 저장 후 Maker에서 로컬 워크스페이스 reload
|
||||
2. `maker_screenshot`으로 전투 화면 하단에 카드 5장이 수평 일렬로 렌더되는지 확인
|
||||
3. 각 카드에 코스트·이름·설명 텍스트가 보이는지, 공격/방어 색 구분이 되는지 확인
|
||||
|
||||
## 후속 단계 (이 스펙 밖)
|
||||
|
||||
- 데이터 연동: `SlayCardCatalog` / `SlayCombatManager` 구현 후 손패를 동적 렌더링
|
||||
- 카드 클릭 → 카드 사용, 에너지 소모, 손패 수 변화
|
||||
- 부채꼴 배치·호버·드래그 등 인터랙션 고도화
|
||||
@@ -1,59 +0,0 @@
|
||||
# 카드 슬롯에 이미지 카드 적용 설계
|
||||
|
||||
- 날짜: 2026-06-06
|
||||
- 브랜치: feature/sts2-combat-layout
|
||||
- 대상: `ui/DefaultGroup.ui`, `tools/gen-cardhand.mjs`
|
||||
- 원본 이미지: `C:\Users\jaeoh\Desktop\workspace\source\images\maple\invincible belief.png`
|
||||
|
||||
## 목표
|
||||
|
||||
하단 손패 5장 중 **5번 자리(현재 강타)** 의 카드 외형을 `invincible belief.png`(완성된 세로형 카드 이미지 "리부트 프로토콜")로 교체한다.
|
||||
|
||||
## 배경
|
||||
|
||||
해당 PNG는 단순 아트가 아니라 코스트·이름·타입·등급·아트·설명·플레이버까지 포함한 **완성된 카드 한 장 전체**(세로 약 2:3 비율)다. 따라서 슬롯의 외형 전체를 이 이미지로 대체하고, 그 슬롯에는 우리가 생성하던 텍스트 오버레이(코스트/이름/설명)를 넣지 않는다(이미지에 이미 포함 → 겹치면 중복/지저분).
|
||||
|
||||
## 범위
|
||||
|
||||
### 포함
|
||||
- PNG를 MSW 계정 sprite 리소스로 업로드 → RUID 발급
|
||||
- 생성기에 카드별 선택적 `image`(RUID) 데이터 추가
|
||||
- `image`가 있는 카드: 단색 배경 대신 해당 RUID 스프라이트(틴트 흰색)로 렌더, 텍스트 자식(Cost/Name/Desc) 생성 안 함
|
||||
- 5번 카드 크기를 이미지 비율에 맞춰 **180×270** 으로 조정(가로 유지, 세로만 +20), 행 중앙 정렬 유지
|
||||
- 나머지 4장은 기존 단색 목업 유지
|
||||
|
||||
### 제외 (YAGNI)
|
||||
- 나머지 4장의 이미지화
|
||||
- 카드 클릭/효과/실제 전투 로직
|
||||
- 이미지 카드의 동적 데이터 연동
|
||||
|
||||
## 구현 방식
|
||||
|
||||
### 1. 에셋 업로드 (`asset_create_account_resource_storage_item`, 2단계)
|
||||
- 1차 호출: `category=sprite`, `subcategory=etc`, `name`/`description` 지정, `contentLength`=파일 바이트 수, `fileUrl` 생략 → `presignedUrl` 수신
|
||||
- presignedUrl로 PNG를 HTTP PUT(raw 바이너리)
|
||||
- 2차 호출: `fileUrl=presignedUrl` → 리소스 생성 완료, 응답에서 **RUID(DataId)** 확보
|
||||
- 업로드 결과 RUID는 재현 가능하도록 생성기 스크립트에 하드코딩한다.
|
||||
|
||||
### 2. 생성기 변경 (`tools/gen-cardhand.mjs`)
|
||||
- `cards[4]`(강타 슬롯)에 `image: '<RUID>'` 필드 추가. (이름/코스트/설명 데이터는 남겨두되 image가 있으면 렌더에 사용하지 않음)
|
||||
- 카드 빌드 루프 수정:
|
||||
- 카드 배경 스프라이트: `image`가 있으면 `sprite({ dataId: image, color: {1,1,1,1}, type: 0 })`, 없으면 기존 `sprite({ color: tint, type: 1 })`
|
||||
- 카드 크기: `image`가 있으면 180×270, 없으면 180×250
|
||||
- 텍스트 자식(Cost/Name/Desc): `image`가 없을 때만 생성
|
||||
- 멱등성/줄바꿈 보존/splice 로직은 그대로 유지
|
||||
|
||||
### 3. 형상관리
|
||||
- 이미지는 MSW 클라우드 리소스로 저장되고, `.ui`·스크립트에는 **RUID 문자열만** 포함. PNG 원본은 slaymaple 저장소에 커밋하지 않는다(원본은 `workspace/source/images/maple/`에 유지).
|
||||
|
||||
## 검증
|
||||
|
||||
1. 업로드 응답에서 유효한 RUID 수신 확인
|
||||
2. 생성기 재실행 → JSON 유효, 5번 카드가 image 스프라이트 + 텍스트 자식 없음, 나머지 4장 불변
|
||||
3. Maker `refresh_workspace` → `play` → `screenshot`로 5번 자리에 "리부트 프로토콜" 카드 이미지가 왜곡 없이 표시되는지 확인
|
||||
4. (커밋 전) 디스크 무결성 확인 후 커밋
|
||||
|
||||
## 리스크
|
||||
|
||||
- 업로드한 sprite가 SpriteGUIRenderer에서 곧바로 렌더되는지(서브카테고리 무관 가정) — 검증 단계에서 확인, 안 되면 subcategory를 item 등으로 재시도
|
||||
- 카드 크기 180×270이 행 정렬에서 약간 위로 솟음 — 의도된 허용 범위
|
||||
@@ -1,64 +0,0 @@
|
||||
# 맵 개선: 다양한 preset 몬스터 + 맵별 타일셋 + StS2 배치 설계
|
||||
|
||||
- 날짜: 2026-06-06
|
||||
- 브랜치: feature/maps-batch (기존 맵 작업 이어서)
|
||||
- 대상: `tools/gen-maps.mjs`, `map/map02.map`~`map11.map` (재생성)
|
||||
|
||||
## 목표
|
||||
|
||||
map02~map11 각 맵에서:
|
||||
1. **다양한 몬스터** 2마리를 배치하되, 기존 map01의 4종(StaticMonster/MoveMonster/ChaseMonster/monster-43) **스프라이트를 재사용하지 않고**, 공식 맵에서 수확한 다양한 몬스터로 채운다.
|
||||
2. 몬스터를 **Slay the Spire 2 배치**(플레이어 좌측, 몬스터 우측 전투 포메이션)로 둔다.
|
||||
3. 맵마다 **다른 타일셋**(TileSetRUID)을 적용한다(같은 바닥 지형, 다른 타일 텍스처).
|
||||
4. 배경은 기존에 수확한 10종(맵별 상이) 유지.
|
||||
|
||||
## 범위
|
||||
|
||||
### 포함
|
||||
- 공식 맵 import로 **몬스터 변형 세트 + 타일셋 RUID** 수확
|
||||
- 생성기에 `MONSTER_VARIANTS`(수확 변형), `TILESETS`(타일셋 풀) 반영
|
||||
- 맵당 서로 다른 몬스터 2종, StS2 우측 배치
|
||||
- 맵당 다른 TileSetRUID
|
||||
- map02~map11 재생성
|
||||
|
||||
### 제외 (YAGNI)
|
||||
- 지형(Tiles/Foothold) 통째 교체 — 타일셋(텍스처)만 교체
|
||||
- 포털 연결, 카드-전투 로직 연동
|
||||
- map01 변경
|
||||
|
||||
## 수확 (공식 맵 import 기법)
|
||||
|
||||
배경 수확과 동일: `maker_import_maplestory_map(id)`가 현재 맵을 그 공식 맵으로 교체 → `maker_save` → `map/<current>.map`에서 데이터 추출.
|
||||
|
||||
- **몬스터 변형** `{ sprite, stand, hit, die }`(RUID): 몬스터가 있는 **필드/사냥맵**을 import해 몬스터 엔티티의 `SpriteRendererComponent.SpriteRUID` + `StateAnimationComponent.ActionSheet`(stand/hit/die)를 추출. ≥12종 distinct 목표. (타운맵은 몬스터 없을 수 있어 필드맵 선택)
|
||||
- **타일셋** `TileSetRUID`: import한 맵의 `TileMapComponent.TileSetRUID` 추출. 10종 distinct (map01의 `9dfea380…`과 겹치지 않게).
|
||||
|
||||
> **스파이크 선행**: 필드맵 1개를 import해 몬스터 엔티티 구조가 `{sprite, stand, hit, die}` 추출 가능한지 먼저 확인. 구조가 다르면 폴백(아래).
|
||||
|
||||
## 생성기 변경 (`tools/gen-maps.mjs`)
|
||||
|
||||
- `MONSTER_VARIANTS = [ {sprite, stand, hit, die}, ... ]` — 수확 결과로 채움(≥12종).
|
||||
- `TILESETS = [ ruid, ... ]` — 수확한 타일셋 10종.
|
||||
- `buildMap(nn)`:
|
||||
- 몬스터 2마리: `MONSTER_VARIANTS`에서 **서로 다른 2종**을 맵 시드로 선택(맵 내 중복 금지). 클론 몬스터 엔티티의 `SpriteRUID` + `ActionSheet`를 변형으로 덮어씀. (기존 map01 스프라이트 미사용 보장 — 항상 변형으로 덮어쓰므로)
|
||||
- 위치: **StS2 배치** — 화면 우측에 2자리 고정(예: Position.x ≈ +3.5, +5.5; y는 map01 몬스터 y값). map01 전투 구도를 기준으로 우측 포메이션.
|
||||
- 타일셋: `TileMap` 엔티티의 `TileMapComponent.TileSetRUID.DataId`를 `TILESETS[(nn-2)%len]`로 설정.
|
||||
- 배경: 기존 `BACKGROUNDS` 유지.
|
||||
- GUID 재발급·경로 치환·SectorConfig 로직은 그대로.
|
||||
|
||||
## 검증
|
||||
|
||||
1. **스파이크**(map02): reload→play→screenshot + Lua로
|
||||
- 몬스터 2마리의 `SpriteRUID`가 수확 변형과 일치(= map01 4종 아님), 우측 배치
|
||||
- `TileMap.TileSetRUID`가 새 타일셋
|
||||
- 화면상 몬스터 외형·타일 텍스처가 바뀌어 보임
|
||||
2. 전체: 맵별 몬스터 2종 distinct, 타일셋 distinct, 배경 distinct, 엔티티 id 중복 없음
|
||||
3. Maker 표본 맵 시각 확인
|
||||
|
||||
## 리스크/폴백
|
||||
- 몬스터 엔티티 구조가 `{sprite,stand,hit,die}`로 안 맞으면 → `SpriteRUID`만 교체하고 `ActionSheet`는 map01 템플릿 유지(최소 시각 변화 보장).
|
||||
- 타일셋 교체 시 tileIndex 의미 차이로 타일이 어색하면 → 스파이크에서 확인 후 호환 타일셋만 선별하거나 사용자와 상의.
|
||||
- 수확 시 import는 현재 맵(map02, 재생성 가능)에 적용 → 수확 후 generator로 map02 재생성하여 정리.
|
||||
|
||||
## 산출물/형상관리
|
||||
- `tools/gen-maps.mjs` 갱신, `map/map02.map`~`map11.map` 재생성을 커밋. 수확 RUID는 문자열만 포함(공식 콘텐츠).
|
||||
@@ -1,62 +0,0 @@
|
||||
# 맵 10개 생성 (랜덤 배경 + 몬스터 2마리) 설계
|
||||
|
||||
- 날짜: 2026-06-06
|
||||
- 브랜치: feature/maps-batch (신규)
|
||||
- 대상: `map/map02.map`~`map11.map`(신규), `Global/SectorConfig.config`, `tools/gen-maps.mjs`(신규)
|
||||
|
||||
## 목표
|
||||
|
||||
`map01`을 템플릿으로 **독립 맵 10개(`map02`~`map11`)** 를 생성한다. 각 맵은 **서로 다른 공식 배경**을 갖고, **몬스터 2마리**가 랜덤 위치에 배치된다.
|
||||
|
||||
## 범위
|
||||
|
||||
### 포함
|
||||
- `map02`~`map11` (신규 10개 맵 파일)
|
||||
- 맵마다 다른 배경(`BackgroundComponent.TemplateRUID`) — 공식 MapleStory 배경 라이브러리에서 10개 서로 다르게
|
||||
- 맵마다 몬스터 2마리, x 위치 랜덤(바닥 위), y는 바닥 높이 고정
|
||||
- `SectorConfig.config`에 `map://map02`~`map://map11` 등록
|
||||
- 재현용 생성기 `tools/gen-maps.mjs`
|
||||
|
||||
### 제외 (YAGNI)
|
||||
- 맵 간 포털/이동 연결 (독립 맵)
|
||||
- 맵별 다른 타일맵/지형 (map01 타일·바닥 그대로 복제)
|
||||
- 카드-전투 로직 연동
|
||||
- map01 변경
|
||||
|
||||
## 몬스터 전략 (스파이크 게이트)
|
||||
|
||||
사용자 선택: **라이브러리에서 다양한 몬스터**. 단, 리소스 검색이 RUID만 반환하고 action(stand/hit/die) 그룹핑·이름을 주지 않아 "완결된 몬스터" 조립이 불확실하다. 따라서:
|
||||
|
||||
- **A. 라이브러리 다양 몬스터 (1차 시도)**: 라이브러리에서 완결된 몬스터 2~3종(스프라이트 + stand/hit/die 애니메이션 RUID 세트)을 조립한다. **먼저 1개 맵으로 스파이크** → Maker Play에서 로드·렌더 검증.
|
||||
- **게이트**: 스파이크에서 라이브러리 몬스터가 정상 렌더되면 → 10개 맵에 A로 확대. 조립/로드 실패 시 → **B로 폴백**.
|
||||
- **B. 폴백 — 기존 몬스터 변형**: 이미 정상 로딩되는 기존 템플릿(StaticMonster/MoveMonster/ChaseMonster/monster-43)의 검증된 RUID 세트에서 맵당 랜덤 2종 + 랜덤 위치. 다양성은 ~4종으로 제한되지만 확실히 동작.
|
||||
|
||||
> 핵심 리스크: 이전에 **사용자 업로드(계정) 리소스는 로컬 워크스페이스 플레이에서 로드 실패**했다. 공식 라이브러리 리소스(배경/몬스터)는 shipped 콘텐츠라 로드될 것으로 보지만(기존 배경·몬스터 RUID가 정상 로딩 중), **확정 전 스파이크로 검증**한다.
|
||||
|
||||
## 구현 방식
|
||||
|
||||
### 생성기 `tools/gen-maps.mjs`
|
||||
1. `map/map01.map`을 텍스트로 읽어 JSON 파싱(템플릿)
|
||||
2. 배경 RUID 풀(10개, 공식 라이브러리에서 확보), 몬스터 정의 풀(A: 라이브러리 세트 / B: 기존 템플릿 세트)을 데이터로 보유
|
||||
3. `NN = 02..11` 각각:
|
||||
- 엔티티 deep-copy, **모든 엔티티 `id` GUID 재발급**(oldId→newId 매핑). `root_entity_id`/`sub_entity_id`가 엔티티 id를 가리키면 함께 치환. (component 안의 리소스 RUID — SpriteRUID, ActionSheet, TemplateRUID, CollisionGroup.Id, DamageSkinId, 타일셋 id — 는 엔티티 id가 아니므로 유지)
|
||||
- `EntryKey`를 `map://mapNN`, 모든 `path`의 `/maps/map01`→`/maps/mapNN`, `name`을 `mapNN`로 치환
|
||||
- `Background` 엔티티의 `TemplateRUID`를 `backgrounds[NN]`로 설정
|
||||
- 기존 몬스터 엔티티들을 제거하고, 선택된 몬스터 2종을 랜덤 x 위치로 추가(각 몬스터는 템플릿 몬스터 엔티티를 복제하고 SpriteRUID/ActionSheet[A] 또는 그대로[B] + Position.x 랜덤)
|
||||
- `map/mapNN.map`로 기록(원본 줄바꿈/포맷 보존 방식은 가능하면, 아니면 표준 JSON 직렬화)
|
||||
4. `Global/SectorConfig.config`의 `Sectors[0].entries`에 `map://map02`~`map11` 추가(중복 방지)
|
||||
|
||||
랜덤은 결정론을 위해 인덱스 기반 시드(맵 번호로 위치/선택 산출) 사용 — 재실행 시 동일 결과.
|
||||
|
||||
### GUID 재발급 주의
|
||||
- 엔티티 id 충돌 방지를 위해 맵마다 고유 GUID 필요. 자기참조(`root_entity_id`==자기 id)는 매핑으로 일관되게 치환.
|
||||
|
||||
## 검증
|
||||
|
||||
1. **스파이크(A)**: map02 1개만 생성 → reload→play→screenshot + Lua로 몬스터 엔티티/스프라이트 로드 확인. 실패 시 B로 전환.
|
||||
2. 전체 생성 후: 각 맵(또는 표본)에서 reload→play(해당 맵)→screenshot으로 배경 상이·몬스터 2마리 확인. 맵 전환은 Maker에서 해당 맵을 열거나 sector 이동으로.
|
||||
3. JSON 유효성(JSON.parse) + SectorConfig 10개 등록 확인 + 엔티티 id 중복 없음 확인.
|
||||
|
||||
## 산출물/형상관리
|
||||
- 신규 파일 `map/map02.map`~`map11.map`, `tools/gen-maps.mjs`, `SectorConfig.config` 변경을 커밋.
|
||||
- 배경/몬스터는 공식 라이브러리 RUID(문자열)만 참조 — 별도 리소스 파일 불필요(공식 콘텐츠). (단 A가 로컬 임포트를 요구하면 그 리소스 파일도 포함)
|
||||
@@ -1,37 +0,0 @@
|
||||
# 덱 컨트롤러 코드리뷰 수정 설계
|
||||
|
||||
- 날짜: 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)
|
||||
적 턴, 카드 효과의 실제 전투 적용, 신규 카드 종류.
|
||||
54
docs/ui-generation-structure.md
Normal file
54
docs/ui-generation-structure.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# UI Generation Structure
|
||||
|
||||
## Current Rule
|
||||
|
||||
Do not hand-edit `ui/DefaultGroup.ui` for SlayDeck UI changes. It is generated
|
||||
by `tools/deck/gen-slaydeck.mjs`.
|
||||
|
||||
The `.ui` file is expected to stay large because MapleStory Worlds stores UI as
|
||||
one JSON entity list. The maintainable source is the generator, not the output.
|
||||
|
||||
## Generated Sections
|
||||
|
||||
`tools/deck/gen-slaydeck.mjs` centralizes generated UI roots in
|
||||
`GENERATED_UI_SECTIONS`:
|
||||
|
||||
- `DeckHud`
|
||||
- `DeckInspectHud`
|
||||
- `DeckAllHud`
|
||||
- `CombatHud`
|
||||
- `RewardHud`
|
||||
- `MapHud`
|
||||
- `ShopHud`
|
||||
- `RestHud`
|
||||
- `MainMenu`
|
||||
- `CharacterSelectHud`
|
||||
|
||||
When the generator runs, existing entities under those roots are removed and
|
||||
emitted again. Stock mobile controls are handled separately by
|
||||
`DISABLED_STOCK_CONTROLS`.
|
||||
|
||||
## How To Change UI
|
||||
|
||||
1. Edit the relevant section in `tools/deck/gen-slaydeck.mjs`.
|
||||
2. Add new top-level HUD roots to `GENERATED_UI_SECTIONS`.
|
||||
3. Emit section entities with `emit('SectionName', entities)`.
|
||||
4. Run `node tools/deck/gen-slaydeck.mjs`.
|
||||
5. Verify JSON parsing for generated files.
|
||||
|
||||
`emit()` validates that a section only emits paths under its own root. This
|
||||
keeps accidental cross-section UI changes from silently landing in
|
||||
`DefaultGroup.ui`.
|
||||
|
||||
## Next Refactor Target
|
||||
|
||||
The next useful split is to move each large section builder into separate files,
|
||||
for example:
|
||||
|
||||
- `tools/deck/ui/combat-hud.mjs`
|
||||
- `tools/deck/ui/shop-hud.mjs`
|
||||
- `tools/deck/ui/map-hud.mjs`
|
||||
- `tools/deck/ui/menu-hud.mjs`
|
||||
|
||||
Keep shared helpers like `entity`, `transform`, `sprite`, `button`, and `text`
|
||||
in one shared UI helper module.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "map://map08",
|
||||
"EntryKey": "map://lobby",
|
||||
"ContentType": "x-mod/map",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
@@ -14,12 +14,12 @@
|
||||
"Use": "Binary",
|
||||
"Entities": [
|
||||
{
|
||||
"id": "00001f40-0000-4000-8000-000000001f40",
|
||||
"path": "/maps/map08",
|
||||
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent",
|
||||
"id": "000dbba0-0000-4000-8000-0000000dbba0",
|
||||
"path": "/maps/lobby",
|
||||
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.LobbyMobility",
|
||||
"jsonString": {
|
||||
"name": "map08",
|
||||
"path": "/maps/map08",
|
||||
"name": "lobby",
|
||||
"path": "/maps/lobby",
|
||||
"nameEditable": false,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -1103,18 +1103,22 @@
|
||||
]
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.LobbyMobility",
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001f41-0000-4000-8000-000000001f41",
|
||||
"path": "/maps/map08/Background",
|
||||
"id": "000dbba1-0000-4000-8000-0000000dbba1",
|
||||
"path": "/maps/lobby/Background",
|
||||
"componentNames": "MOD.Core.BackgroundComponent",
|
||||
"jsonString": {
|
||||
"name": "Background",
|
||||
"path": "/maps/map08/Background",
|
||||
"path": "/maps/lobby/Background",
|
||||
"nameEditable": false,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -1132,7 +1136,7 @@
|
||||
"b": 0.5019608,
|
||||
"a": 0.7058824
|
||||
},
|
||||
"TemplateRUID": "0c398bbb2cf6400992532465b9d53024",
|
||||
"TemplateRUID": "65c4167ea7484196b890022354e5a4a4",
|
||||
"Type": 1,
|
||||
"WebUrl": "eab37efa7f0d400f94259a2df836eb8a",
|
||||
"Enable": true
|
||||
@@ -1142,12 +1146,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001f42-0000-4000-8000-000000001f42",
|
||||
"path": "/maps/map08/MapleMapLayer",
|
||||
"id": "000dbba2-0000-4000-8000-0000000dbba2",
|
||||
"path": "/maps/lobby/MapleMapLayer",
|
||||
"componentNames": "MOD.Core.MapLayerComponent",
|
||||
"jsonString": {
|
||||
"name": "MapleMapLayer",
|
||||
"path": "/maps/map08/MapleMapLayer",
|
||||
"path": "/maps/lobby/MapleMapLayer",
|
||||
"nameEditable": false,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -1178,12 +1182,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001f43-0000-4000-8000-000000001f43",
|
||||
"path": "/maps/map08/TileMap",
|
||||
"id": "000dbba3-0000-4000-8000-0000000dbba3",
|
||||
"path": "/maps/lobby/TileMap",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.TileMapComponent",
|
||||
"jsonString": {
|
||||
"name": "TileMap",
|
||||
"path": "/maps/map08/TileMap",
|
||||
"path": "/maps/lobby/TileMap",
|
||||
"nameEditable": false,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -1237,7 +1241,7 @@
|
||||
"SortingLayer": "MapLayer0",
|
||||
"TileMapVersion": 1,
|
||||
"TileSetRUID": {
|
||||
"DataId": "2667829326dd46de80ef26f6bb7f26ae"
|
||||
"DataId": "9dfea3808bbd49a5877d8624df21b1c7"
|
||||
},
|
||||
"Tiles": [
|
||||
{
|
||||
@@ -6280,12 +6284,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001f44-0000-4000-8000-000000001f44",
|
||||
"path": "/maps/map08/SpawnLocation",
|
||||
"id": "000dbba4-0000-4000-8000-0000000dbba4",
|
||||
"path": "/maps/lobby/SpawnLocation",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.SpriteRendererComponent,MOD.Core.SpawnLocationComponent",
|
||||
"jsonString": {
|
||||
"name": "SpawnLocation",
|
||||
"path": "/maps/map08/SpawnLocation",
|
||||
"path": "/maps/lobby/SpawnLocation",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6356,12 +6360,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001f45-0000-4000-8000-000000001f45",
|
||||
"path": "/maps/map08/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",
|
||||
"id": "000dbba5-0000-4000-8000-0000000dbba5",
|
||||
"path": "/maps/lobby/NpcRun",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent,MOD.Core.TouchReceiveComponent,script.LobbyNpc",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map08/Monster1",
|
||||
"name": "NpcRun",
|
||||
"path": "/maps/lobby/NpcRun",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6371,17 +6375,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00001f45-0000-4000-8000-000000001f45",
|
||||
"root_entity_id": "000dbba5-0000-4000-8000-0000000dbba5",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"x": -3,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6401,9 +6405,9 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "ed3908e24d694bb786023fc1ed073489",
|
||||
"hit": "4763c9bebc9245998c9c499b6316aa9f",
|
||||
"die": "b168793b92a844a3a3a6f4ce647a14d2"
|
||||
"stand": "122095fd155c4633867b0da4f375bc3c",
|
||||
"hit": "122095fd155c4633867b0da4f375bc3c",
|
||||
"die": "122095fd155c4633867b0da4f375bc3c"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6413,102 +6417,37 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "ed3908e24d694bb786023fc1ed073489",
|
||||
"SpriteRUID": "122095fd155c4633867b0da4f375bc3c",
|
||||
"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": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"@type": "MOD.Core.TouchReceiveComponent",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
"AutoFitToSize": true
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"@type": "script.LobbyNpc",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
"NpcId": "run",
|
||||
"MarkName": "NpcRunMark"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001f46-0000-4000-8000-000000001f46",
|
||||
"path": "/maps/map08/Monster2",
|
||||
"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",
|
||||
"id": "000dbba6-0000-4000-8000-0000000dbba6",
|
||||
"path": "/maps/lobby/NpcRunMark",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map08/Monster2",
|
||||
"name": "NpcRunMark",
|
||||
"path": "/maps/lobby/NpcRunMark",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6518,17 +6457,88 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00001f46-0000-4000-8000-000000001f46",
|
||||
"root_entity_id": "000dbba6-0000-4000-8000-0000000dbba6",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"x": -3,
|
||||
"y": 1.6349999800000001,
|
||||
"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": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"hit": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"die": "bd4afdde295f40318fceb4166978ebaa"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000dbba7-0000-4000-8000-0000000dbba7",
|
||||
"path": "/maps/lobby/NpcCodex",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent,MOD.Core.TouchReceiveComponent,script.LobbyNpc",
|
||||
"jsonString": {
|
||||
"name": "NpcCodex",
|
||||
"path": "/maps/lobby/NpcCodex",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000dbba7-0000-4000-8000-0000000dbba7",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": -0.5,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6548,9 +6558,9 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"hit": "c3cf643b618346c7bfa6574187b396f9",
|
||||
"die": "a88d9b3d60f941e4890dc89a6ccaa8ee"
|
||||
"stand": "4c264be6a64f4ac3970b2e6818d04e40",
|
||||
"hit": "4c264be6a64f4ac3970b2e6818d04e40",
|
||||
"die": "4c264be6a64f4ac3970b2e6818d04e40"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6560,90 +6570,402 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"SpriteRUID": "4c264be6a64f4ac3970b2e6818d04e40",
|
||||
"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": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"@type": "MOD.Core.TouchReceiveComponent",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
"AutoFitToSize": true
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"@type": "script.LobbyNpc",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
"NpcId": "codex",
|
||||
"MarkName": "NpcCodexMark"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000dbba8-0000-4000-8000-0000000dbba8",
|
||||
"path": "/maps/lobby/NpcCodexMark",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent",
|
||||
"jsonString": {
|
||||
"name": "NpcCodexMark",
|
||||
"path": "/maps/lobby/NpcCodexMark",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000dbba8-0000-4000-8000-0000000dbba8",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": -0.5,
|
||||
"y": 1.6349999800000001,
|
||||
"z": 999.999
|
||||
},
|
||||
"PositionOffset": {
|
||||
"QuaternionRotation": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"Scale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"hit": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"die": "bd4afdde295f40318fceb4166978ebaa"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000dbba9-0000-4000-8000-0000000dbba9",
|
||||
"path": "/maps/lobby/NpcShop",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent,MOD.Core.TouchReceiveComponent,script.LobbyNpc",
|
||||
"jsonString": {
|
||||
"name": "NpcShop",
|
||||
"path": "/maps/lobby/NpcShop",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000dbba9-0000-4000-8000-0000000dbba9",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 2,
|
||||
"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": "69987ccdc486423f8bedd786bd6cb5d9",
|
||||
"hit": "69987ccdc486423f8bedd786bd6cb5d9",
|
||||
"die": "69987ccdc486423f8bedd786bd6cb5d9"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "69987ccdc486423f8bedd786bd6cb5d9",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.TouchReceiveComponent",
|
||||
"Enable": true,
|
||||
"AutoFitToSize": true
|
||||
},
|
||||
{
|
||||
"@type": "script.LobbyNpc",
|
||||
"Enable": true,
|
||||
"NpcId": "shop",
|
||||
"MarkName": "NpcShopMark"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000dbbaa-0000-4000-8000-0000000dbbaa",
|
||||
"path": "/maps/lobby/NpcShopMark",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent",
|
||||
"jsonString": {
|
||||
"name": "NpcShopMark",
|
||||
"path": "/maps/lobby/NpcShopMark",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000dbbaa-0000-4000-8000-0000000dbbaa",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 2,
|
||||
"y": 1.6349999800000001,
|
||||
"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": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"hit": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"die": "bd4afdde295f40318fceb4166978ebaa"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000dbbab-0000-4000-8000-0000000dbbab",
|
||||
"path": "/maps/lobby/NpcBoard",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent,MOD.Core.TouchReceiveComponent,script.LobbyNpc",
|
||||
"jsonString": {
|
||||
"name": "NpcBoard",
|
||||
"path": "/maps/lobby/NpcBoard",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000dbbab-0000-4000-8000-0000000dbbab",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 4.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": "8a99bd87d667482cb1f3b2193f8a19c1",
|
||||
"hit": "8a99bd87d667482cb1f3b2193f8a19c1",
|
||||
"die": "8a99bd87d667482cb1f3b2193f8a19c1"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "8a99bd87d667482cb1f3b2193f8a19c1",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.TouchReceiveComponent",
|
||||
"Enable": true,
|
||||
"AutoFitToSize": true
|
||||
},
|
||||
{
|
||||
"@type": "script.LobbyNpc",
|
||||
"Enable": true,
|
||||
"NpcId": "board",
|
||||
"MarkName": "NpcBoardMark"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000dbbac-0000-4000-8000-0000000dbbac",
|
||||
"path": "/maps/lobby/NpcBoardMark",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.StateComponent",
|
||||
"jsonString": {
|
||||
"name": "NpcBoardMark",
|
||||
"path": "/maps/lobby/NpcBoardMark",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000dbbac-0000-4000-8000-0000000dbbac",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 4.5,
|
||||
"y": 1.6349999800000001,
|
||||
"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": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"hit": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"die": "bd4afdde295f40318fceb4166978ebaa"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "bd4afdde295f40318fceb4166978ebaa",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
1687
map/map01.map
1687
map/map01.map
File diff suppressed because it is too large
Load Diff
1560
map/map02.map
1560
map/map02.map
File diff suppressed because it is too large
Load Diff
864
map/map03.map
864
map/map03.map
@@ -16,7 +16,7 @@
|
||||
{
|
||||
"id": "00000bb8-0000-4000-8000-000000000bb8",
|
||||
"path": "/maps/map03",
|
||||
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent",
|
||||
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock",
|
||||
"jsonString": {
|
||||
"name": "map03",
|
||||
"path": "/maps/map03",
|
||||
@@ -1103,6 +1103,14 @@
|
||||
]
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.MapCamera",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.PlayerLock",
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6356,12 +6364,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000bbd-0000-4000-8000-000000000bbd",
|
||||
"path": "/maps/map03/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",
|
||||
"id": "00000dac-0000-4000-8000-000000000dac",
|
||||
"path": "/maps/map03/combat_1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map03/Monster1",
|
||||
"name": "combat_1",
|
||||
"path": "/maps/map03/combat_1",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6371,17 +6379,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000bbd-0000-4000-8000-000000000bbd",
|
||||
"root_entity_id": "00000dac-0000-4000-8000-000000000dac",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"x": 2.3,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6401,9 +6409,9 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"hit": "c3cf643b618346c7bfa6574187b396f9",
|
||||
"die": "a88d9b3d60f941e4890dc89a6ccaa8ee"
|
||||
"stand": "ed3908e24d694bb786023fc1ed073489",
|
||||
"hit": "4763c9bebc9245998c9c499b6316aa9f",
|
||||
"die": "b168793b92a844a3a3a6f4ce647a14d2"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6413,42 +6421,10 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"SpriteRUID": "ed3908e24d694bb786023fc1ed073489",
|
||||
"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": {
|
||||
@@ -6461,24 +6437,32 @@
|
||||
},
|
||||
"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",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
@@ -6497,18 +6481,47 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "pig",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000bbe-0000-4000-8000-000000000bbe",
|
||||
"path": "/maps/map03/Monster2",
|
||||
"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",
|
||||
"id": "00000dad-0000-4000-8000-000000000dad",
|
||||
"path": "/maps/map03/combat_2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map03/Monster2",
|
||||
"name": "combat_2",
|
||||
"path": "/maps/map03/combat_2",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6518,17 +6531,169 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000bbe-0000-4000-8000-000000000bbe",
|
||||
"root_entity_id": "00000dad-0000-4000-8000-000000000dad",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"x": 3.8,
|
||||
"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": "a2204a21d88942b281d2cac6053ffbaa",
|
||||
"hit": "afc08936b8a64b26bc3dd8c03ead1f26",
|
||||
"die": "fc1c6d9ba9bc413ab53b6dbfae3ac45b"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "a2204a21d88942b281d2cac6053ffbaa",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "red_snail",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000dae-0000-4000-8000-000000000dae",
|
||||
"path": "/maps/map03/combat_3",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "combat_3",
|
||||
"path": "/maps/map03/combat_3",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000dae-0000-4000-8000-000000000dae",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.2,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6564,38 +6729,6 @@
|
||||
"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": {
|
||||
@@ -6608,24 +6741,32 @@
|
||||
},
|
||||
"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",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
@@ -6644,6 +6785,491 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000daf-0000-4000-8000-000000000daf",
|
||||
"path": "/maps/map03/elite_4",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "elite_4",
|
||||
"path": "/maps/map03/elite_4",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000daf-0000-4000-8000-000000000daf",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3,
|
||||
"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": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"hit": "c3cf643b618346c7bfa6574187b396f9",
|
||||
"die": "a88d9b3d60f941e4890dc89a6ccaa8ee"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "modified_snail",
|
||||
"Group": "elite"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000db0-0000-4000-8000-000000000db0",
|
||||
"path": "/maps/map03/elite_5",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "elite_5",
|
||||
"path": "/maps/map03/elite_5",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000db0-0000-4000-8000-000000000db0",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 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": "4ca39dbfa1c6492283ba8bd352d12b0a",
|
||||
"hit": "7ac78511036e4ebe988b97c35fc275d1",
|
||||
"die": "740f3f2b2e7a4b71bec5eac84e8539f9"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "4ca39dbfa1c6492283ba8bd352d12b0a",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "mushmom",
|
||||
"Group": "elite"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000db1-0000-4000-8000-000000000db1",
|
||||
"path": "/maps/map03/boss_6",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "boss_6",
|
||||
"path": "/maps/map03/boss_6",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000db1-0000-4000-8000-000000000db1",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 4,
|
||||
"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": "f86992ba9c41487c8480fcb893fcbda6",
|
||||
"hit": "d305b942b1704c8084548108ff3b7a6b",
|
||||
"die": "5a563e5fd98c4132b61057dc6bb8aaf2"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "f86992ba9c41487c8480fcb893fcbda6",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "slime_boss",
|
||||
"Group": "boss"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
864
map/map04.map
864
map/map04.map
@@ -16,7 +16,7 @@
|
||||
{
|
||||
"id": "00000fa0-0000-4000-8000-000000000fa0",
|
||||
"path": "/maps/map04",
|
||||
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent",
|
||||
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock",
|
||||
"jsonString": {
|
||||
"name": "map04",
|
||||
"path": "/maps/map04",
|
||||
@@ -1103,6 +1103,14 @@
|
||||
]
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.MapCamera",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.PlayerLock",
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6356,12 +6364,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000fa5-0000-4000-8000-000000000fa5",
|
||||
"path": "/maps/map04/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",
|
||||
"id": "00001194-0000-4000-8000-000000001194",
|
||||
"path": "/maps/map04/combat_1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map04/Monster1",
|
||||
"name": "combat_1",
|
||||
"path": "/maps/map04/combat_1",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6371,17 +6379,473 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000fa5-0000-4000-8000-000000000fa5",
|
||||
"root_entity_id": "00001194-0000-4000-8000-000000001194",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.5,
|
||||
"x": 2.3,
|
||||
"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": "f86992ba9c41487c8480fcb893fcbda6",
|
||||
"hit": "d305b942b1704c8084548108ff3b7a6b",
|
||||
"die": "5a563e5fd98c4132b61057dc6bb8aaf2"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "f86992ba9c41487c8480fcb893fcbda6",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "blue_mushroom",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001195-0000-4000-8000-000000001195",
|
||||
"path": "/maps/map04/combat_2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "combat_2",
|
||||
"path": "/maps/map04/combat_2",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00001195-0000-4000-8000-000000001195",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.8,
|
||||
"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": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"hit": "c3cf643b618346c7bfa6574187b396f9",
|
||||
"die": "a88d9b3d60f941e4890dc89a6ccaa8ee"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "stump",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001196-0000-4000-8000-000000001196",
|
||||
"path": "/maps/map04/combat_3",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "combat_3",
|
||||
"path": "/maps/map04/combat_3",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00001196-0000-4000-8000-000000001196",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.2,
|
||||
"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": "a2204a21d88942b281d2cac6053ffbaa",
|
||||
"hit": "afc08936b8a64b26bc3dd8c03ead1f26",
|
||||
"die": "fc1c6d9ba9bc413ab53b6dbfae3ac45b"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "a2204a21d88942b281d2cac6053ffbaa",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "green_mushroom",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001197-0000-4000-8000-000000001197",
|
||||
"path": "/maps/map04/elite_4",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "elite_4",
|
||||
"path": "/maps/map04/elite_4",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00001197-0000-4000-8000-000000001197",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6417,38 +6881,6 @@
|
||||
"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": {
|
||||
@@ -6461,24 +6893,32 @@
|
||||
},
|
||||
"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",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
@@ -6497,18 +6937,47 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "modified_snail",
|
||||
"Group": "elite"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000fa6-0000-4000-8000-000000000fa6",
|
||||
"path": "/maps/map04/Monster2",
|
||||
"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",
|
||||
"id": "00001198-0000-4000-8000-000000001198",
|
||||
"path": "/maps/map04/elite_5",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map04/Monster2",
|
||||
"name": "elite_5",
|
||||
"path": "/maps/map04/elite_5",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6518,17 +6987,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "StaticMonster",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00000fa6-0000-4000-8000-000000000fa6",
|
||||
"root_entity_id": "00001198-0000-4000-8000-000000001198",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "staticmonster",
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.5,
|
||||
"x": 5,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6548,9 +7017,9 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "3109357701ae41a4bcc7543f52f1f4c3",
|
||||
"hit": "ce0269079e884545b5bb6ea075e2a67f",
|
||||
"die": "a5e65650e00e47878cac1be7a5b999a0"
|
||||
"stand": "ed3908e24d694bb786023fc1ed073489",
|
||||
"hit": "4763c9bebc9245998c9c499b6316aa9f",
|
||||
"die": "b168793b92a844a3a3a6f4ce647a14d2"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6560,42 +7029,10 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "3109357701ae41a4bcc7543f52f1f4c3",
|
||||
"SpriteRUID": "ed3908e24d694bb786023fc1ed073489",
|
||||
"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": {
|
||||
@@ -6608,24 +7045,32 @@
|
||||
},
|
||||
"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",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
@@ -6644,6 +7089,187 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "mushmom",
|
||||
"Group": "elite"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00001199-0000-4000-8000-000000001199",
|
||||
"path": "/maps/map04/boss_6",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "boss_6",
|
||||
"path": "/maps/map04/boss_6",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "00001199-0000-4000-8000-000000001199",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 4,
|
||||
"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": "48c10437ae8344a9b2a1d3f36185728f",
|
||||
"hit": "9044063647854f5e9128efcf80e909be",
|
||||
"die": "f414577d18c94cc387c275df4abdbc3b"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "48c10437ae8344a9b2a1d3f36185728f",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "slime_boss",
|
||||
"Group": "boss"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
1560
map/map05.map
1560
map/map05.map
File diff suppressed because it is too large
Load Diff
6654
map/map06.map
6654
map/map06.map
File diff suppressed because it is too large
Load Diff
6654
map/map07.map
6654
map/map07.map
File diff suppressed because it is too large
Load Diff
6654
map/map09.map
6654
map/map09.map
File diff suppressed because it is too large
Load Diff
6654
map/map10.map
6654
map/map10.map
File diff suppressed because it is too large
Load Diff
6654
map/map11.map
6654
map/map11.map
File diff suppressed because it is too large
Load Diff
369
tools/balance/sim-balance.mjs
Normal file
369
tools/balance/sim-balance.mjs
Normal file
@@ -0,0 +1,369 @@
|
||||
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
|
||||
// ⚠️ 전투 규칙은 tools/deck/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;
|
||||
}
|
||||
|
||||
// 공격 피해 공식 — Lua CalcPlayerAttack(힘·약화) + DealDamageToTarget(취약)과 동기화.
|
||||
// floor((base + str) * (weak>0 ? 0.75 : 1)) → floor(... * (vulnOnTarget>0 ? 1.5 : 1))
|
||||
// 보상 카드 등급 추첨 (Lua OfferReward 미러) — roll ∈ 1..100, normal 70 / unique 25 / legend 5
|
||||
export function rarityForRoll(roll) {
|
||||
if (roll > 95) return 'legend';
|
||||
if (roll > 70) return 'unique';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
export function calcAttack(base, str, weak, vulnOnTarget) {
|
||||
let dmg = base + str;
|
||||
if (weak > 0) dmg = Math.floor(dmg * 0.75);
|
||||
if (vulnOnTarget > 0) dmg = Math.floor(dmg * 1.5);
|
||||
if (dmg < 0) dmg = 0;
|
||||
return dmg;
|
||||
}
|
||||
|
||||
// 방어 우선 차감 후 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 ids = enemiesData.simEncounter || [enemiesData.activeEnemy];
|
||||
const monsters = ids.map((id) => {
|
||||
const e = enemiesData.enemies[id];
|
||||
if (!e) throw new Error(`simEncounter 적 없음: ${id}`);
|
||||
return { name: e.name, maxHp: e.maxHp, intents: e.intents };
|
||||
});
|
||||
// 시뮬 기본 덱은 전사 시작 덱 (클래스별 시뮬은 starterDeck 직접 주입으로 가능)
|
||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDecks.warrior, monsters };
|
||||
}
|
||||
|
||||
// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
||||
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
|
||||
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
|
||||
export function chooseAction(hand, cards, energy) {
|
||||
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id] && cards[x.id].cost <= energy && !cards[x.id].unplayable);
|
||||
const powers = entries.filter((x) => cards[x.id].kind === 'Power');
|
||||
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) / Math.max(cards[x.id].cost, 1);
|
||||
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(cards[x.id].cost, 1);
|
||||
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
|
||||
if (powers.length) return powers[0].i;
|
||||
if (attacks.length) return bestBy(attacks, dmgEff).i;
|
||||
if (skills.length) return bestBy(skills, blkEff).i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 공격 타겟 선택: 이번 타격으로 처치 가능한 최소 유효체력, 없으면 유효체력 최소.
|
||||
export function chooseTarget(aliveMonsters, plannedDamage) {
|
||||
const eff = (m) => m.hp + m.block;
|
||||
const killable = aliveMonsters.filter((m) => eff(m) <= plannedDamage);
|
||||
const pool = killable.length ? killable : aliveMonsters;
|
||||
return pool.slice().sort((a, b) => eff(a) - eff(b) || pool.indexOf(a) - pool.indexOf(b))[0];
|
||||
}
|
||||
|
||||
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, monsters } = data;
|
||||
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: PLAYER_HP };
|
||||
let drawPile = shuffle(starterDeck, rng);
|
||||
let discard = [];
|
||||
const exhaust = [];
|
||||
let hand = [];
|
||||
let pHp = PLAYER_HP, pBlock = 0;
|
||||
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0;
|
||||
const powers = [];
|
||||
const mob = monsters.map((m) => ({
|
||||
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: 0, weak: 0, vuln: 0, poison: 0,
|
||||
intents: m.intents, intentIdx: 0, alive: true,
|
||||
}));
|
||||
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;
|
||||
const card = drawPile.pop();
|
||||
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
|
||||
if (hand.length >= 10) {
|
||||
discard.push(card);
|
||||
triggerSly(card);
|
||||
} else hand.push(card);
|
||||
}
|
||||
}
|
||||
function addCardsToHand(id, n) {
|
||||
for (let k = 0; k < n; k++) {
|
||||
if (hand.length >= 10) discard.push(id);
|
||||
else hand.push(id);
|
||||
}
|
||||
}
|
||||
const aliveList = () => mob.filter((m) => m.alive);
|
||||
function resolveCardEffects(id, c, costSpent, recordStats = true) {
|
||||
const alive = aliveList();
|
||||
let dmg = 0;
|
||||
let blockGained = 0;
|
||||
if (c.kind === 'Attack') {
|
||||
if (alive.length && c.damage) {
|
||||
const target = chooseTarget(alive, calcAttack(c.damage || 0, pStr, pWeak, 0));
|
||||
if (c.weak) target.weak += c.weak;
|
||||
if (c.vuln) target.vuln += c.vuln;
|
||||
const hitN = c.hits || 1;
|
||||
let totalNv = 0;
|
||||
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
|
||||
dmg = totalNv;
|
||||
if (c.aoe === true) {
|
||||
for (const m2 of aliveList()) {
|
||||
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
const r2 = applyDamage(m2.hp, m2.block, d2);
|
||||
m2.hp = r2.hp; m2.block = r2.block;
|
||||
if (m2.hp <= 0) m2.alive = false;
|
||||
}
|
||||
} else {
|
||||
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
if (c.pierce === true) {
|
||||
target.hp -= dmg;
|
||||
if (target.hp < 0) target.hp = 0;
|
||||
} else {
|
||||
const r = applyDamage(target.hp, target.block, dmg);
|
||||
target.hp = r.hp; target.block = r.block;
|
||||
}
|
||||
if (target.hp <= 0) target.alive = false;
|
||||
}
|
||||
}
|
||||
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
|
||||
} else if (c.kind === 'Power') {
|
||||
if (recordStats) powers.push(id);
|
||||
} else {
|
||||
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
|
||||
if ((c.weak || c.vuln || c.poison) && alive.length) {
|
||||
const target = chooseTarget(alive, 0);
|
||||
if (c.weak) target.weak += c.weak;
|
||||
if (c.vuln) target.vuln += c.vuln;
|
||||
if (c.poison) target.poison += c.poison;
|
||||
}
|
||||
}
|
||||
if (c.strength) pStr += c.strength;
|
||||
if (c.dex) pDex += c.dex;
|
||||
if (c.thorns) pThorns += c.thorns;
|
||||
if (c.selfVuln) pVuln += c.selfVuln;
|
||||
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
||||
if (c.draw) draw(c.draw);
|
||||
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
|
||||
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
|
||||
}
|
||||
function triggerSly(id) {
|
||||
const c = cards[id];
|
||||
if (!c?.sly) return;
|
||||
resolveCardEffects(id, c, 0, false);
|
||||
}
|
||||
function discardHandCard(idx, trigger = true) {
|
||||
const [id] = hand.splice(idx, 1);
|
||||
if (!id) return;
|
||||
discard.push(id);
|
||||
if (trigger) triggerSly(id);
|
||||
}
|
||||
function applyDiscardEffects(c) {
|
||||
let discarded = 0;
|
||||
if (c.discardAll) {
|
||||
while (hand.length) { discardHandCard(hand.length - 1, true); discarded++; }
|
||||
} else if (c.discard) {
|
||||
const n = Math.min(c.discard, hand.length);
|
||||
for (let i = 0; i < n; i++) { discardHandCard(hand.length - 1, true); discarded++; }
|
||||
}
|
||||
if (c.addShiv && (c.discard || c.discardAll === true)) addCardsToHand('Shiv', c.addShiv);
|
||||
if (c.addShivPerDiscard === true) addCardsToHand('Shiv', discarded);
|
||||
}
|
||||
|
||||
while (turns < MAX_TURNS) {
|
||||
turns++;
|
||||
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
|
||||
pBlock = 0;
|
||||
let energyBonus = 0;
|
||||
for (const pid of powers) {
|
||||
const pc = cards[pid];
|
||||
if (!pc) continue;
|
||||
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
|
||||
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
|
||||
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
|
||||
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
|
||||
}
|
||||
let energy = ENERGY + energyBonus; draw(HAND_SIZE);
|
||||
while (true) {
|
||||
const alive = aliveList();
|
||||
if (alive.length === 0) break;
|
||||
const idx = chooseAction(hand, cards, energy);
|
||||
if (idx < 0) break;
|
||||
const id = hand[idx], c = cards[id];
|
||||
energy -= c.cost;
|
||||
resolveCardEffects(id, c, c.cost);
|
||||
hand.splice(idx, 1);
|
||||
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
|
||||
else if (c.kind !== 'Power') discard.push(id);
|
||||
applyDiscardEffects(c);
|
||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
|
||||
}
|
||||
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
|
||||
let burn = 0;
|
||||
for (const hid of hand) { const hc = cards[hid]; if (hc && hc.endTurnDamage) burn += hc.endTurnDamage; }
|
||||
if (burn > 0) { pHp -= burn; if (pHp < 0) pHp = 0; }
|
||||
const kept = [];
|
||||
for (const hid of hand) {
|
||||
const hc = cards[hid];
|
||||
if (hc?.retain === true) kept.push(hid);
|
||||
else discard.push(hid);
|
||||
}
|
||||
hand = kept;
|
||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||
// 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전)
|
||||
if (pWeak > 0) pWeak--;
|
||||
if (pVuln > 0) pVuln--;
|
||||
for (const m of mob) {
|
||||
if (!m.alive) continue;
|
||||
// 독 틱 — 행동 시작 시 (Lua EnemyActStep 동기화). 사망 시 행동 생략
|
||||
if (m.poison > 0) {
|
||||
m.hp -= m.poison;
|
||||
m.poison--;
|
||||
if (m.hp <= 0) { m.hp = 0; m.alive = false; continue; }
|
||||
}
|
||||
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
|
||||
// 정의된 intent 중 랜덤 선택 (Lua EnemyActStep 동기화 — 순차→랜덤)
|
||||
const it = m.intents.length ? m.intents[Math.floor(rng() * m.intents.length)] : null;
|
||||
if (it) {
|
||||
if (it.kind === 'Attack') {
|
||||
const atk = calcAttack(it.value, m.str, m.weak, pVuln);
|
||||
const beforeHp = pHp;
|
||||
const r = applyDamage(pHp, pBlock, atk); pHp = r.hp; pBlock = r.block;
|
||||
if (beforeHp > pHp && pThorns > 0) {
|
||||
m.hp -= pThorns;
|
||||
if (m.hp <= 0) m.alive = false;
|
||||
}
|
||||
} else if (it.kind === 'Defend') { m.block += it.value; }
|
||||
else if (it.kind === 'Debuff') {
|
||||
if (it.effect === 'weak') pWeak += it.value;
|
||||
else if (it.effect === 'vuln') pVuln += it.value;
|
||||
} else if (it.kind === 'AddCard') {
|
||||
// StS2식 덱 오염 — 저주 카드를 버린 더미에 추가 (Lua 동기화)
|
||||
const cnt = it.count || 1;
|
||||
for (let k = 0; k < cnt; k++) discard.push(it.card);
|
||||
}
|
||||
}
|
||||
// 적 디버프 감소 — Lua EnemyActStep 동기화 (자기 행동 후)
|
||||
if (m.weak > 0) m.weak--;
|
||||
if (m.vuln > 0) m.vuln--;
|
||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||
}
|
||||
// 독 사망 등 적 페이즈 중 전멸 처리 (Lua FinishEnemyTurn→CheckCombatEnd 동기화)
|
||||
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp };
|
||||
}
|
||||
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, monsters: data.monsters, seed,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatReport(r) {
|
||||
const L = [];
|
||||
L.push(`=== 밸런스 시뮬레이션 (인카운터: ${r.monsters.map((m) => `${m.name}(${m.maxHp})`).join(', ')}) ===`);
|
||||
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', 'Power']) {
|
||||
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' : kind === 'Power' ? '(지속)' : '블록/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();
|
||||
463
tools/balance/sim-balance.test.mjs
Normal file
463
tools/balance/sim-balance.test.mjs
Normal file
@@ -0,0 +1,463 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, rarityForRoll,
|
||||
} from './sim-balance.mjs';
|
||||
|
||||
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
||||
assert.equal(rarityForRoll(1), 'normal');
|
||||
assert.equal(rarityForRoll(70), 'normal');
|
||||
assert.equal(rarityForRoll(71), 'unique');
|
||||
assert.equal(rarityForRoll(95), 'unique');
|
||||
assert.equal(rarityForRoll(96), 'legend');
|
||||
assert.equal(rarityForRoll(100), 'legend');
|
||||
});
|
||||
|
||||
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(['Defend', 'Strike'], CARDS, 3);
|
||||
assert.equal(idx, 1); // Strike
|
||||
});
|
||||
|
||||
test('chooseAction: 공격 없으면 스킬 선택', () => {
|
||||
const idx = chooseAction(['Defend'], CARDS, 3);
|
||||
assert.equal(idx, 0);
|
||||
});
|
||||
|
||||
test('chooseAction: 사용 가능 카드 없으면 -1', () => {
|
||||
const idx = chooseAction(['Bash'], CARDS, 1);
|
||||
assert.equal(idx, -1);
|
||||
});
|
||||
|
||||
test('chooseTarget: 이번 타격으로 처치 가능한 최소 체력 우선', () => {
|
||||
const mob = [
|
||||
{ hp: 20, block: 0, alive: true },
|
||||
{ hp: 5, block: 0, alive: true },
|
||||
{ hp: 8, block: 0, alive: true },
|
||||
];
|
||||
assert.equal(chooseTarget(mob, 6), mob[1]); // 5<=6 처치 가능, 최소
|
||||
});
|
||||
|
||||
test('chooseTarget: 처치 불가면 유효체력 최소 선택', () => {
|
||||
const mob = [
|
||||
{ hp: 20, block: 0, alive: true },
|
||||
{ hp: 12, block: 5, alive: true },
|
||||
{ hp: 14, block: 0, alive: true },
|
||||
];
|
||||
assert.equal(chooseTarget(mob, 6), mob[2]); // 유효 14 < 17 < 20
|
||||
});
|
||||
|
||||
const DATA = {
|
||||
cards: CARDS,
|
||||
starterDeck: ['Strike', 'Strike', 'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend', 'Defend', 'Bash'],
|
||||
monsters: [
|
||||
{ name: '주황버섯', maxHp: 16, intents: [{ kind: 'Attack', value: 5 }, { kind: 'Defend', value: 4 }] },
|
||||
{ name: '파란버섯', maxHp: 12, intents: [{ kind: 'Attack', 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('simulateCombat: 강한 다수 적이면 패배 가능', () => {
|
||||
const hard = {
|
||||
cards: CARDS,
|
||||
starterDeck: DATA.starterDeck,
|
||||
monsters: Array.from({ length: 4 }, () => ({ name: '슬라임', maxHp: 60, intents: [{ kind: 'Attack', value: 12 }] })),
|
||||
};
|
||||
let losses = 0;
|
||||
for (let i = 0; i < 30; i++) if (!simulateCombat(hard, mulberry32(i + 1)).win) losses++;
|
||||
assert.ok(losses >= 1, `강한 적엔 패배가 나와야 함, 실제 패 ${losses}/30`);
|
||||
});
|
||||
|
||||
test('simulateCombat: 턴 상한 초과 시 draw 반환', () => {
|
||||
const immortal = {
|
||||
cards: { Defend: { name: '방어', cost: 1, kind: 'Skill', block: 5 } },
|
||||
starterDeck: Array(10).fill('Defend'),
|
||||
monsters: [{ name: '불사', maxHp: 9999, intents: [{ kind: 'Attack', value: 1 }] }],
|
||||
};
|
||||
const r = simulateCombat(immortal, mulberry32(1));
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.win, false);
|
||||
});
|
||||
|
||||
test('simulateCombat: 몬스터 없으면 즉시 승리', () => {
|
||||
const r = simulateCombat({ cards: {}, starterDeck: [], monsters: [] }, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 0);
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
test('simulateCombat: 복합 카드(공격+방어) 블록이 적 공격을 흡수', () => {
|
||||
const data = {
|
||||
cards: { Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 1, block: 3 } },
|
||||
starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'],
|
||||
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 9 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
// 매 턴 3장(에너지3) → 블록 9 = 적 공격 9 전부 흡수 → 무피해로 MAX_TURNS 도달(draw), HP 유지.
|
||||
// 블록 미적용이면 매턴 -9로 사망(win=false, draw 아님).
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.playerHpRemaining, 80);
|
||||
});
|
||||
|
||||
test('calcAttack: 힘·약화·취약 공식 (Lua CalcPlayerAttack·DealDamageToTarget 동기화)', () => {
|
||||
assert.equal(calcAttack(6, 0, 0, 0), 6); // 기본
|
||||
assert.equal(calcAttack(6, 2, 0, 0), 8); // 힘+2
|
||||
assert.equal(calcAttack(6, 0, 1, 0), 4); // 약화 floor(6*0.75)
|
||||
assert.equal(calcAttack(6, 0, 0, 1), 9); // 취약 floor(6*1.5)
|
||||
assert.equal(calcAttack(10, 2, 1, 1), 13); // floor(floor(12*0.75)=9 → floor(9*1.5))=13
|
||||
});
|
||||
|
||||
test('simulateCombat: 적 Debuff 인텐트만 사용 → 플레이어 무피해', () => {
|
||||
const data = {
|
||||
cards: { Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 } },
|
||||
starterDeck: ['Hit', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Debuff', effect: 'weak', value: 1 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.playerHpRemaining, 80);
|
||||
});
|
||||
|
||||
test('simulateCombat: 플레이어 약화 시 공격 피해 감소 반영', () => {
|
||||
// 약화 영구 부여 적: 4피해 카드가 floor(4*0.75)=3으로 감소
|
||||
const data = {
|
||||
cards: { Hit: { name: '타격', cost: 3, kind: 'Attack', damage: 4 } },
|
||||
starterDeck: ['Hit', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||
monsters: [{ name: '적', maxHp: 10, intents: [{ kind: 'Debuff', effect: 'weak', value: 99 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
// 턴1: 4 (약화 전), 이후 매턴 3 → 10피해 도달 = 턴3 (4+3+3)
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 3);
|
||||
});
|
||||
|
||||
test('simulateCombat: 카드 취약 부여가 같은 카드 피해에 선적용 (Lua 동기화)', () => {
|
||||
const data = {
|
||||
cards: { CB: { name: '차지', cost: 3, kind: 'Attack', damage: 8, vuln: 2 } },
|
||||
starterDeck: ['CB', 'CB', 'CB', 'CB', 'CB'],
|
||||
monsters: [{ name: '적', maxHp: 12, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
// 취약 선적용이면 floor(8*1.5)=12 → 1턴 처치. 후적용이면 8 → 2턴.
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
test('simulateCombat: Power(매턴 힘) 누적', () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Rage: { name: '분노', cost: 1, kind: 'Power', powerEffect: 'strengthPerTurn', value: 5 },
|
||||
Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 },
|
||||
},
|
||||
starterDeck: ['Rage', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||
monsters: [{ name: '적', maxHp: 60, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.ok(r.turns <= 6, `파워 누적으로 빠른 처치 기대, 실제 ${r.turns}턴`);
|
||||
});
|
||||
|
||||
test('simulateCombat: 적 약화 인텐트 → 적 공격력 감소는 적용 안 됨(적 자신은 약화 안 걸림)', () => {
|
||||
// 회귀 가드: Debuff 인텐트는 플레이어에게만 적용
|
||||
const data = {
|
||||
cards: { Skip: { name: '대기', cost: 3, kind: 'Skill', block: 0 } },
|
||||
starterDeck: ['Skip', 'Skip', 'Skip', 'Skip', 'Skip'],
|
||||
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Debuff', effect: 'vuln', value: 1 }, { kind: 'Attack', value: 10 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
// 턴1: 취약1 부여 → 플레이어 취약. 턴1 종료 시 1 감소 → 0. 턴2: 공격 10 (취약 소멸) → 정확히 10만 피해.
|
||||
// MAX_TURNS 동안 2턴 주기 공격 → 사망까지 충분 → win=false
|
||||
assert.equal(r.win, false);
|
||||
});
|
||||
|
||||
test('simulateCombat: 다단히트(hits) — 힘이 타격마다 적용, 취약은 합산 1회 (Lua 동기화)', () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Buff: { name: '버프', cost: 1, kind: 'Skill', strength: 2 },
|
||||
Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 5, hits: 2 },
|
||||
},
|
||||
starterDeck: ['Buff', 'Combo', 'Combo', 'Combo', 'Combo'],
|
||||
monsters: [{ name: '적', maxHp: 200, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// 공격 우선 휴리스틱: 턴1 콤보×3 (힘0) = 10×3 = 30
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(typeof r.win, 'boolean'); // 동작 보장 (수치는 아래 단위 검증)
|
||||
});
|
||||
|
||||
test('hits 수치: 힘+2일 때 5×2회 = (5+2)*2 = 14', () => {
|
||||
const data = {
|
||||
cards: { Combo: { name: '콤보', cost: 3, kind: 'Attack', damage: 5, hits: 2, strength: 0 } },
|
||||
starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'],
|
||||
monsters: [{ name: '적', maxHp: 10, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// 턴1: 10 피해 → 정확히 처치 (5×2)
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
test('simulateCombat: pierce — 적 방어도 무시', () => {
|
||||
const data = {
|
||||
cards: { P: { name: '피어스', cost: 3, kind: 'Attack', damage: 9, pierce: true } },
|
||||
starterDeck: ['P', 'P', 'P', 'P', 'P'],
|
||||
monsters: [{ name: '적', maxHp: 18, intents: [{ kind: 'Defend', value: 50 }] }],
|
||||
};
|
||||
// 턴1: 9 (방어 없음), 적이 방어 50. 턴2: pierce 9 → 처치. 비관통이면 흡수돼 불가.
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 2);
|
||||
});
|
||||
|
||||
test('simulateCombat: selfVuln — 자가 취약으로 받는 피해 증가', () => {
|
||||
const data = {
|
||||
cards: { B: { name: '버서크류', cost: 1, kind: 'Skill', selfVuln: 9, block: 0 } },
|
||||
starterDeck: ['B', 'B', 'B', 'B', 'B'],
|
||||
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 2 }] }],
|
||||
};
|
||||
// 매턴 스킬 사용으로 취약 유지 → 적 공격 2 → floor(2*1.5)=3 → 80/3 ≈ 27턴 사망 (취약 없으면 40턴)
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, false);
|
||||
assert.ok(r.turns <= 30, `취약 반영 시 30턴 내 사망, 실제 ${r.turns}`);
|
||||
});
|
||||
|
||||
test('simulateCombat: energyPerTurn 파워 — 다음 턴부터 에너지 증가', () => {
|
||||
const data = {
|
||||
cards: {
|
||||
E: { name: '버서크', cost: 1, kind: 'Power', powerEffect: 'energyPerTurn', value: 1 },
|
||||
Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 },
|
||||
},
|
||||
starterDeck: ['E', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||
monsters: [{ name: '적', maxHp: 14, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// 턴1: 파워+히트2 = 2, 턴2~4: 에너지4·손패 히트4 = 4/턴 → 2+4+4+4 = 14 → 턴4 처치
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 4);
|
||||
});
|
||||
|
||||
test('simulateCombat: blockPerTurn 파워 — 매턴 방어로 약공 무효', () => {
|
||||
const data = {
|
||||
cards: {
|
||||
B: { name: '하이퍼 바디', cost: 1, kind: 'Power', powerEffect: 'blockPerTurn', value: 3 },
|
||||
S: { name: '대기', cost: 3, kind: 'Skill', block: 0 },
|
||||
},
|
||||
starterDeck: ['B', 'S', 'S', 'S', 'S'],
|
||||
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 3 }] }],
|
||||
};
|
||||
// 턴1: 파워 설치, 적 3 피해(방어 없음) → 77. 턴2부터 매턴 방어3 = 공격3 전부 흡수 → draw, HP 77 유지
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.playerHpRemaining, 77);
|
||||
});
|
||||
|
||||
test('simulateCombat: poison — 적 행동 시작 시 틱·1 감소·독 사망 시 승리 처리', () => {
|
||||
const data = {
|
||||
cards: { PB: { name: '포이즌', cost: 3, kind: 'Skill', poison: 4 } },
|
||||
starterDeck: ['PB', 'PB', 'PB', 'PB', 'PB'],
|
||||
monsters: [{ name: '적', maxHp: 10, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// T1: 독4 부여 → 틱 4 (hp 6, 독 3). T2: +4 → 7 틱 → hp 0 사망 → 승리
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 2);
|
||||
});
|
||||
|
||||
test('simulateCombat: aoe — 모든 생존 적에게 피해', () => {
|
||||
const data = {
|
||||
cards: { TB: { name: '썬더 볼트', cost: 3, kind: 'Attack', damage: 6, aoe: true } },
|
||||
starterDeck: ['TB', 'TB', 'TB', 'TB', 'TB'],
|
||||
monsters: [
|
||||
{ name: 'A', maxHp: 6, intents: [{ kind: 'Attack', value: 5 }] },
|
||||
{ name: 'B', maxHp: 6, intents: [{ kind: 'Attack', value: 5 }] },
|
||||
{ name: 'C', maxHp: 6, intents: [{ kind: 'Attack', value: 5 }] },
|
||||
],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
test('simulateCombat: heal — 최대 HP 클램프', () => {
|
||||
const data = {
|
||||
cards: { H: { name: '힐', cost: 1, kind: 'Skill', heal: 10 } },
|
||||
starterDeck: ['H', 'H', 'H', 'H', 'H'],
|
||||
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 10 }] }],
|
||||
};
|
||||
// 매턴: 힐로 80까지 회복(클램프) → 적 10 → 70. MAX_TURNS 도달 시 hp 70
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.playerHpRemaining, 70);
|
||||
});
|
||||
|
||||
test('simulateCombat: draw — 카드 드로로 손패 보충', () => {
|
||||
const data = {
|
||||
cards: {
|
||||
D: { name: '텔레포트류', cost: 0, kind: 'Skill', draw: 1, block: 0 },
|
||||
Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 },
|
||||
},
|
||||
starterDeck: ['D', 'D', 'D', 'D', 'D', 'Hit', 'Hit', 'Hit'],
|
||||
monsters: [{ name: '적', maxHp: 4, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// 드로 덕에 첫 턴 히트 3장 전부 접근 → 늦어도 2턴 내 처치 (시드 무관)
|
||||
for (let s = 1; s <= 10; s++) {
|
||||
const r = simulateCombat(data, mulberry32(s));
|
||||
assert.equal(r.win, true, `seed ${s}`);
|
||||
assert.ok(r.turns <= 2, `seed ${s}: ${r.turns}턴`);
|
||||
}
|
||||
});
|
||||
|
||||
test('chooseAction: unplayable(저주) 카드는 건너뜀', () => {
|
||||
const cards = { Strike: { cost: 1, kind: 'Attack', damage: 6 }, Wound: { cost: 0, kind: 'Status', unplayable: true } };
|
||||
assert.equal(chooseAction(['Wound', 'Strike'], cards, 3), 1); // Strike 선택
|
||||
assert.equal(chooseAction(['Wound'], cards, 3), -1); // 낼 카드 없음
|
||||
});
|
||||
|
||||
test('simulateCombat: AddCard intent가 저주를 덱에 추가(오염)', () => {
|
||||
const data = {
|
||||
cards: { Hit: { name: '히트', cost: 1, kind: 'Attack', damage: 1 }, Wound: { name: '상처', cost: 0, kind: 'Status', unplayable: true } },
|
||||
starterDeck: ['Hit', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||
monsters: [{ name: '오염자', maxHp: 9999, intents: [{ kind: 'AddCard', card: 'Wound', count: 1 }] }],
|
||||
};
|
||||
// 적은 공격 안 하고 매 턴 저주만 추가 → 플레이어 무피해(승리 불가, 9999hp) → 무승부, 사망 아님
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, false);
|
||||
assert.equal(r.draw, true);
|
||||
});
|
||||
|
||||
test('simulateCombat: endTurnDamage(화상)이 턴 종료 시 누적 피해', () => {
|
||||
const data = {
|
||||
cards: { Skip: { name: '대기', cost: 3, kind: 'Skill', block: 0 }, Burn: { name: '화상', cost: 0, kind: 'Status', unplayable: true, endTurnDamage: 2 } },
|
||||
starterDeck: ['Burn', 'Skip', 'Skip', 'Skip', 'Skip'],
|
||||
monsters: [{ name: '무공격', maxHp: 9999, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// 적은 방어만(무피해). 손패의 Burn이 매 턴 -2 → 80hp 잠식 → MAX_TURNS 전 사망 → win false(draw 아님)
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, false);
|
||||
assert.notEqual(r.draw, true);
|
||||
});
|
||||
|
||||
test("simulateCombat: sly discarded card resolves for free", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Toss: { name: "Toss", cost: 1, kind: "Skill", discardAll: true },
|
||||
SlyHit: { name: "SlyHit", cost: 99, kind: "Attack", damage: 10, sly: true },
|
||||
Blank: { name: "Blank", cost: 99, kind: "Skill", block: 0 },
|
||||
},
|
||||
starterDeck: ["Toss", "SlyHit", "Blank", "Blank", "Blank"],
|
||||
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Defend", value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
test("simulateCombat: retain keeps card in hand across turns", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Boost: { name: "Boost", cost: 3, kind: "Power", powerEffect: "energyPerTurn", value: 98 },
|
||||
Hold: { name: "Hold", cost: 100, kind: "Attack", damage: 10, retain: true },
|
||||
Blank: { name: "Blank", cost: 99, kind: "Skill", block: 0 },
|
||||
},
|
||||
starterDeck: ["Blank", "Blank", "Blank", "Blank", "Blank", "Boost", "Hold", "Blank", "Blank", "Blank"],
|
||||
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Defend", value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, () => 0.999999);
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 2);
|
||||
});
|
||||
|
||||
test("simulateCombat: exhaust cards do not return through discard reshuffle", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
BurnOut: { name: "BurnOut", cost: 1, kind: "Attack", damage: 10, exhaust: true },
|
||||
},
|
||||
starterDeck: ["BurnOut"],
|
||||
monsters: [{ name: "Dummy", maxHp: 12, intents: [{ kind: "Defend", value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, false);
|
||||
assert.equal(r.draw, true);
|
||||
});
|
||||
|
||||
test("simulateCombat: dex increases block gained from cards", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Footwork: { name: "Footwork", cost: 1, kind: "Power", dex: 2 },
|
||||
Defend: { name: "Defend", cost: 1, kind: "Skill", block: 5 },
|
||||
},
|
||||
starterDeck: ["Footwork", "Defend"],
|
||||
monsters: [{ name: "Dummy", maxHp: 99, intents: [{ kind: "Attack", value: 6 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, () => 0.999999);
|
||||
assert.equal(r.win, false);
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.playerHpRemaining, 80);
|
||||
});
|
||||
|
||||
test("simulateCombat: thorns reflects unblocked attack damage", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Spikes: { name: "Spikes", cost: 1, kind: "Power", thorns: 4 },
|
||||
},
|
||||
starterDeck: ["Spikes"],
|
||||
monsters: [{ name: "Dummy", maxHp: 4, intents: [{ kind: "Attack", value: 1 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, () => 0.999999);
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
assert.equal(r.playerHpRemaining, 79);
|
||||
});
|
||||
|
||||
test("simulateCombat: addShiv creates shuriken cards in hand", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
MakeShiv: { name: "MakeShiv", cost: 0, kind: "Skill", addShiv: 2 },
|
||||
Shiv: { name: "표창", cost: 0, kind: "Attack", damage: 4, exhaust: true },
|
||||
},
|
||||
starterDeck: ["MakeShiv"],
|
||||
monsters: [{ name: "Dummy", maxHp: 8, intents: [{ kind: "Attack", value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, () => 0.999999);
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
102
tools/camera/gen-camera.mjs
Normal file
102
tools/camera/gen-camera.mjs
Normal file
@@ -0,0 +1,102 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
// 맵별 고정 카메라: 맵 로드 시 플레이어 CameraComponent를 data/camera.json 값으로 설정.
|
||||
// 새 CameraComponent를 만들지 않고(엔진 소유) 기존 카메라 속성만 런타임 설정한다.
|
||||
// 플레이어 입력 차단·시선 고정은 tools/player/gen-player-lock.mjs(script.PlayerLock)로 분리됨.
|
||||
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
|
||||
const MAP_NUMBERS = Array.from({ length: 5 }, (_, i) => i + 1); // map01~05
|
||||
|
||||
function prop(Type, Name, DefaultValue = 'nil') {
|
||||
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };
|
||||
}
|
||||
function method(Name, Code, Arguments = [], ExecSpace = 6) {
|
||||
return {
|
||||
Return: { Type: 'void', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null },
|
||||
Arguments,
|
||||
Code,
|
||||
Scope: 2,
|
||||
ExecSpace,
|
||||
Attributes: [],
|
||||
Name,
|
||||
};
|
||||
}
|
||||
|
||||
function writeCodeblock() {
|
||||
const cb = {
|
||||
Id: '',
|
||||
GameId: '',
|
||||
EntryKey: 'codeblock://mapcamera',
|
||||
ContentType: 'x-mod/codeblock',
|
||||
Content: '',
|
||||
Usage: 0,
|
||||
UsePublish: 1,
|
||||
UseService: 0,
|
||||
CoreVersion: '26.5.0.0',
|
||||
StudioVersion: '',
|
||||
DynamicLoading: 0,
|
||||
ContentProto: {
|
||||
Use: 'Json',
|
||||
Json: {
|
||||
CoreVersion: { Major: 0, Minor: 2 },
|
||||
ScriptVersion: { Major: 1, Minor: 0 },
|
||||
Description: '',
|
||||
Id: 'MapCamera',
|
||||
Language: 1,
|
||||
Name: 'MapCamera',
|
||||
Type: 1,
|
||||
Source: 0,
|
||||
Target: null,
|
||||
Properties: [prop('number', 'CamTries', '0')],
|
||||
Methods: [
|
||||
method('OnBeginPlay', `self.CamTries = 0
|
||||
local eventId = 0
|
||||
local function apply()
|
||||
self.CamTries = self.CamTries + 1
|
||||
local cam = nil
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
cam = lp.CameraComponent
|
||||
end
|
||||
if cam == nil then
|
||||
cam = _CameraService:GetCurrentCameraComponent()
|
||||
end
|
||||
if cam ~= nil then
|
||||
cam.ZoomRatio = ${CAM.zoomRatio}
|
||||
cam.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY})
|
||||
cam.ConfineCameraArea = ${CAM.confineCameraArea}
|
||||
cam.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY})
|
||||
end
|
||||
if cam ~= nil then
|
||||
_TimerService:ClearTimer(eventId)
|
||||
elseif self.CamTries > 30 then
|
||||
_TimerService:ClearTimer(eventId)
|
||||
end
|
||||
end
|
||||
eventId = _TimerService:SetTimerRepeat(apply, 0.1)`),
|
||||
],
|
||||
EntityEventHandlers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
writeFileSync('RootDesk/MyDesk/MapCamera.codeblock', JSON.stringify(cb, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function patchMap(nn) {
|
||||
const tag = String(nn).padStart(2, '0');
|
||||
const file = `map/map${tag}.map`;
|
||||
const map = JSON.parse(readFileSync(file, 'utf8'));
|
||||
const root = map.ContentProto.Entities.find((e) => e.path === `/maps/map${tag}`);
|
||||
if (!root) throw new Error(`[gen-camera] 맵 루트 없음: ${file}`);
|
||||
// idempotent: 기존 script.MapCamera 제거 후 재추가
|
||||
root.jsonString['@components'] = root.jsonString['@components'].filter((c) => c['@type'] !== 'script.MapCamera');
|
||||
root.jsonString['@components'].push({ '@type': 'script.MapCamera', Enable: true });
|
||||
const names = (root.componentNames || '').split(',').filter((s) => s && s !== 'script.MapCamera');
|
||||
names.push('script.MapCamera');
|
||||
root.componentNames = names.join(',');
|
||||
writeFileSync(file, JSON.stringify(map, null, 2), 'utf8');
|
||||
return `map${tag}`;
|
||||
}
|
||||
|
||||
writeCodeblock();
|
||||
const patched = MAP_NUMBERS.map(patchMap);
|
||||
console.log('MapCamera codeblock written; patched maps:', patched.join(', '));
|
||||
74
tools/deck/cb/boot.mjs
Normal file
74
tools/deck/cb/boot.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaCharsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const bootMethods = [
|
||||
method('OnBeginPlay', `${luaCardsTable(CARDS.cards)}
|
||||
${luaFramesTable()}
|
||||
${luaNodeIconsTable()}
|
||||
${luaCharsTable()}
|
||||
${luaSoulShopTable(SOUL_UNLOCKS)}
|
||||
self.SoulUnlocks = {}
|
||||
self.SoulPoints = self.SoulPoints or 0
|
||||
self:ShowLobby()
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
self:ReqLoadAscension(lp.PlayerComponent.UserId)
|
||||
self:ReqLoadSouls(lp.PlayerComponent.UserId)
|
||||
end
|
||||
_InputService:ConnectEvent(KeyDownEvent, function(e)
|
||||
if e.key == KeyboardKey.LeftControl then
|
||||
local lp2 = _UserService.LocalPlayer
|
||||
if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then
|
||||
self:PlayerAttackMotion()
|
||||
end
|
||||
end
|
||||
end)`),
|
||||
method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||
local errCode, value = ds:GetAndWait("ascensionUnlocked")
|
||||
local n = 0
|
||||
if errCode == 0 and value ~= nil and value ~= "" then
|
||||
n = tonumber(value) or 0
|
||||
end
|
||||
self:RecvAscension(n, userId)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }], 5),
|
||||
method('RecvAscension', `self.AscensionUnlocked = n
|
||||
if self.AscensionLevel > self.AscensionUnlocked then
|
||||
self.AscensionLevel = self.AscensionUnlocked
|
||||
end
|
||||
self:RenderAscension()`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' },
|
||||
], 6),
|
||||
method('SaveAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||
ds:SetAndWait("ascensionUnlocked", tostring(n))`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' },
|
||||
], 5),
|
||||
method('AdjustAscension', `local v = self.AscensionLevel + delta
|
||||
if v < 0 then v = 0 end
|
||||
if v > self.AscensionUnlocked then v = self.AscensionUnlocked end
|
||||
self.AscensionLevel = v
|
||||
self:RenderAscension()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delta' }]),
|
||||
method('RenderAscension', `self:SetText("/ui/DefaultGroup/MainMenu/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))
|
||||
self:SetText("/ui/DefaultGroup/LobbyHud/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`),
|
||||
method('AscHpMult', `local m = 1
|
||||
if self.AscensionLevel >= 1 then m = m + 0.1 end
|
||||
if self.AscensionLevel >= 6 then m = m + 0.1 end
|
||||
return m`, [], 0, 'number'),
|
||||
method('AscAtkMult', `local m = 1
|
||||
if self.AscensionLevel >= 2 then m = m + 0.1 end
|
||||
if self.AscensionLevel >= 7 then m = m + 0.1 end
|
||||
return m`, [], 0, 'number'),
|
||||
method('AscEliteBonus', `local b = 0
|
||||
if self.AscensionLevel >= 4 then b = b + 0.2 end
|
||||
if self.AscensionLevel >= 9 then b = b + 0.2 end
|
||||
return b`, [], 0, 'number'),
|
||||
method('AscGoldMult', `local m = 1
|
||||
if self.AscensionLevel >= 5 then m = m - 0.25 end
|
||||
if self.AscensionLevel >= 10 then m = m - 0.25 end
|
||||
return m`, [], 0, 'number'),
|
||||
method('AscStartHpPenalty', `local p = 0
|
||||
if self.AscensionLevel >= 3 then p = p + 10 end
|
||||
if self.AscensionLevel >= 8 then p = p + 10 end
|
||||
return p`, [], 0, 'number'),
|
||||
];
|
||||
70
tools/deck/cb/charselect.mjs
Normal file
70
tools/deck/cb/charselect.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const charSelectMethods = [
|
||||
method('ShowCharacterSelect', `self.SelectedClass = ""
|
||||
self:ShowState("charselect")
|
||||
self:RenderCharacterSelect()`),
|
||||
method('SelectClass', `self.SelectedClass = className
|
||||
self:RenderCharacterSelect()`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' },
|
||||
]),
|
||||
method('RenderCharacterSelect', `local warriorArt = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton/Art")
|
||||
if warriorArt ~= nil and warriorArt.SpriteGUIRendererComponent ~= nil and self.ClassPortraits ~= nil and self.ClassPortraits["warrior"] ~= nil then
|
||||
warriorArt.SpriteGUIRendererComponent.ImageRUID = self.ClassPortraits["warrior"]
|
||||
end
|
||||
local mageArt = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton/Art")
|
||||
if mageArt ~= nil and mageArt.SpriteGUIRendererComponent ~= nil and self.ClassPortraits ~= nil and self.ClassPortraits["magician"] ~= nil then
|
||||
mageArt.SpriteGUIRendererComponent.ImageRUID = self.ClassPortraits["magician"]
|
||||
end
|
||||
local thiefArt = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton/Art")
|
||||
if thiefArt ~= nil and thiefArt.SpriteGUIRendererComponent ~= nil and self.ClassPortraits ~= nil and self.ClassPortraits["bandit"] ~= nil then
|
||||
thiefArt.SpriteGUIRendererComponent.ImageRUID = self.ClassPortraits["bandit"]
|
||||
end
|
||||
local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
|
||||
if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then
|
||||
if self.SelectedClass == "warrior" then
|
||||
warrior.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
||||
else
|
||||
warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||||
end
|
||||
end
|
||||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
||||
if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then
|
||||
if self.SelectedClass == "magician" then
|
||||
mage.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
||||
else
|
||||
mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||||
end
|
||||
end
|
||||
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
|
||||
if thief ~= nil and thief.SpriteGUIRendererComponent ~= nil then
|
||||
if self.SelectedClass == "bandit" then
|
||||
thief.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
||||
else
|
||||
thief.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||||
end
|
||||
end
|
||||
if self.SelectedClass == "warrior" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨")
|
||||
elseif self.SelectedClass == "bandit" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "도적 선택됨")
|
||||
elseif self.SelectedClass == "magician" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "마법사 선택됨")
|
||||
else
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요")
|
||||
end`),
|
||||
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "bandit" and self.SelectedClass ~= "magician" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요")
|
||||
return
|
||||
end
|
||||
self:StartRun()`),
|
||||
method('SetEntityEnabled', `local e = _EntityService:GetEntityByPath(path)
|
||||
if e ~= nil then
|
||||
e.Enable = enabled
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
|
||||
]),
|
||||
];
|
||||
480
tools/deck/cb/combat.mjs
Normal file
480
tools/deck/cb/combat.mjs
Normal file
@@ -0,0 +1,480 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const combatMethods = [
|
||||
method('PlayCard', `if self:IsDiscardSelecting() == true then
|
||||
self:SelectDiscardSlot(slot)
|
||||
return
|
||||
end
|
||||
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == 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 c.unplayable == true then
|
||||
self:Toast("사용할 수 없는 카드입니다")
|
||||
return
|
||||
end
|
||||
if self.Energy < c.cost then
|
||||
self:Toast("에너지가 부족합니다")
|
||||
return
|
||||
end
|
||||
self.Energy = self.Energy - c.cost
|
||||
self:ResolveCardEffects(cardId, c, false)
|
||||
table.remove(self.Hand, slot)
|
||||
if c.exhaust == true then
|
||||
if self.ExhaustPile == nil then self.ExhaustPile = {} end
|
||||
table.insert(self.ExhaustPile, cardId)
|
||||
elseif c.kind ~= "Power" then
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
if self:BeginDiscardSelection(c) == true then
|
||||
return
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('OnCardButton', `if self:IsDiscardSelecting() == true then
|
||||
self:SelectDiscardSlot(slot)
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('FindMonsterAtTouch', `local best = 0
|
||||
local bestDist = 200
|
||||
for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
|
||||
local wp = m.entity.TransformComponent.WorldPosition
|
||||
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
|
||||
local dx = sp.x - touchPoint.x
|
||||
local dy = sp.y - touchPoint.y
|
||||
local d = math.sqrt(dx * dx + dy * dy)
|
||||
if d < bestDist then
|
||||
bestDist = d
|
||||
best = i
|
||||
end
|
||||
end
|
||||
end
|
||||
return best`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }], 0, 'number'),
|
||||
method('RenderTargetFrames', `local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
|
||||
local shownTarget = self.TargetIndex
|
||||
if dragActive == true then shownTarget = self.DragTargetIndex end
|
||||
for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
local active = false
|
||||
if m ~= nil and m.alive == true and i == shownTarget then active = true end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetFrame", active)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
|
||||
end`),
|
||||
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||
return
|
||||
end
|
||||
if self.Hand == nil or self.Hand[slot] == nil then
|
||||
return
|
||||
end
|
||||
if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
|
||||
_TimerService:ClearTimer(self.CardHoverTweenId)
|
||||
self.CardHoverTweenId = 0
|
||||
end
|
||||
for i = 1, 10 do
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
|
||||
end
|
||||
end
|
||||
self.DragSlot = slot
|
||||
self.DragTargetIndex = 0
|
||||
self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('OnCardDrag', `if self.DragSlot ~= slot then
|
||||
return
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
local ui = _UILogic:ScreenToUIPosition(touchPoint)
|
||||
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
local c = nil
|
||||
if cardId ~= nil then c = self.Cards[cardId] end
|
||||
if c ~= nil and c.kind == "Attack" then
|
||||
local best = self:FindMonsterAtTouch(touchPoint)
|
||||
if best ~= self.DragTargetIndex then
|
||||
self.DragTargetIndex = best
|
||||
self:RenderTargetFrames()
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||
]),
|
||||
method('OnCardDragEnd', `if self.DragSlot ~= slot then
|
||||
return
|
||||
end
|
||||
self.DragSlot = 0
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
end
|
||||
self:ResolveCardDrop(slot, touchPoint)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||
]),
|
||||
method('ResolveCardDrop', `if self:IsDiscardSelecting() == true then
|
||||
self:SelectDiscardSlot(slot)
|
||||
return
|
||||
end
|
||||
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true 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 c.kind == "Attack" then
|
||||
local best = self.DragTargetIndex or 0
|
||||
if best <= 0 then best = self:FindMonsterAtTouch(touchPoint) end
|
||||
self.DragTargetIndex = 0
|
||||
if best > 0 then
|
||||
self.TargetIndex = best
|
||||
self:PlayCard(slot)
|
||||
self:RenderTargetFrames()
|
||||
else
|
||||
self:RenderTargetFrames()
|
||||
end
|
||||
else
|
||||
self.DragTargetIndex = 0
|
||||
self:RenderTargetFrames()
|
||||
local ui = _UILogic:ScreenToUIPosition(touchPoint)
|
||||
if ui.y > -180 then
|
||||
self:PlayCard(slot)
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||
]),
|
||||
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
||||
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
||||
if m == nil or m.alive ~= true then
|
||||
m = nil
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end
|
||||
end
|
||||
end
|
||||
if m == nil then
|
||||
return
|
||||
end
|
||||
local dmg = amount
|
||||
if m.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
end
|
||||
if m.block > 0 and pierce ~= true then
|
||||
local absorbed = math.min(m.block, dmg)
|
||||
m.block = m.block - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
m.hp = m.hp - dmg
|
||||
self:MonsterHitMotion(m.slot)
|
||||
if m.hp <= 0 then
|
||||
m.hp = 0
|
||||
self:KillMonster(m.slot)
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||||
]),
|
||||
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
|
||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||
self:DealDamageToTarget(damage, pierce)
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
return
|
||||
end
|
||||
self.FxBusy = true
|
||||
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
|
||||
if fx ~= nil then
|
||||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||||
end
|
||||
if fx.UITransformComponent ~= nil and m.entity.TransformComponent ~= nil then
|
||||
local wp = m.entity.TransformComponent.WorldPosition
|
||||
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
|
||||
fx.UITransformComponent.anchoredPosition = _UILogic:ScreenToUIPosition(sp)
|
||||
end
|
||||
fx.Enable = true
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if fx ~= nil then fx.Enable = false end
|
||||
self.FxBusy = false
|
||||
local shown = damage
|
||||
local mt = self.Monsters[targetIndex]
|
||||
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
||||
shown = math.floor(damage * 1.5)
|
||||
end
|
||||
self:DealDamageToTarget(damage, pierce)
|
||||
self:ShowDmgPop(targetIndex, shown)
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
end, 0.35)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||||
]),
|
||||
method('PlayAoeFx', `self.FxBusy = true
|
||||
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
|
||||
if fx ~= nil then
|
||||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||||
end
|
||||
if fx.UITransformComponent ~= nil then
|
||||
fx.UITransformComponent.anchoredPosition = Vector2(300, 60)
|
||||
end
|
||||
fx.Enable = true
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if fx ~= nil then fx.Enable = false end
|
||||
self.FxBusy = false
|
||||
for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
if m ~= nil and m.alive == true then
|
||||
local dmg = damage
|
||||
if m.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
end
|
||||
if m.block > 0 then
|
||||
local absorbed = math.min(m.block, dmg)
|
||||
m.block = m.block - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
m.hp = m.hp - dmg
|
||||
self:ShowDmgPop(i, dmg)
|
||||
self:MonsterHitMotion(i)
|
||||
if m.hp <= 0 then
|
||||
m.hp = 0
|
||||
self:KillMonster(m.slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
end, 0.35)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||||
]),
|
||||
method('KillMonster', `local m = self.Monsters[slot]
|
||||
if m == nil then
|
||||
return
|
||||
end
|
||||
m.alive = false
|
||||
if m.entity ~= nil and isvalid(m.entity) then
|
||||
local ent = m.entity
|
||||
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
|
||||
end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false)
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
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
|
||||
if dmg > 0 then
|
||||
self.PlayerHp = self.PlayerHp - dmg
|
||||
local reflect = self.PlayerThorns or 0
|
||||
if self:HasRelic("bronzeScales") then
|
||||
reflect = reflect + 3
|
||||
end
|
||||
if reflect > 0 and attackerSlot ~= nil and attackerSlot > 0 then
|
||||
local am = self.Monsters[attackerSlot]
|
||||
if am ~= nil and am.alive == true then
|
||||
am.hp = am.hp - reflect
|
||||
self:ShowDmgPop(am.slot, reflect)
|
||||
self:MonsterHitMotion(am.slot)
|
||||
if am.hp <= 0 then
|
||||
am.hp = 0
|
||||
self:KillMonster(am.slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
if self:HasRelic("selfFormingClay") then
|
||||
self.ClayBlockNext = self.ClayBlockNext + 3
|
||||
end
|
||||
if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then
|
||||
self.FirstHpLossDone = true
|
||||
self:DrawCards(3)
|
||||
self:RenderHand(false)
|
||||
end
|
||||
end
|
||||
if self.PlayerHp < 0 then
|
||||
self.PlayerHp = 0
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'attackerSlot' },
|
||||
]),
|
||||
method('EnemyTurn', `self.TurnBusy = true
|
||||
self:EnemyActStep(1)`),
|
||||
method('EnemyActStep', `local idx = 0
|
||||
for i = fromIndex, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then idx = i; break end
|
||||
end
|
||||
if idx == 0 or self.PlayerHp <= 0 then
|
||||
self:FinishEnemyTurn()
|
||||
return
|
||||
end
|
||||
local m = self.Monsters[idx]
|
||||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx)
|
||||
self:SetEntityEnabled(base .. "/ActFrame", true)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if m.poison ~= nil and m.poison > 0 then
|
||||
m.hp = m.hp - m.poison
|
||||
self:ShowDmgPop(idx, m.poison)
|
||||
self:MonsterHitMotion(idx)
|
||||
m.poison = m.poison - 1
|
||||
if m.hp <= 0 then
|
||||
m.hp = 0
|
||||
self:KillMonster(m.slot)
|
||||
self:RenderCombat()
|
||||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
||||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
||||
return
|
||||
end
|
||||
end
|
||||
m.block = 0
|
||||
local intent = m.intents[m.intentIdx]
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
self:MonsterLunge(idx)
|
||||
local atk = intent.value + m.str
|
||||
if m.weak > 0 then
|
||||
atk = math.floor(atk * 0.75)
|
||||
end
|
||||
if self.PlayerVuln > 0 then
|
||||
atk = math.floor(atk * 1.5)
|
||||
end
|
||||
local before = self.PlayerHp
|
||||
self:DealDamageToPlayer(atk, idx)
|
||||
self:ShowPlayerDmgPop(before - self.PlayerHp)
|
||||
self:PlayerHitMotion()
|
||||
elseif intent.kind == "Defend" then
|
||||
m.block = m.block + intent.value
|
||||
elseif intent.kind == "Debuff" then
|
||||
if intent.effect == "weak" then
|
||||
self.PlayerWeak = self.PlayerWeak + intent.value
|
||||
elseif intent.effect == "vuln" then
|
||||
self.PlayerVuln = self.PlayerVuln + intent.value
|
||||
end
|
||||
elseif intent.kind == "AddCard" then
|
||||
local cnt = intent.count or 1
|
||||
for ci = 1, cnt do
|
||||
table.insert(self.DiscardPile, intent.card)
|
||||
end
|
||||
self:RenderPiles()
|
||||
local cn = intent.card
|
||||
local cc = self.Cards[intent.card]
|
||||
if cc ~= nil then cn = cc.name end
|
||||
self:Toast(m.name .. ": " .. cn .. " 추가!")
|
||||
end
|
||||
end
|
||||
if #m.intents > 0 then
|
||||
m.intentIdx = math.random(1, #m.intents)
|
||||
end
|
||||
if m.weak > 0 then m.weak = m.weak - 1 end
|
||||
if m.vuln > 0 then m.vuln = m.vuln - 1 end
|
||||
self:RenderCombat()
|
||||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
||||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
||||
end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]),
|
||||
method('FinishEnemyTurn', `self.TurnBusy = false
|
||||
self:CheckCombatEnd()
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`),
|
||||
method('ClearCombatCards', `self.DrawPile = {}
|
||||
self.DiscardPile = {}
|
||||
self.ExhaustPile = {}
|
||||
self.Hand = {}
|
||||
self.DiscardSelectRemaining = 0
|
||||
self.DiscardSelectTotal = 0
|
||||
self.DiscardPostShiv = 0
|
||||
self.DiscardShivPerPick = 0
|
||||
self:UpdateDiscardPrompt()
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()`),
|
||||
method('CheckCombatEnd', `local anyAlive = false
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then anyAlive = true; break end
|
||||
end
|
||||
if anyAlive == false then
|
||||
self.CombatOver = true
|
||||
self:ClearCombatCards()
|
||||
self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult())
|
||||
self:ApplyRelics("combatEnd")
|
||||
self:ApplyRelics("combatReward")
|
||||
self:MaybeDropPotion()
|
||||
self:RenderRun()
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil and node.type == "elite" then
|
||||
self.Gold = self.Gold + 15
|
||||
local nid = self:PickNewRelic()
|
||||
if nid ~= "" then
|
||||
self:AddRelic(nid)
|
||||
local nr = self.Relics[nid]
|
||||
if nr ~= nil then
|
||||
self:Toast("유물 획득: " .. nr.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
if node ~= nil and node.type == "boss" then
|
||||
if self.PlayerJob == "" and self.Floor < self.RunLength then
|
||||
self:ShowJobChoice()
|
||||
else
|
||||
if self.PlayerJob ~= "" then self:AwardSouls(1) end
|
||||
local bid = self:PickNewRelic()
|
||||
if bid ~= "" then
|
||||
self:AddRelic(bid)
|
||||
local br = self.Relics[bid]
|
||||
if br ~= nil then
|
||||
self:Toast("유물 획득: " .. br.name)
|
||||
end
|
||||
end
|
||||
self:ContinueAfterBoss()
|
||||
end
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
elseif self.PlayerHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:EndRun("패배...")
|
||||
end`),
|
||||
method('ContinueAfterBoss', `if self.Floor < self.RunLength then
|
||||
self.Floor = self.Floor + 1
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self:GenerateMap()
|
||||
self:RenderRun()
|
||||
self:TeleportToActMap()
|
||||
self:ShowMap()
|
||||
else
|
||||
self:EndRun("런 클리어!")
|
||||
end`),
|
||||
];
|
||||
344
tools/deck/cb/deckturn.mjs
Normal file
344
tools/deck/cb/deckturn.mjs
Normal file
@@ -0,0 +1,344 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const deckTurnMethods = [
|
||||
method('Shuffle', `if list == nil then
|
||||
\treturn
|
||||
end
|
||||
for i = #list, 2, -1 do
|
||||
\tlocal j = math.random(1, i)
|
||||
\tlist[i], list[j] = list[j], list[i]
|
||||
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]),
|
||||
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
|
||||
local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile")
|
||||
if drawPile ~= nil and drawPile.ButtonComponent ~= nil then
|
||||
if self.DrawPileHandler ~= nil then
|
||||
drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)
|
||||
self.DrawPileHandler = nil
|
||||
end
|
||||
self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end)
|
||||
end
|
||||
local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile")
|
||||
if discardPile ~= nil and discardPile.ButtonComponent ~= nil then
|
||||
if self.DiscardPileHandler ~= nil then
|
||||
discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)
|
||||
self.DiscardPileHandler = nil
|
||||
end
|
||||
self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end)
|
||||
end
|
||||
local exhaustPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/ExhaustPile")
|
||||
if exhaustPile ~= nil and exhaustPile.ButtonComponent ~= nil then
|
||||
if self.ExhaustPileHandler ~= nil then
|
||||
exhaustPile:DisconnectEvent(ButtonClickEvent, self.ExhaustPileHandler)
|
||||
self.ExhaustPileHandler = nil
|
||||
end
|
||||
self.ExhaustPileHandler = exhaustPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("exhaust") end)
|
||||
end
|
||||
local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close")
|
||||
if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then
|
||||
if self.DeckInspectCloseHandler ~= nil then
|
||||
inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)
|
||||
self.DeckInspectCloseHandler = nil
|
||||
end
|
||||
self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)
|
||||
end
|
||||
local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton")
|
||||
if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then
|
||||
if self.AllDeckHandler ~= nil then
|
||||
allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)
|
||||
self.AllDeckHandler = nil
|
||||
end
|
||||
self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)
|
||||
end
|
||||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
||||
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
||||
if self.AllDeckCloseHandler ~= nil then
|
||||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||
self.AllDeckCloseHandler = nil
|
||||
end
|
||||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||
end
|
||||
self:BindClassDeckTabs()
|
||||
for i = 1, 10 do
|
||||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||||
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
|
||||
local cardPath = "/ui/DefaultGroup/CardHand/Card" .. tostring(i)
|
||||
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
|
||||
cardEntity:ConnectEvent(UITouchDragEvent, function(ev) self:OnCardDrag(i, ev.TouchPoint) end)
|
||||
cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(i, ev.TouchPoint) end)
|
||||
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:HoverCard(i) end)
|
||||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end)
|
||||
if cardEntity.ButtonComponent ~= nil then
|
||||
cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end)
|
||||
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)
|
||||
if rc.UITouchReceiveComponent ~= nil then
|
||||
local cardPath = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(i)
|
||||
rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||
rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||
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
|
||||
local mapNodeIds = {}
|
||||
for r = 1, ${MAP_ROWS} do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
table.insert(mapNodeIds, "r" .. tostring(r) .. "c" .. tostring(c))
|
||||
end
|
||||
end
|
||||
table.insert(mapNodeIds, "boss")
|
||||
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)
|
||||
if sc.UITouchReceiveComponent ~= nil then
|
||||
local cardPath = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
||||
sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||
sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||
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 shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
||||
if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then
|
||||
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() 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
|
||||
for i = 1, ${MAX_MONSTERS} do
|
||||
local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i))
|
||||
if ms ~= nil and ms.ButtonComponent ~= nil then
|
||||
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
|
||||
end
|
||||
end
|
||||
for i = 1, 10 do
|
||||
local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
|
||||
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
|
||||
local idx = i
|
||||
rs:ConnectEvent(UITouchEnterEvent, function()
|
||||
local rid = nil
|
||||
if self.RunRelics ~= nil then rid = self.RunRelics[idx] end
|
||||
if rid ~= nil and self.Relics[rid] ~= nil then
|
||||
self:ShowTooltip(self.Relics[rid].name, self.Relics[rid].desc, -240 + (idx - 1) * 48)
|
||||
end
|
||||
end)
|
||||
rs:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
|
||||
end
|
||||
end
|
||||
for i = 1, 5 do
|
||||
local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
|
||||
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
|
||||
local idx = i
|
||||
ps:ConnectEvent(UITouchEnterEvent, function()
|
||||
local pid = nil
|
||||
if self.RunPotions ~= nil then pid = self.RunPotions[idx] end
|
||||
if pid ~= nil and self.Potions[pid] ~= nil then
|
||||
self:ShowTooltip(self.Potions[pid].name, self.Potions[pid].desc, 240 + (idx - 1) * 44)
|
||||
end
|
||||
end)
|
||||
ps:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
|
||||
ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end)
|
||||
end
|
||||
end
|
||||
local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use")
|
||||
if pmUse ~= nil and pmUse.ButtonComponent ~= nil then
|
||||
pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end)
|
||||
end
|
||||
local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss")
|
||||
if pmToss ~= nil and pmToss.ButtonComponent ~= nil then
|
||||
pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end)
|
||||
end
|
||||
local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close")
|
||||
if pmClose ~= nil and pmClose.ButtonComponent ~= nil then
|
||||
pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end)
|
||||
end
|
||||
local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
||||
if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then
|
||||
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
|
||||
end
|
||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||||
if chest ~= nil and chest.ButtonComponent ~= nil then
|
||||
chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end)
|
||||
end
|
||||
local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave")
|
||||
if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then
|
||||
treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||
end
|
||||
local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton")
|
||||
if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then
|
||||
jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end)
|
||||
end
|
||||
local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton")
|
||||
if jcJob ~= nil and jcJob.ButtonComponent ~= nil then
|
||||
jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end)
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local slotIdx = i
|
||||
local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i))
|
||||
if jb ~= nil and jb.ButtonComponent ~= nil then
|
||||
jb:ConnectEvent(ButtonClickEvent, function()
|
||||
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
|
||||
self:SetJob(self.JobOpts[slotIdx].id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end`),
|
||||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||||
self.Energy = self.MaxEnergy
|
||||
self:ApplyRelics("turnStart")
|
||||
self.PlayerBlock = 0
|
||||
if self.ClayBlockNext > 0 then
|
||||
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
|
||||
self.ClayBlockNext = 0
|
||||
end
|
||||
if self.PlayerPowers ~= nil then
|
||||
for i = 1, #self.PlayerPowers do
|
||||
local pc = self.Cards[self.PlayerPowers[i]]
|
||||
if pc ~= nil then
|
||||
if pc.powerEffect == "strengthPerTurn" then
|
||||
self.PlayerStr = self.PlayerStr + pc.value
|
||||
elseif pc.powerEffect == "energyPerTurn" then
|
||||
self.Energy = self.Energy + pc.value
|
||||
elseif pc.powerEffect == "blockPerTurn" then
|
||||
self.PlayerBlock = self.PlayerBlock + pc.value
|
||||
end
|
||||
if pc.turnStartShiv ~= nil then
|
||||
self:AddCardsToHand("Shiv", pc.turnStartShiv)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self:DrawCards(5)
|
||||
self:RenderHand(true)
|
||||
self:RenderCombat()`),
|
||||
method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||
return
|
||||
end
|
||||
if self:IsDiscardSelecting() == true then
|
||||
self:Toast("버릴 카드를 먼저 선택하세요")
|
||||
return
|
||||
end
|
||||
local burn = 0
|
||||
for bi = 1, #self.Hand do
|
||||
\tlocal hc = self.Cards[self.Hand[bi]]
|
||||
\tif hc ~= nil and hc.endTurnDamage ~= nil then burn = burn + hc.endTurnDamage end
|
||||
end
|
||||
if burn > 0 then
|
||||
\tself.PlayerHp = self.PlayerHp - burn
|
||||
\tif self.PlayerHp < 0 then self.PlayerHp = 0 end
|
||||
\tself:ShowPlayerDmgPop(burn)
|
||||
\tself:RenderCombat()
|
||||
end
|
||||
local kept = {}
|
||||
for i = 1, #self.Hand do
|
||||
\tlocal cardId = self.Hand[i]
|
||||
\tlocal c = self.Cards[cardId]
|
||||
\tif c ~= nil and c.retain == true then
|
||||
\t\ttable.insert(kept, cardId)
|
||||
\telse
|
||||
\t\ttable.insert(self.DiscardPile, cardId)
|
||||
\tend
|
||||
end
|
||||
self.Hand = kept
|
||||
if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end
|
||||
if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:EnemyTurn()`),
|
||||
method('DrawCards', `local drawnSlots = {}
|
||||
for i = 1, amount do
|
||||
\tif #self.DrawPile <= 0 then
|
||||
\t\tself:RecycleDiscardIntoDraw()
|
||||
\tend
|
||||
\tif #self.DrawPile <= 0 then
|
||||
\t\tbreak
|
||||
\tend
|
||||
\tlocal cardId = table.remove(self.DrawPile)
|
||||
\tif #self.Hand >= 10 then
|
||||
\t\ttable.insert(self.DiscardPile, cardId)
|
||||
\t\tself:TriggerSly(cardId)
|
||||
\telse
|
||||
\t\ttable.insert(self.Hand, cardId)
|
||||
\t\tif #self.Hand <= 5 then
|
||||
\t\t\ttable.insert(drawnSlots, #self.Hand)
|
||||
\t\tend
|
||||
\tend
|
||||
end
|
||||
self:RenderPiles()
|
||||
if animate == true and #drawnSlots > 0 then
|
||||
\tself:RenderHand(false)
|
||||
\tlocal drawStart = Vector2(-590, 8)
|
||||
\tfor i = 1, #drawnSlots do
|
||||
\t\tlocal slot = drawnSlots[i]
|
||||
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
|
||||
\tend
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
|
||||
]),
|
||||
method('AddCardsToHand', `if self.Hand == nil then
|
||||
self.Hand = {}
|
||||
end
|
||||
if self.DiscardPile == nil then
|
||||
self.DiscardPile = {}
|
||||
end
|
||||
for i = 1, amount do
|
||||
if #self.Hand >= 10 then
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
else
|
||||
table.insert(self.Hand, cardId)
|
||||
end
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
]),
|
||||
method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then
|
||||
\treturn
|
||||
end
|
||||
self.DrawPile = {}
|
||||
for i = 1, #self.DiscardPile do
|
||||
\tself.DrawPile[i] = self.DiscardPile[i]
|
||||
end
|
||||
self.DiscardPile = {}
|
||||
self:Shuffle(self.DrawPile)`),
|
||||
method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", self:FormatNumber(#self.DrawPile))
|
||||
self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", self:FormatNumber(#self.DiscardPile))
|
||||
self:SetText("/ui/DefaultGroup/DeckHud/ExhaustPile/Count", self:FormatNumber(#(self.ExhaustPile or {})))
|
||||
self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
|
||||
local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||||
if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then
|
||||
self:OpenDeckInspect(self.DeckInspectKind)
|
||||
end`),
|
||||
];
|
||||
229
tools/deck/cb/deckview.mjs
Normal file
229
tools/deck/cb/deckview.mjs
Normal file
@@ -0,0 +1,229 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const deckViewMethods = [
|
||||
method('OpenDeckInspect', `self.DeckInspectKind = kind
|
||||
if self.DeckAllOpen == true then
|
||||
self.DeckAllOpen = false
|
||||
local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||||
if allHud ~= nil then
|
||||
allHud.Enable = false
|
||||
end
|
||||
end
|
||||
local pile = {}
|
||||
local title = ""
|
||||
if kind == "discard" then
|
||||
pile = self.DiscardPile or {}
|
||||
title = "버린 덱"
|
||||
elseif kind == "exhaust" then
|
||||
pile = self.ExhaustPile or {}
|
||||
title = "소멸 덱"
|
||||
else
|
||||
pile = self.DrawPile or {}
|
||||
title = "뽑을 덱"
|
||||
end
|
||||
self:RenderDeckInspect(pile, title)
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||||
method('CloseDeckInspect', `self.DeckInspectKind = ""
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end`),
|
||||
method('RenderDeckInspect', `local count = 0
|
||||
if pile ~= nil then
|
||||
count = #pile
|
||||
end
|
||||
local suffix = " (" .. tostring(count) .. ")"
|
||||
if count > 60 then
|
||||
suffix = suffix .. " - 60장까지 표시"
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix)
|
||||
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty")
|
||||
if empty ~= nil then
|
||||
empty.Enable = count <= 0
|
||||
end
|
||||
for i = 1, 60 do
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i))
|
||||
if e ~= nil then
|
||||
local cardId = nil
|
||||
if pile ~= nil then
|
||||
cardId = pile[i]
|
||||
end
|
||||
if cardId == nil then
|
||||
e.Enable = false
|
||||
else
|
||||
e.Enable = true
|
||||
self:ApplyInspectCardVisual(i, cardId)
|
||||
end
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
|
||||
]),
|
||||
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/WarriorTab")
|
||||
if warriorTab ~= nil and warriorTab.ButtonComponent ~= nil then
|
||||
if self.WarriorDeckTabHandler ~= nil then
|
||||
warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler)
|
||||
self.WarriorDeckTabHandler = nil
|
||||
end
|
||||
self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end)
|
||||
end
|
||||
local thiefTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/ThiefTab")
|
||||
if thiefTab ~= nil and thiefTab.ButtonComponent ~= nil then
|
||||
if self.ThiefDeckTabHandler ~= nil then
|
||||
thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler)
|
||||
self.ThiefDeckTabHandler = nil
|
||||
end
|
||||
self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("bandit") end)
|
||||
end
|
||||
local mageTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/MageTab")
|
||||
if mageTab ~= nil and mageTab.ButtonComponent ~= nil then
|
||||
if self.MageDeckTabHandler ~= nil then
|
||||
mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler)
|
||||
self.MageDeckTabHandler = nil
|
||||
end
|
||||
self.MageDeckTabHandler = mageTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("magician") end)
|
||||
end`),
|
||||
method('OpenClassDeck', `self.CodexMode = false
|
||||
self.ClassDeckMode = true
|
||||
self.DeckAllOpen = true
|
||||
self:SetClassDeckTab(className)
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
||||
method('SetClassDeckTab', `if self.ClassDeckMode ~= true then
|
||||
return
|
||||
end
|
||||
self.ClassDeckCards = {}
|
||||
self.ClassDeckTitle = "직업 덱"
|
||||
if className ~= "warrior" and className ~= "magician" and className ~= "bandit" then
|
||||
className = "bandit"
|
||||
end
|
||||
self.ClassDeckClass = className
|
||||
local allowed = {}
|
||||
if className == "warrior" then
|
||||
allowed["warrior"] = true
|
||||
allowed["fighter"] = true
|
||||
allowed["page"] = true
|
||||
allowed["spearman"] = true
|
||||
self.ClassDeckTitle = "전사 전체 덱"
|
||||
elseif className == "magician" then
|
||||
allowed["magician"] = true
|
||||
allowed["firepoison"] = true
|
||||
allowed["icelightning"] = true
|
||||
allowed["cleric"] = true
|
||||
self.ClassDeckTitle = "마법사 전체 덱"
|
||||
else
|
||||
allowed["bandit"] = true
|
||||
allowed["shiv"] = true
|
||||
allowed["poisoner"] = true
|
||||
allowed["trickster"] = true
|
||||
self.ClassDeckTitle = "도적 전체 덱"
|
||||
end
|
||||
for id, c in pairs(self.Cards) do
|
||||
if c ~= nil and c.curse ~= true and allowed[c.class] == true then
|
||||
table.insert(self.ClassDeckCards, id)
|
||||
end
|
||||
end
|
||||
table.sort(self.ClassDeckCards, function(a, b)
|
||||
local ca = self.Cards[a]
|
||||
local cb = self.Cards[b]
|
||||
local na = a
|
||||
local nb = b
|
||||
if ca ~= nil and ca.name ~= nil then na = ca.name end
|
||||
if cb ~= nil and cb.name ~= nil then nb = cb.name end
|
||||
if na == nb then return a < b end
|
||||
return na < nb
|
||||
end)
|
||||
self:RenderAllDeck()
|
||||
self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
||||
method('RenderClassDeckTabs', `local tabs = {
|
||||
{ path = "/ui/DefaultGroup/DeckAllHud/WarriorTab", cls = "warrior" },
|
||||
{ path = "/ui/DefaultGroup/DeckAllHud/ThiefTab", cls = "bandit" },
|
||||
{ path = "/ui/DefaultGroup/DeckAllHud/MageTab", cls = "magician" },
|
||||
}
|
||||
for i = 1, #tabs do
|
||||
local e = _EntityService:GetEntityByPath(tabs[i].path)
|
||||
if e ~= nil then
|
||||
e.Enable = self.ClassDeckMode == true
|
||||
if e.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ClassDeckClass == tabs[i].cls then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||||
if inspectHud ~= nil then
|
||||
inspectHud.Enable = false
|
||||
end
|
||||
self.DeckInspectKind = ""
|
||||
self.ClassDeckMode = false
|
||||
self.ClassDeckClass = ""
|
||||
self:RenderClassDeckTabs()
|
||||
self.DeckAllOpen = true
|
||||
self:RenderAllDeck()
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('CloseAllDeck', `self.DeckAllOpen = false
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
if self.ClassDeckMode == true then
|
||||
self.ClassDeckMode = false
|
||||
self.ClassDeckCards = {}
|
||||
self.ClassDeckTitle = ""
|
||||
self.ClassDeckClass = ""
|
||||
end
|
||||
self:RenderClassDeckTabs()
|
||||
if self.CodexMode == true then
|
||||
self.CodexMode = false
|
||||
self:ShowLobby()
|
||||
end`),
|
||||
method('RenderAllDeck', `local pile = self.RunDeck or {}
|
||||
local title = "모든 덱"
|
||||
if self.ClassDeckMode == true then
|
||||
pile = self.ClassDeckCards or {}
|
||||
title = self.ClassDeckTitle
|
||||
elseif self.CodexMode == true then
|
||||
pile = self.CodexCards or {}
|
||||
title = "카드 도감"
|
||||
end
|
||||
local count = #pile
|
||||
self:SetText("/ui/DefaultGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")")
|
||||
self:RenderClassDeckTabs()
|
||||
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Empty")
|
||||
if empty ~= nil then
|
||||
empty.Enable = count <= 0
|
||||
end
|
||||
for i = 1, 120 do
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(i))
|
||||
if e ~= nil then
|
||||
local cardId = pile[i]
|
||||
if cardId == nil then
|
||||
e.Enable = false
|
||||
else
|
||||
e.Enable = true
|
||||
self:ApplyAllDeckCardVisual(i, cardId)
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
];
|
||||
401
tools/deck/cb/hand.mjs
Normal file
401
tools/deck/cb/hand.mjs
Normal file
@@ -0,0 +1,401 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const handMethods = [
|
||||
method('GetHandSlotX', `local n = 0
|
||||
if self.Hand ~= nil then
|
||||
n = #self.Hand
|
||||
end
|
||||
if n <= 0 then
|
||||
return 0
|
||||
end
|
||||
local spacing = 175
|
||||
if n > 8 then spacing = math.floor(1400 / n) end
|
||||
local startX = -((n - 1) * spacing) / 2
|
||||
return startX + (slot - 1) * spacing`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'),
|
||||
method('RenderHand', `local n = #self.Hand
|
||||
local spacing = 175
|
||||
if n > 8 then spacing = math.floor(1400 / n) end
|
||||
local startX = -((n - 1) * spacing) / 2
|
||||
local drawStart = Vector2(-590, 8)
|
||||
for i = 1, 10 do
|
||||
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||||
\tif cardEntity ~= nil then
|
||||
\t\tlocal cardId = self.Hand[i]
|
||||
\t\tif cardId == nil then
|
||||
\t\t\tcardEntity.Enable = false
|
||||
\t\telse
|
||||
\t\t\tcardEntity.Enable = true
|
||||
\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end
|
||||
\t\t\tself:ApplyCardVisual(i, cardId)
|
||||
\t\t\tlocal tx = self:GetHandSlotX(i)
|
||||
\t\t\tif animate == true then
|
||||
\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03)
|
||||
\t\t\telse
|
||||
\t\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.anchoredPosition = Vector2(tx, 0) end
|
||||
\t\t\tend
|
||||
\t\tend
|
||||
\tend
|
||||
end
|
||||
self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]),
|
||||
method('ApplyCardFace', `local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill", class = "warrior", rarity = "normal" }
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
end
|
||||
local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"]
|
||||
local ruid = nil
|
||||
if frames ~= nil then
|
||||
ruid = frames[c.rarity or "normal"]
|
||||
end
|
||||
if ruid ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ruid
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||||
self:SetText(base .. "/Name", c.name)
|
||||
self:SetText(base .. "/Desc", c.desc)
|
||||
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
||||
if art ~= nil then
|
||||
if c.image ~= nil and c.image ~= "" then
|
||||
art.Enable = true
|
||||
if art.SpriteGUIRendererComponent ~= nil then
|
||||
art.SpriteGUIRendererComponent.ImageRUID = c.image
|
||||
end
|
||||
else
|
||||
art.Enable = false
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('SetCardHover', `local prefix = ""
|
||||
local count = 0
|
||||
local xs = {}
|
||||
local baseY = 0
|
||||
local hoverIndex = 0
|
||||
local push = 110
|
||||
if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then
|
||||
if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||||
return
|
||||
end
|
||||
prefix = "/ui/DefaultGroup/CardHand/Card"
|
||||
count = 0
|
||||
if self.Hand ~= nil then count = #self.Hand end
|
||||
for i = 1, count do
|
||||
xs[i] = self:GetHandSlotX(i)
|
||||
end
|
||||
baseY = 0
|
||||
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
||||
elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then
|
||||
prefix = "/ui/DefaultGroup/RewardHud/Reward"
|
||||
count = 3
|
||||
xs = { -300, 0, 300 }
|
||||
baseY = 0
|
||||
hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0
|
||||
elseif string.find(path, "/ui/DefaultGroup/ShopHud/Card") == 1 then
|
||||
prefix = "/ui/DefaultGroup/ShopHud/Card"
|
||||
count = 3
|
||||
xs = { -300, 0, 300 }
|
||||
baseY = 20
|
||||
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
||||
end
|
||||
if count <= 0 then
|
||||
return
|
||||
end
|
||||
if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
|
||||
_TimerService:ClearTimer(self.CardHoverTweenId)
|
||||
self.CardHoverTweenId = 0
|
||||
end
|
||||
local items = {}
|
||||
for i = 1, count do
|
||||
local e = _EntityService:GetEntityByPath(prefix .. tostring(i))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
local tr = e.UITransformComponent
|
||||
local tx = xs[i]
|
||||
local ty = baseY
|
||||
local sc = 1
|
||||
if hover == true and hoverIndex > 0 then
|
||||
if i == hoverIndex and e.Enable == true then
|
||||
sc = 1.5
|
||||
elseif i < hoverIndex then
|
||||
tx = tx - push
|
||||
elseif i > hoverIndex then
|
||||
tx = tx + push
|
||||
end
|
||||
end
|
||||
table.insert(items, { tr = tr, sx = tr.anchoredPosition.x, sy = tr.anchoredPosition.y, ss = tr.UIScale.x, tx = tx, ty = ty, ts = sc })
|
||||
end
|
||||
end
|
||||
local elapsed = 0
|
||||
local duration = 0.12
|
||||
local eventId = 0
|
||||
eventId = _TimerService:SetTimerRepeat(function()
|
||||
elapsed = elapsed + 1 / 60
|
||||
local t = math.min(elapsed / duration, 1)
|
||||
local eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)
|
||||
for i = 1, #items do
|
||||
local it = items[i]
|
||||
local x = it.sx + (it.tx - it.sx) * eased
|
||||
local y = it.sy + (it.ty - it.sy) * eased
|
||||
local s = it.ss + (it.ts - it.ss) * eased
|
||||
it.tr.anchoredPosition = Vector2(x, y)
|
||||
it.tr.UIScale = Vector3(s, s, 1)
|
||||
end
|
||||
if t >= 1 then
|
||||
_TimerService:ClearTimer(eventId)
|
||||
if self.CardHoverTweenId == eventId then
|
||||
self.CardHoverTweenId = 0
|
||||
end
|
||||
end
|
||||
end, 1 / 60)
|
||||
self.CardHoverTweenId = eventId`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' },
|
||||
]),
|
||||
method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('SetText', `local entity = _EntityService:GetEntityByPath(path)
|
||||
if entity ~= nil and entity.TextComponent ~= nil then
|
||||
\tentity.TextComponent.Text = value
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' },
|
||||
]),
|
||||
method('FormatNumber', `if value == nil then
|
||||
return ""
|
||||
end
|
||||
local n = tonumber(value)
|
||||
if n == nil then
|
||||
return tostring(value)
|
||||
end
|
||||
if math.abs(n - math.floor(n)) < 0.00001 then
|
||||
return string.format("%d", math.floor(n))
|
||||
end
|
||||
return tostring(n)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }], 0, 'string'),
|
||||
method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||||
if cardEntity == nil or cardEntity.UITransformComponent == nil then
|
||||
\treturn
|
||||
end
|
||||
local tr = cardEntity.UITransformComponent
|
||||
tr.anchoredPosition = fromPos
|
||||
local elapsed = 0
|
||||
local eventId = 0
|
||||
eventId = _TimerService:SetTimerRepeat(function()
|
||||
\telapsed = elapsed + 1 / 60
|
||||
\tlocal t = math.min(elapsed / duration, 1)
|
||||
\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)
|
||||
\ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased)
|
||||
\tif t >= 1 then
|
||||
\t\t_TimerService:ClearTimer(eventId)
|
||||
\tend
|
||||
end, 1 / 60)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
|
||||
]),
|
||||
method('AddCardBlock', `local amount = base or 0
|
||||
if amount > 0 and self.PlayerDex ~= nil then
|
||||
amount = amount + self.PlayerDex
|
||||
end
|
||||
if amount < 0 then
|
||||
amount = 0
|
||||
end
|
||||
self.PlayerBlock = self.PlayerBlock + amount
|
||||
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||
method('CalcPlayerAttack', `local base2 = base
|
||||
self.FightAttackCount = self.FightAttackCount + 1
|
||||
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
|
||||
base2 = base2 + 8
|
||||
end
|
||||
local dmg = base2 + self.PlayerStr
|
||||
if self:HasRelic("penNib") and self.FightAttackCount % 10 == 0 then
|
||||
dmg = dmg * 2
|
||||
end
|
||||
if self.PlayerWeak > 0 then
|
||||
dmg = math.floor(dmg * 0.75)
|
||||
end
|
||||
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
|
||||
dmg = 5
|
||||
end
|
||||
if dmg < 0 then
|
||||
dmg = 0
|
||||
end
|
||||
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||
method('ResolveCardEffects', `if c == nil then
|
||||
return
|
||||
end
|
||||
if c.kind == "Attack" then
|
||||
if c.damage ~= nil then
|
||||
self:PlayerAttackMotion()
|
||||
local total = 0
|
||||
local hitN = c.hits or 1
|
||||
for h = 1, hitN do
|
||||
total = total + self:CalcPlayerAttack(c.damage)
|
||||
end
|
||||
if c.aoe == true then
|
||||
self:PlayAoeFx(c.fx or c.image, total)
|
||||
else
|
||||
self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true)
|
||||
end
|
||||
end
|
||||
if c.block ~= nil then
|
||||
self:AddCardBlock(c.block)
|
||||
end
|
||||
if free ~= true then
|
||||
self:ApplyRelics("cardPlayed")
|
||||
end
|
||||
elseif c.kind == "Skill" then
|
||||
if c.block ~= nil then
|
||||
self:AddCardBlock(c.block)
|
||||
end
|
||||
elseif c.kind == "Power" then
|
||||
if free ~= true then
|
||||
table.insert(self.PlayerPowers, cardId)
|
||||
end
|
||||
end
|
||||
if c.strength ~= nil then
|
||||
self.PlayerStr = self.PlayerStr + c.strength
|
||||
end
|
||||
if c.dex ~= nil then
|
||||
self.PlayerDex = self.PlayerDex + c.dex
|
||||
end
|
||||
if c.thorns ~= nil then
|
||||
self.PlayerThorns = self.PlayerThorns + c.thorns
|
||||
end
|
||||
if c.selfVuln ~= nil then
|
||||
self.PlayerVuln = self.PlayerVuln + c.selfVuln
|
||||
end
|
||||
if c.heal ~= nil then
|
||||
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
||||
end
|
||||
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
|
||||
local tm = self.Monsters[self.TargetIndex]
|
||||
if tm == nil or tm.alive ~= true then
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
|
||||
end
|
||||
end
|
||||
if tm ~= nil and tm.alive == true then
|
||||
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
|
||||
if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end
|
||||
if c.vuln ~= nil then
|
||||
tm.vuln = tm.vuln + c.vuln
|
||||
if self:HasRelic("championBelt") then
|
||||
tm.weak = tm.weak + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if c.draw ~= nil then
|
||||
self:DrawCards(c.draw, true)
|
||||
end
|
||||
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
||||
self:AddCardsToHand("Shiv", c.addShiv)
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' },
|
||||
]),
|
||||
method('TriggerSly', `local c = self.Cards[cardId]
|
||||
if c == nil or c.sly ~= true then
|
||||
return
|
||||
end
|
||||
self:Toast("교활 발동: " .. c.name)
|
||||
self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
|
||||
method('DiscardHandCard', `if self.Hand == nil then
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
table.remove(self.Hand, slot)
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
if triggerSly == true then
|
||||
self:TriggerSly(cardId)
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'triggerSly' },
|
||||
]),
|
||||
method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'),
|
||||
method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/DiscardPrompt")
|
||||
if e == nil then
|
||||
return
|
||||
end
|
||||
if self:IsDiscardSelecting() == true then
|
||||
local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. self:FormatNumber(picked + 1) .. "/" .. self:FormatNumber(self.DiscardSelectTotal))
|
||||
e.Enable = true
|
||||
else
|
||||
e.Enable = false
|
||||
end`),
|
||||
method('BeginDiscardSelection', `if c == nil or self.Hand == nil then
|
||||
return false
|
||||
end
|
||||
local n = 0
|
||||
if c.discardAll == true then
|
||||
n = #self.Hand
|
||||
elseif c.discard ~= nil then
|
||||
n = math.min(c.discard, #self.Hand)
|
||||
end
|
||||
if n <= 0 then
|
||||
return false
|
||||
end
|
||||
self.DiscardSelectRemaining = n
|
||||
self.DiscardSelectTotal = n
|
||||
self.DiscardPostShiv = 0
|
||||
self.DiscardShivPerPick = 0
|
||||
if c.addShiv ~= nil then
|
||||
self.DiscardPostShiv = c.addShiv
|
||||
end
|
||||
if c.addShivPerDiscard == true then
|
||||
self.DiscardShivPerPick = 1
|
||||
end
|
||||
self:UpdateDiscardPrompt()
|
||||
self:Toast("버릴 카드를 선택하세요")
|
||||
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||
method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0
|
||||
self.DiscardSelectTotal = 0
|
||||
local shivCount = self.DiscardPostShiv or 0
|
||||
self.DiscardPostShiv = 0
|
||||
self.DiscardShivPerPick = 0
|
||||
self:UpdateDiscardPrompt()
|
||||
if shivCount > 0 then
|
||||
self:AddCardsToHand("Shiv", shivCount)
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()`),
|
||||
method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then
|
||||
return false
|
||||
end
|
||||
if self.Hand == nil or self.Hand[slot] == nil then
|
||||
return true
|
||||
end
|
||||
local discarded = self.Hand[slot]
|
||||
self:DiscardHandCard(slot, true)
|
||||
if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then
|
||||
self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick
|
||||
end
|
||||
self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1
|
||||
if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then
|
||||
self:FinishDiscardSelection()
|
||||
else
|
||||
self:UpdateDiscardPrompt()
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
end
|
||||
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
|
||||
];
|
||||
213
tools/deck/cb/items.mjs
Normal file
213
tools/deck/cb/items.mjs
Normal file
@@ -0,0 +1,213 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const itemMethods = [
|
||||
method('HasRelic', `if self.RunRelics == nil then
|
||||
return false
|
||||
end
|
||||
for i = 1, #self.RunRelics do
|
||||
if self.RunRelics[i] == id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
||||
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 == "strength" then
|
||||
self.PlayerStr = self.PlayerStr + r.value
|
||||
elseif r.effect == "draw" then
|
||||
self:DrawCards(r.value)
|
||||
self:RenderHand(false)
|
||||
elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then
|
||||
self.PlayerHp = self.PlayerHp + r.value
|
||||
if self.PlayerHp > self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
elseif r.effect == "healIfLow" then
|
||||
if self.PlayerHp * 2 <= self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerHp + r.value
|
||||
if self.PlayerHp > self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
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)
|
||||
local r = self.Relics[id]
|
||||
if r ~= nil and r.hook == "passive" then
|
||||
if r.effect == "potionSlots" then
|
||||
self.PotionSlots = r.value
|
||||
self:RenderPotions()
|
||||
elseif r.effect == "maxHp" then
|
||||
self.PlayerMaxHp = self.PlayerMaxHp + r.value
|
||||
self.PlayerHp = self.PlayerHp + r.value
|
||||
end
|
||||
end
|
||||
self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
method('PickNewRelic', `local pool = {}
|
||||
for i = 1, #self.RelicPool do
|
||||
if self:HasRelic(self.RelicPool[i]) == false then
|
||||
table.insert(pool, self.RelicPool[i])
|
||||
end
|
||||
end
|
||||
if #pool == 0 then
|
||||
self.Gold = self.Gold + 25
|
||||
self:Toast("유물을 모두 모았습니다! 메소 +25")
|
||||
return ""
|
||||
end
|
||||
return pool[math.random(1, #pool)]`, [], 0, 'string'),
|
||||
method('AddPotion', `if self.RunPotions == nil then
|
||||
self.RunPotions = {}
|
||||
end
|
||||
if #self.RunPotions >= self.PotionSlots then
|
||||
self:Toast("물약 슬롯이 가득 찼습니다")
|
||||
return false
|
||||
end
|
||||
table.insert(self.RunPotions, pid)
|
||||
self:RenderPotions()
|
||||
return true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pid' }], 0, 'boolean'),
|
||||
method('MaybeDropPotion', `if math.random() > ${POTIONS.dropChance} then
|
||||
return
|
||||
end
|
||||
local keys = {}
|
||||
for pid, _ in pairs(self.Potions) do
|
||||
table.insert(keys, pid)
|
||||
end
|
||||
table.sort(keys)
|
||||
local pid = keys[math.random(1, #keys)]
|
||||
if self:AddPotion(pid) == true then
|
||||
local p = self.Potions[pid]
|
||||
self:Toast("물약 획득: " .. p.name)
|
||||
end`),
|
||||
method('RenderPotions', `for i = 1, 5 do
|
||||
local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
local pid = nil
|
||||
if self.RunPotions ~= nil then
|
||||
pid = self.RunPotions[i]
|
||||
end
|
||||
if pid ~= nil and self.Potions[pid] ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = self.Potions[pid].icon
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
elseif i > self.PotionSlots then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.1, 0.1, 0.12, 0.85)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.25, 0.3, 0.9)
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('OpenPotionMenu', `if self.RunPotions == nil or self.RunPotions[slot] == nil then
|
||||
return
|
||||
end
|
||||
self.PotionMenuSlot = slot
|
||||
local pid = self.RunPotions[slot]
|
||||
local p = self.Potions[pid]
|
||||
if p ~= nil then
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc)
|
||||
end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('ClosePotionMenu', `self.PotionMenuSlot = 0
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`),
|
||||
method('UsePotion', `if self.PotionMenuSlot <= 0 then
|
||||
return
|
||||
end
|
||||
if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then
|
||||
self:Toast("지금은 사용할 수 없습니다")
|
||||
return
|
||||
end
|
||||
local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud")
|
||||
local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand")
|
||||
if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then
|
||||
self:Toast("전투 중에만 사용할 수 있습니다")
|
||||
return
|
||||
end
|
||||
local pid = self.RunPotions[self.PotionMenuSlot]
|
||||
if pid == nil then
|
||||
return
|
||||
end
|
||||
local p = self.Potions[pid]
|
||||
if p == nil then
|
||||
return
|
||||
end
|
||||
if p.effect == "heal" then
|
||||
self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp)
|
||||
elseif p.effect == "damage" then
|
||||
self:DealDamageToTarget(p.value, false)
|
||||
self:ShowDmgPop(self.TargetIndex, p.value)
|
||||
elseif p.effect == "strength" then
|
||||
self.PlayerStr = self.PlayerStr + p.value
|
||||
elseif p.effect == "block" then
|
||||
self.PlayerBlock = self.PlayerBlock + p.value
|
||||
elseif p.effect == "energy" then
|
||||
self.Energy = self.Energy + p.value
|
||||
elseif p.effect == "weak" then
|
||||
local tm = self.Monsters[self.TargetIndex]
|
||||
if tm ~= nil and tm.alive == true then
|
||||
tm.weak = tm.weak + p.value
|
||||
end
|
||||
end
|
||||
table.remove(self.RunPotions, self.PotionMenuSlot)
|
||||
self:Toast("물약 사용: " .. p.name)
|
||||
self:ClosePotionMenu()
|
||||
self:RenderPotions()
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()`),
|
||||
method('TossPotion', `if self.PotionMenuSlot <= 0 then
|
||||
return
|
||||
end
|
||||
local pid = self.RunPotions[self.PotionMenuSlot]
|
||||
if pid ~= nil then
|
||||
local p = self.Potions[pid]
|
||||
table.remove(self.RunPotions, self.PotionMenuSlot)
|
||||
if p ~= nil then
|
||||
self:Toast("물약 버림: " .. p.name)
|
||||
end
|
||||
end
|
||||
self:ClosePotionMenu()
|
||||
self:RenderPotions()`),
|
||||
method('RenderRelics', `local count = 0
|
||||
if self.RunRelics ~= nil then
|
||||
count = #self.RunRelics
|
||||
end
|
||||
for i = 1, 10 do
|
||||
local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
local rid = nil
|
||||
if self.RunRelics ~= nil then
|
||||
rid = self.RunRelics[i]
|
||||
end
|
||||
if rid ~= nil and self.Relics[rid] ~= nil and (i < 10 or count <= 10) then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = self.Relics[rid].icon
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.15, 0.16, 0.2, 0.6)
|
||||
end
|
||||
end
|
||||
end
|
||||
local of = ""
|
||||
if count > 10 then
|
||||
of = "+" .. tostring(count - 9)
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`),
|
||||
];
|
||||
79
tools/deck/cb/jobs.mjs
Normal file
79
tools/deck/cb/jobs.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const jobMethods = [
|
||||
method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`),
|
||||
method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
|
||||
if kind == "relic" then
|
||||
local bid = self:PickNewRelic()
|
||||
if bid ~= "" then
|
||||
self:AddRelic(bid)
|
||||
local br = self.Relics[bid]
|
||||
if br ~= nil then
|
||||
self:Toast("유물 획득: " .. br.name)
|
||||
end
|
||||
end
|
||||
self:ContinueAfterBoss()
|
||||
else
|
||||
self:ShowJobSelect()
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||||
method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass]
|
||||
if opts == nil then
|
||||
opts = self.Jobs["warrior"]
|
||||
end
|
||||
self.JobOpts = opts
|
||||
for i = 1, 3 do
|
||||
local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)
|
||||
local o = opts[i]
|
||||
if o ~= nil then
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", o.name)
|
||||
self:SetText(base .. "/Desc", o.desc)
|
||||
local sc = self.Cards[o.starter]
|
||||
if sc ~= nil then
|
||||
self:SetText(base .. "/Starter", "대표 카드: " .. sc.name)
|
||||
end
|
||||
else
|
||||
self:SetEntityEnabled(base, false)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`),
|
||||
method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then
|
||||
for cls, list in pairs(self.Jobs) do
|
||||
for i = 1, #list do
|
||||
if list[i].id == self.PlayerJob then
|
||||
return list[i].name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.SelectedClass == "warrior" then
|
||||
return "전사"
|
||||
elseif self.SelectedClass == "bandit" then
|
||||
return "도적"
|
||||
elseif self.SelectedClass == "magician" then
|
||||
return "마법사"
|
||||
end
|
||||
return "플레이어"`, [], 0, 'string'),
|
||||
method('SetJob', `self.PlayerJob = jobId
|
||||
local starter = ""
|
||||
local opts = self.Jobs[self.SelectedClass] or {}
|
||||
for i = 1, #opts do
|
||||
if opts[i].id == jobId then
|
||||
starter = opts[i].starter
|
||||
end
|
||||
end
|
||||
if starter ~= "" then
|
||||
table.insert(self.RunDeck, starter)
|
||||
local sc = self.Cards[starter]
|
||||
if sc ~= nil then
|
||||
self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name)
|
||||
end
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
|
||||
self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]),
|
||||
];
|
||||
230
tools/deck/cb/map.mjs
Normal file
230
tools/deck/cb/map.mjs
Normal file
@@ -0,0 +1,230 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const mapMethods = [
|
||||
method('ShowMap', `self:ShowState("map")
|
||||
self:RenderMap()`),
|
||||
method('GenerateMap', `-- 절차 생성 — tools/map/rogue-map.mjs(JS 미러)와 로직 동기화 유지
|
||||
self.MapNodes = {}
|
||||
self.MapStart = {}
|
||||
self.VisitedNodes = {}
|
||||
self.Depth = 0
|
||||
self.MapNodes["boss"] = { type = "boss", row = ${MAP_ROWS} + 1, col = 0, next = {} }
|
||||
local cols = { 1, 2, 3, 4 }
|
||||
for i = #cols, 2, -1 do
|
||||
local j = math.random(1, i)
|
||||
cols[i], cols[j] = cols[j], cols[i]
|
||||
end
|
||||
local starts = { cols[1], cols[2], math.random(1, ${MAP_COLS}), math.random(1, ${MAP_COLS}) }
|
||||
for p = 1, 4 do
|
||||
local c = starts[p]
|
||||
local sid = "r1c" .. tostring(c)
|
||||
if self.MapNodes[sid] == nil then
|
||||
self.MapNodes[sid] = { type = "combat", row = 1, col = c, next = {} }
|
||||
end
|
||||
local found = false
|
||||
for i = 1, #self.MapStart do
|
||||
if self.MapStart[i] == sid then found = true end
|
||||
end
|
||||
if found == false then
|
||||
table.insert(self.MapStart, sid)
|
||||
end
|
||||
for r = 1, ${MAP_ROWS} - 1 do
|
||||
local nc = c + math.random(-1, 1)
|
||||
if nc < 1 then nc = 1 end
|
||||
if nc > ${MAP_COLS} then nc = ${MAP_COLS} end
|
||||
local nid = "r" .. tostring(r + 1) .. "c" .. tostring(nc)
|
||||
if self.MapNodes[nid] == nil then
|
||||
self.MapNodes[nid] = { type = "combat", row = r + 1, col = nc, next = {} }
|
||||
end
|
||||
local fid = "r" .. tostring(r) .. "c" .. tostring(c)
|
||||
local dup = false
|
||||
for i = 1, #self.MapNodes[fid].next do
|
||||
if self.MapNodes[fid].next[i] == nid then dup = true end
|
||||
end
|
||||
if dup == false then
|
||||
table.insert(self.MapNodes[fid].next, nid)
|
||||
end
|
||||
c = nc
|
||||
end
|
||||
local lid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c)
|
||||
local bdup = false
|
||||
for i = 1, #self.MapNodes[lid].next do
|
||||
if self.MapNodes[lid].next[i] == "boss" then bdup = true end
|
||||
end
|
||||
if bdup == false then
|
||||
table.insert(self.MapNodes[lid].next, "boss")
|
||||
end
|
||||
end
|
||||
for r = 3, ${MAP_ROWS} do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
local id = "r" .. tostring(r) .. "c" .. tostring(c)
|
||||
local node = self.MapNodes[id]
|
||||
if node ~= nil then
|
||||
-- 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지)
|
||||
local parentTypes = {}
|
||||
for pid, pn in pairs(self.MapNodes) do
|
||||
if pn.row == r - 1 then
|
||||
for i = 1, #pn.next do
|
||||
if pn.next[i] == id then parentTypes[pn.type] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
local w
|
||||
if r == ${MAP_ROWS} then
|
||||
w = { { "rest", 50 }, { "combat", 25 }, { "shop", 10 }, { "elite", 8 }, { "treasure", 7 } }
|
||||
elseif r >= 4 then
|
||||
w = { { "combat", 45 }, { "elite", 16 }, { "shop", 12 }, { "rest", 12 }, { "treasure", 15 } }
|
||||
else
|
||||
w = { { "combat", 45 }, { "shop", 12 }, { "rest", 12 } }
|
||||
end
|
||||
local total = 0
|
||||
for i = 1, #w do
|
||||
local t = w[i][1]
|
||||
if (t == "elite" or t == "rest" or t == "shop") and parentTypes[t] == true then
|
||||
w[i][2] = 0
|
||||
end
|
||||
total = total + w[i][2]
|
||||
end
|
||||
local roll = math.random() * total
|
||||
local acc = 0
|
||||
for i = 1, #w do
|
||||
acc = acc + w[i][2]
|
||||
if roll <= acc then
|
||||
node.type = w[i][1]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
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('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e == nil then
|
||||
return
|
||||
end
|
||||
local node = self.MapNodes[id]
|
||||
if node == nil then
|
||||
e.Enable = false
|
||||
return
|
||||
end
|
||||
e.Enable = true
|
||||
local ruid = self.NodeIcons[node.type]
|
||||
if ruid == nil then
|
||||
ruid = self.NodeIcons["combat"]
|
||||
end
|
||||
if e.SpriteGUIRendererComponent ~= nil and ruid ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ruid
|
||||
end
|
||||
local reachable = self:IsReachable(id)
|
||||
local visited = false
|
||||
if self.VisitedNodes ~= nil then
|
||||
for i = 1, #self.VisitedNodes do
|
||||
if self.VisitedNodes[i] == id then visited = true end
|
||||
end
|
||||
end
|
||||
if e.SpriteGUIRendererComponent ~= nil then
|
||||
if id == self.CurrentNodeId then
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
||||
elseif visited == true then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.9)
|
||||
elseif reachable == true then
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85)
|
||||
end
|
||||
end
|
||||
if e.ButtonComponent ~= nil then
|
||||
e.ButtonComponent.Enable = reachable
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
method('RenderMapDots', `local node = self.MapNodes[fromId]
|
||||
local has = false
|
||||
if node ~= nil then
|
||||
for i = 1, #node.next do
|
||||
if node.next[i] == toId then has = true end
|
||||
end
|
||||
end
|
||||
for k = 1, 3 do
|
||||
local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
|
||||
if d ~= nil then
|
||||
d.Enable = has
|
||||
if has == true and d.SpriteGUIRendererComponent ~= nil then
|
||||
if fromId == self.CurrentNodeId then
|
||||
d.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1)
|
||||
else
|
||||
d.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.8)
|
||||
end
|
||||
end
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'dotId' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromId' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toId' },
|
||||
]),
|
||||
method('RenderMap', `for r = 1, ${MAP_ROWS} do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
self:RenderMapNode("r" .. tostring(r) .. "c" .. tostring(c))
|
||||
end
|
||||
end
|
||||
self:RenderMapNode("boss")
|
||||
for r = 1, ${MAP_ROWS} - 1 do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
local fid = "r" .. tostring(r) .. "c" .. tostring(c)
|
||||
for c2 = c - 1, c + 1 do
|
||||
if c2 >= 1 and c2 <= ${MAP_COLS} then
|
||||
self:RenderMapDots(fid .. "_" .. tostring(c2), fid, "r" .. tostring(r + 1) .. "c" .. tostring(c2))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for c = 1, ${MAP_COLS} do
|
||||
local fid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c)
|
||||
self:RenderMapDots(fid .. "_b", fid, "boss")
|
||||
end
|
||||
`),
|
||||
method('PickNode', `if self.RunActive ~= true then
|
||||
return
|
||||
end
|
||||
if self:IsReachable(id) ~= true then
|
||||
return
|
||||
end
|
||||
self.CurrentNodeId = id
|
||||
if self.VisitedNodes == nil then
|
||||
self.VisitedNodes = {}
|
||||
end
|
||||
table.insert(self.VisitedNodes, id)
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
local node = self.MapNodes[id]
|
||||
self.Depth = node.row
|
||||
self:RenderRun()
|
||||
if node.type == "shop" then
|
||||
self:ShowShop()
|
||||
elseif node.type == "rest" then
|
||||
self:ShowRest()
|
||||
elseif node.type == "treasure" then
|
||||
self:ShowTreasure()
|
||||
else
|
||||
self.CurrentEnemyId = ""
|
||||
self:StartCombat()
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
];
|
||||
308
tools/deck/cb/render.mjs
Normal file
308
tools/deck/cb/render.mjs
Normal file
@@ -0,0 +1,308 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const renderMethods = [
|
||||
method('BuffsLabel', `local parts = {}
|
||||
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
|
||||
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
|
||||
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
|
||||
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
|
||||
return table.concat(parts, " ")`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
|
||||
], 0, 'string'),
|
||||
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
|
||||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)
|
||||
local m = self.Monsters[i]
|
||||
if m ~= nil and m.alive == true then
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", m.name)
|
||||
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
|
||||
local intent = m.intents[m.intentIdx]
|
||||
local t = ""
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
local atk = intent.value + m.str
|
||||
if m.weak > 0 then atk = math.floor(atk * 0.75) end
|
||||
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
|
||||
t = "공격 " .. tostring(atk)
|
||||
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
|
||||
elseif intent.kind == "Debuff" then
|
||||
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
|
||||
else t = "취약 " .. tostring(intent.value) .. " 부여" end
|
||||
elseif intent.kind == "AddCard" then
|
||||
t = "저주 카드 추가"
|
||||
end
|
||||
end
|
||||
self:SetText(base .. "/Intent", t)
|
||||
local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
|
||||
local shownTarget = self.TargetIndex
|
||||
if dragActive == true then shownTarget = self.DragTargetIndex end
|
||||
self:SetEntityEnabled(base .. "/TargetFrame", i == shownTarget)
|
||||
self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive)
|
||||
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
|
||||
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
|
||||
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1)
|
||||
elseif intent.kind == "Debuff" then
|
||||
intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1)
|
||||
elseif intent.kind == "AddCard" then
|
||||
intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1)
|
||||
else
|
||||
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
|
||||
end
|
||||
end
|
||||
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
|
||||
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
|
||||
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
|
||||
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0))
|
||||
else
|
||||
self:SetEntityEnabled(base, false)
|
||||
end
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
|
||||
self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
|
||||
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
|
||||
if self.PlayerDex ~= nil and self.PlayerDex > 0 then
|
||||
if pb ~= "" then pb = pb .. " " end
|
||||
pb = pb .. "민첩+" .. tostring(self.PlayerDex)
|
||||
end
|
||||
if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then
|
||||
if pb ~= "" then pb = pb .. " " end
|
||||
pb = pb .. "가시" .. tostring(self.PlayerThorns)
|
||||
end
|
||||
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
|
||||
local names = {}
|
||||
for i = 1, #self.PlayerPowers do
|
||||
local pc = self.Cards[self.PlayerPowers[i]]
|
||||
if pc ~= nil then table.insert(names, pc.name) end
|
||||
end
|
||||
if pb ~= "" then pb = pb .. " · " end
|
||||
pb = pb .. table.concat(names, " ")
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb)
|
||||
self:RenderRun()`),
|
||||
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
|
||||
local base = "/ui/DefaultGroup/CombatHud/DmgPop" .. slotKey
|
||||
local pop = _EntityService:GetEntityByPath(base)
|
||||
if pop == nil then
|
||||
return
|
||||
end
|
||||
self.DmgPopSeq = (self.DmgPopSeq or 0) + 1
|
||||
local popSeq = self.DmgPopSeq
|
||||
self:SetText(base, "")
|
||||
local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
|
||||
local shown = tostring(math.max(0, math.floor(amount)))
|
||||
if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
|
||||
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
|
||||
end
|
||||
local digits = {}
|
||||
for i = 1, string.len(shown) do
|
||||
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
|
||||
end
|
||||
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
|
||||
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
|
||||
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
|
||||
self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false)
|
||||
end
|
||||
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
|
||||
local digitPath = base .. "/Digit" .. tostring(i)
|
||||
local digitEntity = _EntityService:GetEntityByPath(digitPath)
|
||||
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
|
||||
if digits[i] ~= nil then
|
||||
digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
|
||||
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
if digitEntity.UITransformComponent ~= nil then
|
||||
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
|
||||
end
|
||||
self:SetEntityEnabled(digitPath, true)
|
||||
else
|
||||
self:SetEntityEnabled(digitPath, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
local popPos = nil
|
||||
local m = self.Monsters[slot]
|
||||
if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
|
||||
local wp = m.entity.TransformComponent.WorldPosition
|
||||
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45}))
|
||||
popPos = _UILogic:ScreenToUIPosition(screen)
|
||||
else
|
||||
local slotEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. slotKey)
|
||||
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
|
||||
local sp = slotEntity.UITransformComponent.anchoredPosition
|
||||
popPos = Vector2(sp.x, sp.y + 76)
|
||||
end
|
||||
end
|
||||
if pop ~= nil and pop.UITransformComponent ~= nil then
|
||||
if popPos ~= nil then
|
||||
pop.UITransformComponent.anchoredPosition = popPos
|
||||
else
|
||||
pop.UITransformComponent.anchoredPosition = Vector2(0, 120)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled(base, true)
|
||||
for i = 1, 6 do
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if self.DmgPopSeq ~= popSeq then
|
||||
return
|
||||
end
|
||||
local p = _EntityService:GetEntityByPath(base)
|
||||
if p ~= nil and p.UITransformComponent ~= nil then
|
||||
local cur = p.UITransformComponent.anchoredPosition
|
||||
p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
|
||||
end
|
||||
end, 0.045 * i)
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if self.DmgPopSeq ~= popSeq then
|
||||
return
|
||||
end
|
||||
self:SetEntityEnabled(base, false)
|
||||
end, 0.48)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
]),
|
||||
method('ShowPlayerDmgPop', `local base = "/ui/DefaultGroup/CombatHud/PlayerPanel/DmgPop"
|
||||
if amount > 0 then
|
||||
self:SetText(base, "-" .. string.format("%d", amount))
|
||||
else
|
||||
self:SetText(base, "막음")
|
||||
end
|
||||
self:SetEntityEnabled(base, true)
|
||||
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||||
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
|
||||
if lp == nil then
|
||||
return
|
||||
end
|
||||
if lp.StateComponent == nil then
|
||||
return
|
||||
end
|
||||
pcall(function() lp.StateComponent:ChangeState("ATTACK") end)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then
|
||||
pcall(function() lp.StateComponent:ChangeState("IDLE") end)
|
||||
end
|
||||
end, 0.5)`),
|
||||
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer
|
||||
if lp == nil then
|
||||
return
|
||||
end
|
||||
if lp.StateComponent ~= nil then
|
||||
pcall(function() lp.StateComponent:ChangeState("HIT") end)
|
||||
end
|
||||
local tr = lp.TransformComponent
|
||||
if tr == nil then
|
||||
return
|
||||
end
|
||||
local p = tr.Position
|
||||
tr.Position = Vector3(p.x - 0.15, p.y, p.z)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then
|
||||
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z)
|
||||
end
|
||||
end, 0.15)`),
|
||||
method('MonsterLunge', `local m = self.Monsters[idx]
|
||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||
return
|
||||
end
|
||||
if m.motionBusy == true then
|
||||
return
|
||||
end
|
||||
m.motionBusy = true
|
||||
local e = m.entity
|
||||
local tr = e.TransformComponent
|
||||
if tr == nil then
|
||||
m.motionBusy = false
|
||||
return
|
||||
end
|
||||
local p = tr.Position
|
||||
tr.Position = Vector3(p.x - 0.35, p.y, p.z)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if isvalid(e) and e.TransformComponent ~= nil then
|
||||
e.TransformComponent.Position = Vector3(p.x, p.y, p.z)
|
||||
end
|
||||
m.motionBusy = false
|
||||
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]),
|
||||
method('MonsterHitMotion', `local m = self.Monsters[slot]
|
||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||
return
|
||||
end
|
||||
local e = m.entity
|
||||
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then
|
||||
e.SpriteRendererComponent.SpriteRUID = m.hitClip
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then
|
||||
e.SpriteRendererComponent.SpriteRUID = m.standClip
|
||||
end
|
||||
end, 0.5)
|
||||
else
|
||||
if m.motionBusy == true then
|
||||
return
|
||||
end
|
||||
m.motionBusy = true
|
||||
local tr = e.TransformComponent
|
||||
if tr == nil then
|
||||
m.motionBusy = false
|
||||
return
|
||||
end
|
||||
local p = tr.Position
|
||||
local seq = { 0.12, -0.12, 0 }
|
||||
for i = 1, #seq do
|
||||
local dx = seq[i]
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if isvalid(e) and e.TransformComponent ~= nil then
|
||||
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z)
|
||||
end
|
||||
if i == #seq then
|
||||
m.motionBusy = false
|
||||
end
|
||||
end, 0.06 * i)
|
||||
end
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
|
||||
if e == nil or e.UITransformComponent == nil then
|
||||
return
|
||||
end
|
||||
local ratio = 0
|
||||
if maxHp > 0 then ratio = hp / maxHp end
|
||||
if ratio < 0 then ratio = 0 end
|
||||
local w = width * ratio
|
||||
e.UITransformComponent.RectSize = Vector2(w, 14)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
|
||||
]),
|
||||
method('PositionMonsterSlot', `local m = self.Monsters[slot]
|
||||
if m == nil or m.entity == nil or not isvalid(m.entity) then
|
||||
return
|
||||
end
|
||||
local tr = m.entity.TransformComponent
|
||||
if tr == nil then
|
||||
return
|
||||
end
|
||||
local wp = tr.WorldPosition
|
||||
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y}))
|
||||
local uipos = _UILogic:ScreenToUIPosition(screen)
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.anchoredPosition = uipos
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
|
||||
self.TargetIndex = slot
|
||||
self:RenderCombat()
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층"
|
||||
if self.AscensionLevel > 0 then
|
||||
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Floor", floorText)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
|
||||
];
|
||||
55
tools/deck/cb/reward.mjs
Normal file
55
tools/deck/cb/reward.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const rewardMethods = [
|
||||
method('CardPool', `local pool = {}
|
||||
for id, c in pairs(self.Cards) do
|
||||
if c.token ~= true and (c.class == self.SelectedClass or (self.PlayerJob ~= "" and c.class == self.PlayerJob)) then
|
||||
table.insert(pool, id)
|
||||
end
|
||||
end
|
||||
table.sort(pool)
|
||||
return pool`, [], 0, 'any'),
|
||||
method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
||||
local pool = self:CardPool()
|
||||
local byRarity = {}
|
||||
for _, id in ipairs(pool) do
|
||||
local r = self.Cards[id].rarity or "normal"
|
||||
if byRarity[r] == nil then byRarity[r] = {} end
|
||||
table.insert(byRarity[r], id)
|
||||
end
|
||||
self.RewardChoices = {}
|
||||
for i = 1, 3 do
|
||||
local roll = math.random(1, 100)
|
||||
local want = "normal"
|
||||
if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end
|
||||
local bucket = byRarity[want]
|
||||
if bucket == nil or #bucket == 0 then bucket = pool end
|
||||
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
|
||||
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
||||
end
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
|
||||
{ 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:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
];
|
||||
208
tools/deck/cb/run.mjs
Normal file
208
tools/deck/cb/run.mjs
Normal file
@@ -0,0 +1,208 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaCharsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const runMethods = [
|
||||
method('StartRun', `if self.SelectedClass == "magician" then
|
||||
self.PlayerMaxHp = ${CLASSES.magician.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} }
|
||||
elseif self.SelectedClass == "bandit" then
|
||||
self.PlayerMaxHp = ${CLASSES.bandit.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.bandit.map(luaStr).join(', ')} }
|
||||
else
|
||||
self.PlayerMaxHp = ${CLASSES.warrior.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} }
|
||||
end
|
||||
self.PlayerMaxHp = self.PlayerMaxHp - self:AscStartHpPenalty()
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
self.Gold = 0
|
||||
self.Floor = 1
|
||||
self.RunLength = ${ACT_COUNT}
|
||||
self.RunActive = true
|
||||
self.RunRelics = {}
|
||||
self.RunPotions = {}
|
||||
self.PotionSlots = ${POTIONS.baseSlots}
|
||||
${luaPotionsTable(POTIONS.potions)}
|
||||
${luaRelicsTable(RELICS.relics)}
|
||||
self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} }
|
||||
${luaEnemiesTable(ENEMIES.enemies)}
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self.PlayerJob = ""
|
||||
${luaJobsTable(JOBS)}
|
||||
${luaFramesTable()}
|
||||
${luaNodeIconsTable()}
|
||||
${luaCharsTable()}
|
||||
self:GenerateMap()
|
||||
self:BindButtons()
|
||||
self:AddRelic("${RELICS.startingRelic}")
|
||||
self:ApplySoulUnlocks()
|
||||
self:RenderPotions()
|
||||
self:TeleportToActMap()
|
||||
self:ShowMap()`),
|
||||
method('KickCombatCamera', `local cam = nil
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then cam = lp.CameraComponent end
|
||||
if cam == nil then cam = _CameraService:GetCurrentCameraComponent() end
|
||||
if cam ~= nil then cam.ConfineCameraArea = false end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
local cc = nil
|
||||
local lp2 = _UserService.LocalPlayer
|
||||
if lp2 ~= nil then cc = lp2.CameraComponent end
|
||||
if cc == nil then cc = _CameraService:GetCurrentCameraComponent() end
|
||||
if cc ~= nil then
|
||||
cc.ZoomRatio = ${CAM.zoomRatio}
|
||||
cc.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY})
|
||||
cc.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY})
|
||||
cc.ConfineCameraArea = true
|
||||
end
|
||||
end, 0.2)`),
|
||||
method('StartCombat', `self:ShowState("combat")
|
||||
self:KickCombatCamera()
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/DiscardPrompt", false)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||||
self.MaxEnergy = 3
|
||||
self.Turn = 0
|
||||
self.PlayerBlock = 0
|
||||
self.PlayerStr = 0
|
||||
self.PlayerDex = 0
|
||||
self.PlayerThorns = 0
|
||||
self.PlayerWeak = 0
|
||||
self.PlayerVuln = 0
|
||||
self.PlayerPowers = {}
|
||||
self.FightAttackCount = 0
|
||||
self.DmgPopSeq = 0
|
||||
self.FirstHpLossDone = false
|
||||
self.ClayBlockNext = 0
|
||||
self.DiscardSelectRemaining = 0
|
||||
self.DiscardSelectTotal = 0
|
||||
self.DiscardPostShiv = 0
|
||||
self.DiscardShivPerPick = 0
|
||||
self.CombatOver = false
|
||||
self.DiscardPile = {}
|
||||
self.ExhaustPile = {}
|
||||
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:BuildMonsters()
|
||||
self:RenderCombat()
|
||||
self:StartPlayerTurn()
|
||||
self:ApplyRelics("combatStart")
|
||||
self:RenderCombat()`),
|
||||
method('RegisterMonster', `if self.Registered == nil then
|
||||
self.Registered = {}
|
||||
end
|
||||
local g = group
|
||||
if g == nil or g == "" then g = "combat" end
|
||||
local mp = mapName
|
||||
if mp == nil then mp = "" end
|
||||
table.insert(self.Registered, { entity = monster, enemyId = enemyId, group = g, map = mp })`, [
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'group' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'mapName' },
|
||||
]),
|
||||
method('BuildMonsters', `self.Monsters = {}
|
||||
local g = "combat"
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil and node.type ~= nil then g = node.type end
|
||||
local pmap = ""
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil and lp.CurrentMapName ~= nil then pmap = lp.CurrentMapName end
|
||||
local reg = self.Registered or {}
|
||||
for i = 1, #reg do
|
||||
if reg[i].entity ~= nil and isvalid(reg[i].entity) then
|
||||
reg[i].entity:SetVisible(false)
|
||||
end
|
||||
end
|
||||
local byGroup = {}
|
||||
for i = 1, #reg do
|
||||
local r = reg[i]
|
||||
if r.entity ~= nil and isvalid(r.entity) and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then
|
||||
local gg = r.group
|
||||
if gg == nil or gg == "" then gg = "combat" end
|
||||
if byGroup[gg] == nil then byGroup[gg] = {} end
|
||||
local x = 0
|
||||
if r.entity.TransformComponent ~= nil then
|
||||
x = r.entity.TransformComponent.WorldPosition.x
|
||||
end
|
||||
table.insert(byGroup[gg], { entity = r.entity, enemyId = r.enemyId, x = x })
|
||||
end
|
||||
end
|
||||
-- 노드 타입별 랜덤 구성: 일반 1~3 / 엘리트 1+일반0~2 / 보스 1
|
||||
local chosen = {}
|
||||
local function takeFrom(key, k)
|
||||
local src = byGroup[key] or {}
|
||||
local pool = {}
|
||||
for i = 1, #src do pool[i] = src[i] end
|
||||
self:Shuffle(pool)
|
||||
local taken = 0
|
||||
for i = 1, #pool do
|
||||
if taken >= k then break end
|
||||
table.insert(chosen, pool[i])
|
||||
taken = taken + 1
|
||||
end
|
||||
end
|
||||
if g == "boss" then
|
||||
takeFrom("boss", 1)
|
||||
elseif g == "elite" then
|
||||
takeFrom("elite", 1)
|
||||
takeFrom("combat", math.random(0, 2))
|
||||
else
|
||||
takeFrom("combat", math.random(1, 3))
|
||||
end
|
||||
if #chosen == 0 then takeFrom(g, 1) end
|
||||
if #chosen == 0 then takeFrom("combat", 1) end
|
||||
table.sort(chosen, function(a, b) return a.x < b.x end)
|
||||
local mult = 1 + (self.Floor - 1) * 0.45
|
||||
if g == "elite" or g == "boss" then
|
||||
mult = mult + self:AscEliteBonus()
|
||||
end
|
||||
local n = #chosen
|
||||
if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end
|
||||
for i = 1, n do
|
||||
local item = chosen[i]
|
||||
local e = self.Enemies[item.enemyId]
|
||||
if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end
|
||||
local intents = {}
|
||||
for k = 1, #e.intents do
|
||||
local v = e.intents[k].value or 0
|
||||
if e.intents[k].kind == "Attack" then
|
||||
v = math.floor(v * mult * self:AscAtkMult())
|
||||
elseif e.intents[k].kind ~= "Debuff" then
|
||||
v = math.floor(v * mult)
|
||||
end
|
||||
intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect, card = e.intents[k].card, count = e.intents[k].count }
|
||||
end
|
||||
local maxHp = math.floor(e.maxHp * mult * self:AscHpMult())
|
||||
local hitClip = nil
|
||||
local standClip = nil
|
||||
if item.entity.StateAnimationComponent ~= nil then
|
||||
pcall(function()
|
||||
hitClip = item.entity.StateAnimationComponent.ActionSheet["hit"]
|
||||
standClip = item.entity.StateAnimationComponent.ActionSheet["stand"]
|
||||
end)
|
||||
end
|
||||
local startIdx = 1
|
||||
if #intents > 0 then startIdx = math.random(1, #intents) end
|
||||
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
||||
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0,
|
||||
hitClip = hitClip, standClip = standClip, motionBusy = false,
|
||||
intents = intents, intentIdx = startIdx, alive = true, slot = i }
|
||||
self:ReviveMonsterEntity(item.entity)
|
||||
self:PositionMonsterSlot(i)
|
||||
end
|
||||
self.TargetIndex = 1`),
|
||||
method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then
|
||||
return
|
||||
end
|
||||
monster:SetEnable(true)
|
||||
monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]),
|
||||
];
|
||||
37
tools/deck/cb/runend.mjs
Normal file
37
tools/deck/cb/runend.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const runEndMethods = [
|
||||
method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} }
|
||||
local target = maps[self.Floor]
|
||||
if target == nil then
|
||||
return
|
||||
end
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp == nil then
|
||||
return
|
||||
end
|
||||
if lp.CurrentMapName == target then
|
||||
return
|
||||
end
|
||||
_TeleportService:TeleportToMapPosition(lp, Vector3(-6, 0.03, 0), target)`),
|
||||
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('EndRun', `local msg = text
|
||||
if text == "런 클리어!" and self.AscensionLevel >= self.AscensionUnlocked and self.AscensionUnlocked < 10 then
|
||||
self.AscensionUnlocked = self.AscensionUnlocked + 1
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
self:SaveAscension(self.AscensionUnlocked, lp.PlayerComponent.UserId)
|
||||
end
|
||||
self:RenderAscension()
|
||||
msg = "런 클리어! 승천 " .. string.format("%d", self.AscensionUnlocked) .. " 해금!"
|
||||
end
|
||||
self:ShowResult(msg)
|
||||
self.RunActive = false
|
||||
_TimerService:SetTimerOnce(function() self:ShowLobby() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||||
];
|
||||
173
tools/deck/cb/shop.mjs
Normal file
173
tools/deck/cb/shop.mjs
Normal file
@@ -0,0 +1,173 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const shopMethods = [
|
||||
method('ShowShop', `local pool = self:CardPool()
|
||||
self.ShopChoices = {}
|
||||
self.ShopBought = { false, false, false }
|
||||
for i = 1, 3 do
|
||||
self.ShopChoices[i] = pool[math.random(1, #pool)]
|
||||
end
|
||||
self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)]
|
||||
self.ShopRelicBought = false
|
||||
local pkeys = {}
|
||||
for pid, _ in pairs(self.Potions) do
|
||||
table.insert(pkeys, pid)
|
||||
end
|
||||
table.sort(pkeys)
|
||||
self.ShopPotion = pkeys[math.random(1, #pkeys)]
|
||||
self.ShopPotionBought = false
|
||||
self:RenderShop()
|
||||
self:ShowState("shop")`),
|
||||
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:ApplyCardFace(base, cid)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
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 re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
||||
if re ~= nil and re.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopRelicBought == true then
|
||||
re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
else
|
||||
re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
local pp = self.Potions[self.ShopPotion]
|
||||
if pp ~= nil then
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc)
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소")
|
||||
local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
||||
if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopPotionBought == true then
|
||||
pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
else
|
||||
pe.SpriteGUIRendererComponent.Color = Color(0.45, 0.7, 0.55, 1)
|
||||
end
|
||||
end
|
||||
end`),
|
||||
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()`),
|
||||
method('BuyPotion', `if self.ShopPotionBought == true then
|
||||
return
|
||||
end
|
||||
if self.Gold < ${POTIONS.shopPrice} then
|
||||
return
|
||||
end
|
||||
if self.RunPotions ~= nil and #self.RunPotions >= self.PotionSlots then
|
||||
self:Toast("물약 슬롯이 가득 찼습니다")
|
||||
return
|
||||
end
|
||||
if self:AddPotion(self.ShopPotion) == true then
|
||||
self.Gold = self.Gold - ${POTIONS.shopPrice}
|
||||
self.ShopPotionBought = true
|
||||
end
|
||||
self:RenderShop()
|
||||
self:RenderRun()`),
|
||||
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()
|
||||
self:ShowState("rest")`),
|
||||
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
|
||||
local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud")
|
||||
if t ~= nil then
|
||||
t.Enable = false
|
||||
end
|
||||
self:ShowMap()`),
|
||||
method('ShowTreasure', `self.ChestOpened = false
|
||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||||
if chest ~= nil then
|
||||
if chest.SpriteGUIRendererComponent ~= nil then
|
||||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}"
|
||||
end
|
||||
if chest.UITransformComponent ~= nil then
|
||||
chest.UITransformComponent.anchoredPosition = Vector2(0, 40)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true)
|
||||
self:ShowState("treasure")`),
|
||||
method('OpenChest', `if self.ChestOpened == true then
|
||||
return
|
||||
end
|
||||
self.ChestOpened = true
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false)
|
||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||||
local steps = { 10, -10, 8, -8, 5, 0 }
|
||||
for i = 1, #steps do
|
||||
local dx = steps[i]
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if chest ~= nil and isvalid(chest) and chest.UITransformComponent ~= nil then
|
||||
chest.UITransformComponent.anchoredPosition = Vector2(dx, 40)
|
||||
end
|
||||
end, 0.08 * i)
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if chest ~= nil and isvalid(chest) and chest.SpriteGUIRendererComponent ~= nil then
|
||||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_OPEN_RUID}"
|
||||
end
|
||||
local g = 40 + math.random(0, 20)
|
||||
local nid = self:PickNewRelic()
|
||||
local msg = ""
|
||||
if nid ~= "" then
|
||||
self:AddRelic(nid)
|
||||
local nr = self.Relics[nid]
|
||||
msg = "유물 획득: " .. nr.name .. " · 메소 +" .. tostring(g)
|
||||
else
|
||||
g = g + 30
|
||||
msg = "메소 +" .. tostring(g)
|
||||
end
|
||||
self.Gold = self.Gold + g
|
||||
self:RenderRun()
|
||||
self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true)
|
||||
end, 0.55)`),
|
||||
];
|
||||
114
tools/deck/cb/soul.mjs
Normal file
114
tools/deck/cb/soul.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const soulMethods = [
|
||||
method('ShowSoulShop', `self:RenderSoulLabel()
|
||||
self:RenderSoulShop()
|
||||
self:BindSoulShopButtons()
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`),
|
||||
method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
|
||||
method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||
local e1, pts = ds:GetAndWait("soulPoints")
|
||||
local e2, unl = ds:GetAndWait("soulUnlocks")
|
||||
local p = 0
|
||||
if e1 == 0 and pts ~= nil and pts ~= "" then p = tonumber(pts) or 0 end
|
||||
local u = ""
|
||||
if e2 == 0 and unl ~= nil then u = unl end
|
||||
self:RecvSouls(p, u, userId)`, [{ Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
|
||||
method('RecvSouls', `self.SoulPoints = p
|
||||
self.SoulUnlocks = {}
|
||||
if u ~= nil and u ~= "" then
|
||||
for key in string.gmatch(u, "([^,]+)") do
|
||||
self.SoulUnlocks[key] = true
|
||||
end
|
||||
end
|
||||
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 6),
|
||||
method('SaveSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||
ds:SetAndWait("soulPoints", tostring(p))
|
||||
ds:SetAndWait("soulUnlocks", u)`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
|
||||
method('SerializeUnlocks', `local parts = {}
|
||||
if self.SoulUnlocks ~= nil then
|
||||
for k, v in pairs(self.SoulUnlocks) do
|
||||
if v == true then table.insert(parts, k) end
|
||||
end
|
||||
end
|
||||
return table.concat(parts, ",")`, [], 0, 'string'),
|
||||
method('AwardSouls', `self.SoulPoints = (self.SoulPoints or 0) + n
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
|
||||
end
|
||||
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]),
|
||||
method('BuySoulUnlock', `local d = nil
|
||||
if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end
|
||||
if d == nil then return end
|
||||
if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then
|
||||
self:Toast("이미 보유 중입니다")
|
||||
return
|
||||
end
|
||||
if (self.SoulPoints or 0) < d.cost then
|
||||
self:Toast("영혼이 부족합니다")
|
||||
return
|
||||
end
|
||||
self.SoulPoints = self.SoulPoints - d.cost
|
||||
if self.SoulUnlocks == nil then self.SoulUnlocks = {} end
|
||||
self.SoulUnlocks[d.key] = true
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
|
||||
end
|
||||
self:Toast(d.name .. " 해금!")
|
||||
self:RenderSoulLabel()
|
||||
self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]),
|
||||
method('RenderSoulShop', `local defs = self.SoulShopDef or {}
|
||||
for i = 1, 4 do
|
||||
local base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)
|
||||
local d = defs[i]
|
||||
if d == nil then
|
||||
self:SetEntityEnabled(base, false)
|
||||
else
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", d.name)
|
||||
self:SetText(base .. "/Desc", d.desc)
|
||||
local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true
|
||||
if owned then
|
||||
self:SetText(base .. "/Status", "보유 중")
|
||||
elseif (self.SoulPoints or 0) >= d.cost then
|
||||
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매")
|
||||
else
|
||||
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족")
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('BindSoulShopButtons', `if self.SoulShopBound == true then
|
||||
return
|
||||
end
|
||||
self.SoulShopBound = true
|
||||
for i = 1, 4 do
|
||||
local idx = i
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i))
|
||||
if e ~= nil and e.ButtonComponent ~= nil then
|
||||
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
|
||||
end
|
||||
end`),
|
||||
method('ApplySoulUnlocks', `if self.SoulUnlocks == nil then return end
|
||||
if self.SoulUnlocks["meso"] == true then self.Gold = self.Gold + 60 end
|
||||
if self.SoulUnlocks["hp"] == true then
|
||||
self.PlayerMaxHp = self.PlayerMaxHp + 15
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
if self.SoulUnlocks["trim"] == true then
|
||||
for i = 1, #self.RunDeck do
|
||||
local cid = self.RunDeck[i]
|
||||
if cid == "Defend" or cid == "MagicGuard" or cid == "DarkSight" then
|
||||
table.remove(self.RunDeck, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.SoulUnlocks["relic"] == true then
|
||||
local nid = self:PickNewRelic()
|
||||
if nid ~= "" then self:AddRelic(nid) end
|
||||
end`),
|
||||
];
|
||||
193
tools/deck/cb/state.mjs
Normal file
193
tools/deck/cb/state.mjs
Normal file
@@ -0,0 +1,193 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const stateMethods = [
|
||||
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
|
||||
method('ShowState', `self:HideGameHud()
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect")
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", state == "lobby")
|
||||
if state == "map" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true)
|
||||
elseif state == "combat" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true)
|
||||
elseif state == "shop" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true)
|
||||
elseif state == "rest" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true)
|
||||
elseif state == "treasure" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true)
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
|
||||
method('ShowMainMenu', `self.SelectedClass = ""
|
||||
self:RenderAscension()
|
||||
self:ShowState("menu")
|
||||
self:SetText("/ui/DefaultGroup/MainMenu/Title", "메이플 덱 어드벤처")
|
||||
self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱을 만들어 모험을 시작하세요")
|
||||
self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임")
|
||||
self:BindMenuButtons()`),
|
||||
method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton")
|
||||
if buttonEntity ~= nil and buttonEntity.ButtonComponent ~= nil then
|
||||
if self.NewGameHandler ~= nil then
|
||||
buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)
|
||||
self.NewGameHandler = nil
|
||||
end
|
||||
self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowCharacterSelect() end)
|
||||
end
|
||||
local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
|
||||
if warrior ~= nil and warrior.ButtonComponent ~= nil then
|
||||
if self.WarriorSelectHandler ~= nil then
|
||||
warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler)
|
||||
self.WarriorSelectHandler = nil
|
||||
end
|
||||
self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end)
|
||||
end
|
||||
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
|
||||
if thief ~= nil and thief.ButtonComponent ~= nil then
|
||||
if self.ThiefSelectHandler ~= nil then
|
||||
thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler)
|
||||
self.ThiefSelectHandler = nil
|
||||
end
|
||||
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") end)
|
||||
end
|
||||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
||||
if mage ~= nil and mage.ButtonComponent ~= nil then
|
||||
if self.MageSelectHandler ~= nil then
|
||||
mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler)
|
||||
self.MageSelectHandler = nil
|
||||
end
|
||||
self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end)
|
||||
end
|
||||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
||||
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
||||
if self.AllDeckCloseHandler ~= nil then
|
||||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||
self.AllDeckCloseHandler = nil
|
||||
end
|
||||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||
end
|
||||
self:BindClassDeckTabs()
|
||||
local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton")
|
||||
if start ~= nil and start.ButtonComponent ~= nil then
|
||||
if self.StartGameHandler ~= nil then
|
||||
start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler)
|
||||
self.StartGameHandler = nil
|
||||
end
|
||||
self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)
|
||||
end
|
||||
local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton")
|
||||
if charBack ~= nil and charBack.ButtonComponent ~= nil then
|
||||
if self.CharBackHandler ~= nil then
|
||||
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
|
||||
self.CharBackHandler = nil
|
||||
end
|
||||
self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
|
||||
end
|
||||
local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus")
|
||||
if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then
|
||||
if self.AscMinusHandler ~= nil then
|
||||
ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler)
|
||||
self.AscMinusHandler = nil
|
||||
end
|
||||
self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end)
|
||||
end
|
||||
local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus")
|
||||
if ascPlus ~= nil and ascPlus.ButtonComponent ~= nil then
|
||||
if self.AscPlusHandler ~= nil then
|
||||
ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler)
|
||||
self.AscPlusHandler = nil
|
||||
end
|
||||
self.AscPlusHandler = ascPlus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(1) end)
|
||||
end`),
|
||||
method('ShowLobby', `self.SelectedClass = ""
|
||||
self:RenderAscension()
|
||||
self:RenderSoulLabel()
|
||||
self:ShowState("lobby")
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)
|
||||
self:BindLobbyButtons()
|
||||
self:BindMenuButtons()
|
||||
self:GoLobbyMap()`),
|
||||
method('GoLobbyMap', `self.LobbyTpTries = 0
|
||||
local eventId = 0
|
||||
local function go()
|
||||
self.LobbyTpTries = self.LobbyTpTries + 1
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
if lp.CurrentMapName ~= "${LOBBY_MAP}" then
|
||||
_TeleportService:TeleportToMapPosition(lp, ${LOBBY_SPAWN}, "${LOBBY_MAP}")
|
||||
end
|
||||
_TimerService:ClearTimer(eventId)
|
||||
elseif self.LobbyTpTries > 50 then
|
||||
_TimerService:ClearTimer(eventId)
|
||||
end
|
||||
end
|
||||
eventId = _TimerService:SetTimerRepeat(go, 0.1)`),
|
||||
method('OnLobbyNpcInteract', `if self.RunActive == true then
|
||||
return
|
||||
end
|
||||
if id == "run" then
|
||||
self:ShowCharacterSelect()
|
||||
elseif id == "codex" then
|
||||
self:ShowCodex()
|
||||
elseif id == "shop" then
|
||||
self:ShowSoulShop()
|
||||
elseif id == "board" then
|
||||
self:ShowBoard()
|
||||
end`, [{ Type: 'string', DefaultValue: '""', SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
method('RenderSoulLabel', `local s = self.SoulPoints or 0
|
||||
self:SetText("/ui/DefaultGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", s))
|
||||
self:SetText("/ui/DefaultGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", s))`),
|
||||
method('BindLobbyButtons', `if self.LobbyBound == true then
|
||||
return
|
||||
end
|
||||
self.LobbyBound = true
|
||||
local function bindClick(path, fn)
|
||||
local e = _EntityService:GetEntityByPath(path)
|
||||
if e ~= nil and e.ButtonComponent ~= nil then
|
||||
e:ConnectEvent(ButtonClickEvent, fn)
|
||||
end
|
||||
end
|
||||
bindClick("/ui/DefaultGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end)
|
||||
bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end)
|
||||
bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end)
|
||||
bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`),
|
||||
method('ShowCodex', `self.CodexMode = true
|
||||
self.ClassDeckMode = true
|
||||
local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
||||
if close ~= nil and close.ButtonComponent ~= nil then
|
||||
if self.AllDeckCloseHandler ~= nil then
|
||||
close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||
end
|
||||
self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||
end
|
||||
self:BindClassDeckTabs()
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
|
||||
self:SetClassDeckTab("warrior")
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end
|
||||
self:RenderAllDeck()`),
|
||||
method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`),
|
||||
method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`),
|
||||
];
|
||||
115
tools/deck/cb/tooltip.mjs
Normal file
115
tools/deck/cb/tooltip.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const tooltipMethods = [
|
||||
method('BuildCardKeywordTooltip', `if c == nil then
|
||||
return ""
|
||||
end
|
||||
local lines = {}
|
||||
local function add(name, desc)
|
||||
for i = 1, #lines do
|
||||
if string.find(lines[i], name .. ":", 1, true) == 1 then
|
||||
return
|
||||
end
|
||||
end
|
||||
table.insert(lines, name .. ": " .. desc)
|
||||
end
|
||||
local cardDesc = c.desc or ""
|
||||
if c.sly == true or string.find(cardDesc, "교활", 1, true) ~= nil then
|
||||
add("교활", "버려지면 비용 없이 사용됩니다.")
|
||||
end
|
||||
if c.retain == true or string.find(cardDesc, "보존", 1, true) ~= nil then
|
||||
add("보존", "턴 종료 시 버려지지 않고 손에 남습니다.")
|
||||
end
|
||||
if c.dex ~= nil and c.dex > 0 or string.find(cardDesc, "민첩", 1, true) ~= nil then
|
||||
add("민첩", "카드로 얻는 방어도가 증가합니다.")
|
||||
end
|
||||
if c.thorns ~= nil and c.thorns > 0 or string.find(cardDesc, "가시", 1, true) ~= nil then
|
||||
add("가시", "피해를 받으면 공격자에게 반사 피해를 줍니다.")
|
||||
end
|
||||
if c.exhaust == true or string.find(cardDesc, "소멸.", 1, true) ~= nil then
|
||||
add("소멸", "사용 후 소멸 덱으로 이동해 이번 전투 동안 다시 나오지 않습니다.")
|
||||
end
|
||||
if string.find(cardDesc, "선천성", 1, true) ~= nil then
|
||||
add("선천성", "전투 시작 시 손패에 들어옵니다.")
|
||||
end
|
||||
if c.vuln ~= nil and c.vuln > 0 then
|
||||
add("취약", "받는 공격 피해가 50% 증가합니다.")
|
||||
end
|
||||
if c.weak ~= nil and c.weak > 0 then
|
||||
add("약화", "주는 공격 피해가 25% 감소합니다.")
|
||||
end
|
||||
if c.poison ~= nil and c.poison > 0 then
|
||||
add("중독", "턴 시작 시 체력을 잃고 수치가 1 감소합니다.")
|
||||
end
|
||||
if c.pierce == true then
|
||||
add("관통", "방어도를 무시하고 피해를 줍니다.")
|
||||
end
|
||||
if c.aoe == true then
|
||||
add("전체", "모든 적에게 적용됩니다.")
|
||||
end
|
||||
if c.kind == "Power" then
|
||||
add("파워", "사용하면 전투 동안 지속 효과로 남습니다.")
|
||||
end
|
||||
if c.unplayable == true then
|
||||
add("저주", "사용할 수 없고 손패를 방해합니다.")
|
||||
end
|
||||
local out = ""
|
||||
for i = 1, #lines do
|
||||
if i > 1 then out = out .. "\\n" end
|
||||
out = out .. lines[i]
|
||||
end
|
||||
return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'string'),
|
||||
method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||||
local tx = 0
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
tx = e.UITransformComponent.anchoredPosition.x
|
||||
e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1)
|
||||
end
|
||||
local c = self.Cards[cardId]
|
||||
if c ~= nil then
|
||||
local tip = self:BuildCardKeywordTooltip(c)
|
||||
if tip ~= "" then
|
||||
local tipX = tx + 270
|
||||
if tx > 180 then tipX = tx - 270 end
|
||||
if tipX > 760 then tipX = tx - 270 end
|
||||
if tipX < -760 then tipX = tx + 270 end
|
||||
self:ShowTooltipAt("키워드", tip, tipX, 90)
|
||||
else
|
||||
self:HideTooltip()
|
||||
end
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
end
|
||||
self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('ShowTooltip', `self:ShowTooltipAt(name, desc, x, 400)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
|
||||
]),
|
||||
method('ShowTooltipAt', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc)
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox")
|
||||
if e ~= nil then
|
||||
if e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.anchoredPosition = Vector2(x, y)
|
||||
end
|
||||
e.Enable = true
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' },
|
||||
]),
|
||||
method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`),
|
||||
];
|
||||
404
tools/deck/gen-slaydeck.mjs
Normal file
404
tools/deck/gen-slaydeck.mjs
Normal file
@@ -0,0 +1,404 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs';
|
||||
import { prop, method, codeblock, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from './lib/codeblock.mjs';
|
||||
import { bootMethods } from './cb/boot.mjs';
|
||||
import { stateMethods } from './cb/state.mjs';
|
||||
import { soulMethods } from './cb/soul.mjs';
|
||||
import { charSelectMethods } from './cb/charselect.mjs';
|
||||
import { runMethods } from './cb/run.mjs';
|
||||
import { deckTurnMethods } from './cb/deckturn.mjs';
|
||||
import { deckViewMethods } from './cb/deckview.mjs';
|
||||
import { handMethods } from './cb/hand.mjs';
|
||||
import { combatMethods } from './cb/combat.mjs';
|
||||
import { jobMethods } from './cb/jobs.mjs';
|
||||
import { runEndMethods } from './cb/runend.mjs';
|
||||
import { renderMethods } from './cb/render.mjs';
|
||||
import { rewardMethods } from './cb/reward.mjs';
|
||||
import { itemMethods } from './cb/items.mjs';
|
||||
import { tooltipMethods } from './cb/tooltip.mjs';
|
||||
import { mapMethods } from './cb/map.mjs';
|
||||
import { shopMethods } from './cb/shop.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from './lib/ui-helpers.mjs';
|
||||
import { buildDeckHud } from './hud/deckhud.mjs';
|
||||
import { buildDeckInspect } from './hud/deckinspect.mjs';
|
||||
import { buildDeckAll } from './hud/deckall.mjs';
|
||||
import { buildCombat } from './hud/combat.mjs';
|
||||
import { buildReward } from './hud/reward.mjs';
|
||||
import { buildMap } from './hud/map.mjs';
|
||||
import { buildShop } from './hud/shop.mjs';
|
||||
import { buildRest } from './hud/rest.mjs';
|
||||
import { buildTreasure } from './hud/treasure.mjs';
|
||||
import { buildJobChoice } from './hud/jobchoice.mjs';
|
||||
import { buildJobSelect } from './hud/jobselect.mjs';
|
||||
import { buildLobby } from './hud/lobby.mjs';
|
||||
import { buildBoard } from './hud/board.mjs';
|
||||
import { buildSoulShop } from './hud/soulshop.mjs';
|
||||
import { buildMainMenu } from './hud/mainmenu.mjs';
|
||||
|
||||
function upsertUi() {
|
||||
const ui = JSON.parse(readFileSync(UI_FILE, 'utf8'));
|
||||
const E = ui.ContentProto.Entities;
|
||||
// CardHand는 스톡 섹션이라 과거 생성된 단색판(NamePlate/CostPlate)이 잔존 → 프레임 이미지 도입으로 제거
|
||||
const obsoletePlate = /^\/ui\/DefaultGroup\/CardHand\/Card\d+\/(NamePlate|CostPlate)$/;
|
||||
ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e) && !obsoletePlate.test(e.path));
|
||||
|
||||
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
|
||||
const uiSections = new Map();
|
||||
const emit = (section, entities) => {
|
||||
if (uiSections.has(section)) {
|
||||
throw new Error(`[gen-slaydeck] duplicate generated UI section: ${section}`);
|
||||
}
|
||||
uiSections.set(section, entities);
|
||||
};
|
||||
|
||||
for (const path of DISABLED_STOCK_CONTROLS.map((name) => uiPath(name))) {
|
||||
const e = byPath.get(path);
|
||||
if (e != null) {
|
||||
e.jsonString.enable = false;
|
||||
e.jsonString.visible = false;
|
||||
for (const component of e.jsonString['@components'] || []) {
|
||||
component.Enable = false;
|
||||
if (component.RaycastTarget != null) component.RaycastTarget = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시
|
||||
const previewIds = Object.keys(CARDS.cards);
|
||||
const cards = Array.from({ length: 10 }, (_, i) => {
|
||||
const c = CARDS.cards[previewIds[i % previewIds.length]];
|
||||
return { name: c.name, cost: String(c.cost), desc: c.desc, frame: frameRuid(c) };
|
||||
});
|
||||
|
||||
// 손패 슬롯 10개 (최대 손패 한도). Card1~5는 기존 엔티티, Card6~10은 신규 생성.
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const cardPath = `/ui/DefaultGroup/CardHand/Card${i}`;
|
||||
let card = byPath.get(cardPath);
|
||||
if (!card) {
|
||||
card = entity({
|
||||
id: guid('dck', 500 + i),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent',
|
||||
displayOrder: 4,
|
||||
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: 0, y: 0 } }),
|
||||
sprite({ color: WHITE, type: 0, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
});
|
||||
ui.ContentProto.Entities.push(card);
|
||||
byPath.set(cardPath, card);
|
||||
}
|
||||
const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||||
const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent');
|
||||
const sx = -680 + (i - 1) * 150;
|
||||
tr.RectSize = { x: CARD_W, y: CARD_H };
|
||||
tr.anchoredPosition = { x: sx, y: 0 };
|
||||
tr.OffsetMin = { x: sx - CARD_W / 2, y: -CARD_H / 2 };
|
||||
tr.OffsetMax = { x: sx + CARD_W / 2, y: CARD_H / 2 };
|
||||
sp.ImageRUID = { DataId: cards[i - 1].frame };
|
||||
sp.Type = 0;
|
||||
sp.Color = WHITE;
|
||||
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';
|
||||
}
|
||||
if (!comps.some((c) => c['@type'] === 'MOD.Core.UITouchReceiveComponent')) {
|
||||
comps.push({ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true });
|
||||
}
|
||||
if (!card.componentNames.includes('MOD.Core.UITouchReceiveComponent')) {
|
||||
card.componentNames += ',MOD.Core.UITouchReceiveComponent';
|
||||
}
|
||||
card.jsonString.enable = true;
|
||||
card.jsonString.visible = true;
|
||||
|
||||
const handLayout = cardFaceLayout(CARD_W);
|
||||
const previewValues = { Cost: cards[i - 1].cost, Name: cards[i - 1].name, Desc: cards[i - 1].desc };
|
||||
const children = handLayout.texts.map(([suffix, cfg]) => [suffix, { ...cfg, value: previewValues[suffix] }]);
|
||||
for (const [suffix, cfg] of children) {
|
||||
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
let child = byPath.get(path);
|
||||
if (!child) {
|
||||
child = entity({
|
||||
id: guid('dck', i * 10 + children.findIndex(([s]) => s === suffix)),
|
||||
path,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: dOrder,
|
||||
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, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
|
||||
],
|
||||
});
|
||||
ui.ContentProto.Entities.push(child);
|
||||
byPath.set(path, child);
|
||||
} else {
|
||||
child.id = guid('dck', i * 10 + children.findIndex(([s]) => s === suffix));
|
||||
child.jsonString.enable = true;
|
||||
child.jsonString.visible = true;
|
||||
child.jsonString.displayOrder = dOrder;
|
||||
const ctr = child.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||||
if (ctr) {
|
||||
const pivot = { x: 0.5, y: 0.5 };
|
||||
ctr.RectSize = cfg.size;
|
||||
ctr.anchoredPosition = cfg.pos;
|
||||
ctr.OffsetMin = { x: cfg.pos.x - pivot.x * cfg.size.x, y: cfg.pos.y - pivot.y * cfg.size.y };
|
||||
ctr.OffsetMax = { x: cfg.pos.x + (1 - pivot.x) * cfg.size.x, y: cfg.pos.y + (1 - pivot.y) * cfg.size.y };
|
||||
}
|
||||
child.jsonString['@components'][2].Text = cfg.value;
|
||||
child.jsonString['@components'][2].FontSize = cfg.fontSize;
|
||||
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
|
||||
child.jsonString['@components'][2].FontColor = cfg.color;
|
||||
child.jsonString['@components'][2].Bold = cfg.bold;
|
||||
child.jsonString['@components'][2].DropShadow = cfg.dropShadow === true;
|
||||
child.jsonString['@components'][2].DropShadowDistance = cfg.dropShadow === true ? 18 : 32;
|
||||
child.jsonString['@components'][2].OutlineWidth = cfg.outlineWidth || 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 프레임 이미지가 이름판·코스트판을 내장하므로 Art만 유지 (잔존 NamePlate/CostPlate는 upsertUi 초입에서 제거)
|
||||
const frameKids = [
|
||||
['Art', 5, handLayout.art, WHITE, 0],
|
||||
];
|
||||
for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) {
|
||||
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||||
let fe = byPath.get(fPath);
|
||||
if (!fe) {
|
||||
fe = entity({
|
||||
id: guid('dck', 200 + i * 10 + dOrder),
|
||||
path: fPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: dOrder,
|
||||
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, type: spriteType, raycast: false }),
|
||||
],
|
||||
});
|
||||
ui.ContentProto.Entities.push(fe);
|
||||
byPath.set(fPath, fe);
|
||||
} else {
|
||||
const ftr = fe.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||||
if (ftr) {
|
||||
ftr.RectSize = cfg.size;
|
||||
ftr.anchoredPosition = cfg.pos;
|
||||
ftr.OffsetMin = { x: cfg.pos.x - cfg.size.x / 2, y: cfg.pos.y - cfg.size.y / 2 };
|
||||
ftr.OffsetMax = { x: cfg.pos.x + cfg.size.x / 2, y: cfg.pos.y + cfg.size.y / 2 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit('DeckHud', buildDeckHud());
|
||||
|
||||
emit('DeckInspectHud', buildDeckInspect());
|
||||
|
||||
emit('DeckAllHud', buildDeckAll());
|
||||
|
||||
emit('CombatHud', buildCombat());
|
||||
|
||||
emit('RewardHud', buildReward());
|
||||
|
||||
emit('MapHud', buildMap());
|
||||
|
||||
emit('ShopHud', buildShop());
|
||||
|
||||
emit('RestHud', buildRest());
|
||||
|
||||
// 유물 방 — 보물 상자 (P8)
|
||||
emit('TreasureHud', buildTreasure());
|
||||
|
||||
// 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직
|
||||
emit('JobChoiceHud', buildJobChoice());
|
||||
|
||||
emit('JobSelectHud', buildJobSelect());
|
||||
|
||||
emit('MainMenu', buildMainMenu());
|
||||
|
||||
// ── LobbyHud — 반복 런의 허브. NPC 클릭으로 런시작/도감/영혼상점/게시판 ──
|
||||
emit('LobbyHud', buildLobby());
|
||||
|
||||
// ── BoardHud — 게시판(공지/팁) ──
|
||||
emit('BoardHud', buildBoard());
|
||||
|
||||
// ── SoulShopHud — 영혼 메타 상점 (Phase 9에서 해금 항목·구매 로직 채움) ──
|
||||
emit('SoulShopHud', buildSoulShop());
|
||||
|
||||
for (const section of UI_APPEND_ORDER) {
|
||||
const entities = uiSections.get(section);
|
||||
if (entities == null) {
|
||||
throw new Error(`[gen-slaydeck] missing generated UI section: ${section}`);
|
||||
}
|
||||
appendUiSection(ui, section, entities);
|
||||
}
|
||||
|
||||
// 엔티티 id 유일성 검증 — 같은 id가 다른 path에 재배정되면 메이커 refresh 병합이 꼬임
|
||||
const seenIds = new Map();
|
||||
for (const e of ui.ContentProto.Entities) {
|
||||
const prev = seenIds.get(e.id);
|
||||
if (prev != null) throw new Error(`[gen-slaydeck] 엔티티 id 중복: ${e.id} (${prev} ↔ ${e.path})`);
|
||||
seenIds.set(e.id, e.path);
|
||||
}
|
||||
|
||||
JSON.parse(JSON.stringify(ui));
|
||||
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function writeCodeblocks() {
|
||||
const combat = codeblock('SlayDeckController', 'SlayDeckController', [
|
||||
prop('any', 'DrawPile'),
|
||||
prop('any', 'DiscardPile'),
|
||||
prop('any', 'ExhaustPile'),
|
||||
prop('any', 'Hand'),
|
||||
prop('number', 'Energy', '0'),
|
||||
prop('number', 'MaxEnergy', '3'),
|
||||
prop('number', 'Turn', '0'),
|
||||
prop('number', 'TweenEventId', '0'),
|
||||
prop('number', 'CardHoverTweenId', '0'),
|
||||
prop('number', 'DmgPopSeq', '0'),
|
||||
prop('any', 'EndTurnHandler'),
|
||||
prop('any', 'NewGameHandler'),
|
||||
prop('any', 'WarriorSelectHandler'),
|
||||
prop('any', 'ThiefSelectHandler'),
|
||||
prop('any', 'MageSelectHandler'),
|
||||
prop('any', 'WarriorDeckTabHandler'),
|
||||
prop('any', 'ThiefDeckTabHandler'),
|
||||
prop('any', 'MageDeckTabHandler'),
|
||||
prop('any', 'AscMinusHandler'),
|
||||
prop('any', 'AscPlusHandler'),
|
||||
prop('any', 'JobOpts'),
|
||||
prop('any', 'Jobs'),
|
||||
prop('number', 'AscensionLevel', '0'),
|
||||
prop('number', 'AscensionUnlocked', '0'),
|
||||
prop('any', 'StartGameHandler'),
|
||||
prop('any', 'CharBackHandler'),
|
||||
prop('string', 'SelectedClass', '""'),
|
||||
prop('any', 'DrawPileHandler'),
|
||||
prop('any', 'DiscardPileHandler'),
|
||||
prop('any', 'ExhaustPileHandler'),
|
||||
prop('any', 'DeckInspectCloseHandler'),
|
||||
prop('any', 'AllDeckHandler'),
|
||||
prop('any', 'AllDeckCloseHandler'),
|
||||
prop('number', 'SoulPoints', '0'),
|
||||
prop('boolean', 'LobbyBound', 'false'),
|
||||
prop('number', 'LobbyTpTries', '0'),
|
||||
prop('boolean', 'CodexMode', 'false'),
|
||||
prop('any', 'CodexCards'),
|
||||
prop('boolean', 'ClassDeckMode', 'false'),
|
||||
prop('any', 'ClassDeckCards'),
|
||||
prop('string', 'ClassDeckTitle', '""'),
|
||||
prop('string', 'ClassDeckClass', '""'),
|
||||
prop('any', 'SoulUnlocks'),
|
||||
prop('any', 'SoulShopDef'),
|
||||
prop('boolean', 'SoulShopBound', 'false'),
|
||||
prop('string', 'DeckInspectKind', '""'),
|
||||
prop('boolean', 'DeckAllOpen', 'false'),
|
||||
prop('any', 'Cards'),
|
||||
prop('any', 'CardFrames'),
|
||||
prop('any', 'NodeIcons'),
|
||||
prop('any', 'ClassPortraits'),
|
||||
prop('any', 'ClassToFrame'),
|
||||
prop('number', 'PlayerHp', '0'),
|
||||
prop('number', 'PlayerMaxHp', '80'),
|
||||
prop('number', 'PlayerBlock', '0'),
|
||||
prop('number', 'PlayerDex', '0'),
|
||||
prop('number', 'PlayerThorns', '0'),
|
||||
prop('boolean', 'CombatOver', 'false'),
|
||||
prop('any', 'Monsters'),
|
||||
prop('any', 'Registered'),
|
||||
prop('number', 'TargetIndex', '1'),
|
||||
prop('any', 'RunDeck'),
|
||||
prop('number', 'Gold', '0'),
|
||||
prop('number', 'Floor', '0'),
|
||||
prop('number', 'RunLength', String(RUN_LENGTH)),
|
||||
prop('any', 'RewardChoices'),
|
||||
prop('boolean', 'RunActive', 'false'),
|
||||
prop('any', 'Enemies'),
|
||||
prop('any', 'MapNodes'),
|
||||
prop('any', 'MapStart'),
|
||||
prop('string', 'CurrentNodeId', '""'),
|
||||
prop('string', 'CurrentEnemyId', '""'),
|
||||
prop('any', 'ShopChoices'),
|
||||
prop('any', 'ShopBought'),
|
||||
prop('any', 'Relics'),
|
||||
prop('any', 'RunRelics'),
|
||||
prop('any', 'RelicPool'),
|
||||
prop('string', 'ShopRelic', '""'),
|
||||
prop('boolean', 'ShopRelicBought', 'false'),
|
||||
prop('number', 'DragSlot', '0'),
|
||||
prop('number', 'DragTargetIndex', '0'),
|
||||
prop('boolean', 'FxBusy', 'false'),
|
||||
prop('boolean', 'TurnBusy', 'false'),
|
||||
prop('number', 'PlayerStr', '0'),
|
||||
prop('number', 'PlayerWeak', '0'),
|
||||
prop('number', 'PlayerVuln', '0'),
|
||||
prop('any', 'PlayerPowers'),
|
||||
prop('any', 'Potions'),
|
||||
prop('any', 'RunPotions'),
|
||||
prop('number', 'PotionSlots', String(POTIONS.baseSlots)),
|
||||
prop('string', 'ShopPotion', '""'),
|
||||
prop('boolean', 'ShopPotionBought', 'false'),
|
||||
prop('number', 'FightAttackCount', '0'),
|
||||
prop('boolean', 'FirstHpLossDone', 'false'),
|
||||
prop('number', 'ClayBlockNext', '0'),
|
||||
prop('number', 'PotionMenuSlot', '0'),
|
||||
prop('number', 'Depth', '0'),
|
||||
prop('any', 'VisitedNodes'),
|
||||
prop('boolean', 'ChestOpened', 'false'),
|
||||
prop('string', 'PlayerJob', '""'),
|
||||
prop('number', 'DiscardSelectRemaining', '0'),
|
||||
prop('number', 'DiscardSelectTotal', '0'),
|
||||
prop('number', 'DiscardPostShiv', '0'),
|
||||
prop('number', 'DiscardShivPerPick', '0'),
|
||||
], [
|
||||
...bootMethods,
|
||||
...stateMethods,
|
||||
...soulMethods,
|
||||
...charSelectMethods,
|
||||
...runMethods,
|
||||
...deckTurnMethods,
|
||||
...deckViewMethods,
|
||||
...handMethods,
|
||||
...combatMethods,
|
||||
...jobMethods,
|
||||
...runEndMethods,
|
||||
...renderMethods,
|
||||
...rewardMethods,
|
||||
...itemMethods,
|
||||
...tooltipMethods,
|
||||
...mapMethods,
|
||||
...shopMethods,
|
||||
]);
|
||||
for (const m of combat.ContentProto.Json.Methods) {
|
||||
if (m.ExecSpace === 0) m.ExecSpace = 6; // 기본은 ClientOnly(6), 서버 RPC(Server=1·Client=2) 명시값은 보존
|
||||
}
|
||||
writeFileSync('RootDesk/MyDesk/SlayDeckController.codeblock', JSON.stringify(combat, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function patchCommon() {
|
||||
const common = JSON.parse(readFileSync(COMMON_FILE, 'utf8'));
|
||||
const entity = common.ContentProto.Entities.find((e) => e.path === '/common');
|
||||
entity.componentNames = 'script.SlayDeckController';
|
||||
entity.jsonString['@components'] = [
|
||||
{ '@type': 'script.SlayDeckController', Enable: true, Energy: 0, MaxEnergy: 3, Turn: 0, TweenEventId: 0 },
|
||||
];
|
||||
JSON.parse(JSON.stringify(common));
|
||||
writeFileSync(COMMON_FILE, JSON.stringify(common, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
upsertUi();
|
||||
writeCodeblocks();
|
||||
patchCommon();
|
||||
|
||||
console.log('Slay deck UI and combat codeblocks generated.');
|
||||
58
tools/deck/hud/board.mjs
Normal file
58
tools/deck/hud/board.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
|
||||
export function buildBoard() {
|
||||
const board = [];
|
||||
let brdId = 0;
|
||||
const boardRoot = entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 14,
|
||||
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.95 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
boardRoot.jsonString.enable = false;
|
||||
board.push(boardRoot);
|
||||
board.push(entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud/Title',
|
||||
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: 700, y: 60 }, pos: { x: 0, y: 400 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '게시판', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
board.push(entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud/Body',
|
||||
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: 1100, y: 520 }, pos: { x: 0, y: 20 } }),
|
||||
sprite({ color: { r: 0.1, g: 0.12, b: 0.16, a: 0.9 }, type: 1 }),
|
||||
text({ value: '· 카드는 직업/등급에 따라 보상 확률이 다릅니다.\n· 몬스터는 매 턴 정해진 행동 중 하나를 무작위로 합니다.\n· 일부 몬스터는 덱에 저주 카드(상처/화상)를 넣습니다.\n· 손패는 최대 10장, 초과분은 자동으로 버려집니다.\n· 2차 전직 후 보스를 잡으면 영혼이 쌓입니다.\n· 영혼은 상인 NPC에서 덱빌딩 해금에 사용합니다.', fontSize: 24, bold: false, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
board.push(entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud/Close',
|
||||
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: -380 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
return board;
|
||||
}
|
||||
494
tools/deck/hud/combat.mjs
Normal file
494
tools/deck/hud/combat.mjs
Normal file
@@ -0,0 +1,494 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
|
||||
export function buildCombat() {
|
||||
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 }),
|
||||
],
|
||||
}));
|
||||
const SLOT_W = 140, SLOT_H = 96;
|
||||
for (let i = 1; i <= MAX_MONSTERS; i++) {
|
||||
const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`;
|
||||
const slot = entity({
|
||||
id: guid('cmb', 40 + i),
|
||||
path: base,
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: 20 + i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }),
|
||||
sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
});
|
||||
slot.jsonString.enable = false;
|
||||
combat.push(slot);
|
||||
const targetFrame = entity({
|
||||
id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
targetFrame.jsonString.enable = false;
|
||||
combat.push(targetFrame);
|
||||
const targetMarker = entity({
|
||||
id: guid('cmb', 360 + i), path: `${base}/TargetMarker`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 116, y: 116 }, pos: { x: 0, y: 2 } }),
|
||||
sprite({ color: { r: 0.95, g: 0.08, b: 0.05, a: 0.92 }, type: 1 }),
|
||||
text({ value: '+', fontSize: 72, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 4 }),
|
||||
],
|
||||
});
|
||||
targetMarker.jsonString.enable = false;
|
||||
combat.push(targetMarker);
|
||||
const targetLabel = entity({
|
||||
id: guid('cmb', 370 + i), path: `${base}/TargetMarker/Label`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 10,
|
||||
components: [
|
||||
transform({ parentW: 116, parentH: 116, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 112, y: 28 }, pos: { x: 0, y: -52 } }),
|
||||
sprite({ color: { r: 0.08, g: 0.02, b: 0.02, a: 0.86 }, type: 1 }),
|
||||
text({ value: 'TARGET', fontSize: 18, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 3 }),
|
||||
],
|
||||
});
|
||||
targetLabel.jsonString.enable = false;
|
||||
combat.push(targetLabel);
|
||||
const actFrame = entity({
|
||||
id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: { r: 0.95, g: 0.3, b: 0.25, a: 0.3 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
actFrame.jsonString.enable = false;
|
||||
combat.push(actFrame);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 30 }, pos: { x: 0, y: 34 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 26 }, pos: { x: 0, y: 6 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }),
|
||||
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }),
|
||||
sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 40, y: 24 }, pos: { x: 0, y: -36 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const dmgPopBase = `/ui/DefaultGroup/CombatHud/DmgPop${i}`;
|
||||
const dmgPop = entity({
|
||||
id: guid('cmb', 250 + i), path: dmgPopBase, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 80 + i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 192, y: 56 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1 }),
|
||||
],
|
||||
});
|
||||
dmgPop.jsonString.enable = false;
|
||||
combat.push(dmgPop);
|
||||
for (let d = 0; d < DAMAGE_POP_MAX_DIGITS; d++) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 380 + i * 10 + d), path: `${dmgPopBase}/Digit${d + 1}`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 90 + i,
|
||||
components: [
|
||||
transform({ parentW: 192, parentH: 56, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: DAMAGE_POP_DIGIT_W, y: DAMAGE_POP_DIGIT_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ dataId: DAMAGE_DIGIT_RUIDS[0], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
const mBlockBadge = entity({
|
||||
id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 6,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 36 }, pos: { x: -HP_BAR_W / 2 - 30, y: -14 } }),
|
||||
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
mBlockBadge.jsonString.enable = false;
|
||||
combat.push(mBlockBadge);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 280 + i), path: `${base}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 40, parentH: 36, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 32 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '0', fontSize: 17, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 290 + i), path: `${base}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 7,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 60, y: 22 }, pos: { x: 0, y: -58 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 15, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel';
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 210), path: PP, 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: 96 }, pos: { x: -760, y: -494 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: PANEL_BG, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }),
|
||||
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 14 }, pos: { x: -94, y: -6 } }),
|
||||
sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const blockBadge = entity({
|
||||
id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }),
|
||||
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
blockBadge.jsonString.enable = false;
|
||||
combat.push(blockBadge);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 217), path: `${PP}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 6,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 22 }, pos: { x: 0, y: -44 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 14, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const playerDmgPop = entity({
|
||||
id: guid('cmb', 260), path: `${PP}/DmgPop`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 30 }, pos: { x: 16, y: 40 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 22, bold: true, color: { r: 1, g: 0.4, b: 0.35, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
playerDmgPop.jsonString.enable = false;
|
||||
combat.push(playerDmgPop);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 200),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar',
|
||||
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: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
const topTexts = [
|
||||
['Floor', -520, 160, '막 1/3', GOLD],
|
||||
['Gold', -360, 160, '메소 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
|
||||
];
|
||||
topTexts.forEach(([suffix, x, w, value, color], ti) => {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 201 + ti),
|
||||
path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`,
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: ti,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize: 22, bold: true, color, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 209),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar/MesoIcon',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 26, y: 26 }, pos: { x: -432, y: 0 } }),
|
||||
sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 300 + i),
|
||||
path: `/ui/DefaultGroup/CombatHud/TopBar/RelicSlot${i}`,
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent',
|
||||
displayOrder: 3 + i,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: -240 + (i - 1) * 48, y: 0 } }),
|
||||
sprite({ color: { r: 0.15, g: 0.16, b: 0.2, a: 0.6 }, type: 0, raycast: true }),
|
||||
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
|
||||
],
|
||||
}));
|
||||
}
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 311),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 14,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 60, y: 30 }, pos: { x: 192, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 18, bold: true, color: { r: 0.8, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 320 + i),
|
||||
path: `/ui/DefaultGroup/CombatHud/TopBar/PotionSlot${i}`,
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent',
|
||||
displayOrder: 14 + i,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: 240 + (i - 1) * 44, y: 0 } }),
|
||||
sprite({ color: { r: 0.22, g: 0.25, b: 0.3, a: 0.9 }, type: 0, raycast: true }),
|
||||
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
|
||||
],
|
||||
}));
|
||||
}
|
||||
const tooltipBox = entity({
|
||||
id: guid('cmb', 330),
|
||||
path: '/ui/DefaultGroup/CombatHud/TooltipBox',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 20,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 150 }, pos: { x: 0, y: 400 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.04, g: 0.05, b: 0.08, a: 0.96 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
tooltipBox.jsonString.enable = false;
|
||||
combat.push(tooltipBox);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 331),
|
||||
path: '/ui/DefaultGroup/CombatHud/TooltipBox/Name',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 28 }, pos: { x: 0, y: 52 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 19, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 332),
|
||||
path: '/ui/DefaultGroup/CombatHud/TooltipBox/Desc',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 102 }, pos: { x: 0, y: -18 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
const discardPrompt = entity({
|
||||
id: guid('cmb', 333),
|
||||
path: '/ui/DefaultGroup/CombatHud/DiscardPrompt',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 22,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 520, y: 48 }, pos: { x: 0, y: -260 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.86 }, type: 1 }),
|
||||
text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
discardPrompt.jsonString.enable = false;
|
||||
combat.push(discardPrompt);
|
||||
const potionMenu = entity({
|
||||
id: guid('cmb', 340),
|
||||
path: '/ui/DefaultGroup/CombatHud/PotionMenu',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 21,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 180 }, pos: { x: 0, y: 120 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.07, g: 0.08, b: 0.12, a: 0.97 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
potionMenu.jsonString.enable = false;
|
||||
combat.push(potionMenu);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 341),
|
||||
path: '/ui/DefaultGroup/CombatHud/PotionMenu/Title',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 36 }, pos: { x: 0, y: 52 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 19, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const pmButtons = [
|
||||
['Use', '사용', -120, { r: 0.32, g: 0.55, b: 0.36, a: 1 }],
|
||||
['Toss', '버리기', 0, { r: 0.6, g: 0.32, b: 0.3, a: 1 }],
|
||||
['Close', '닫기', 120, { r: 0.25, g: 0.28, b: 0.35, a: 1 }],
|
||||
];
|
||||
pmButtons.forEach(([suffix, label, x, color], bi) => {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 342 + bi),
|
||||
path: `/ui/DefaultGroup/CombatHud/PotionMenu/${suffix}`,
|
||||
modelId: 'uibutton', entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1 + bi,
|
||||
components: [
|
||||
transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 104, y: 46 }, pos: { x, y: -40 } }),
|
||||
sprite({ color, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: label, fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 205),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton',
|
||||
modelId: 'uibutton', entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 140, y: 40 }, pos: { x: 528, y: 0 } }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '모든덱보기', fontSize: 18, bold: true, color: GOLD, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
const skillFx = entity({
|
||||
id: guid('cmb', 230), path: '/ui/DefaultGroup/CombatHud/SkillFx',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 30,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 110, y: 110 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }),
|
||||
],
|
||||
});
|
||||
skillFx.jsonString.enable = false;
|
||||
combat.push(skillFx);
|
||||
const result = entity({
|
||||
id: guid('cmb', 2),
|
||||
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);
|
||||
return combat;
|
||||
}
|
||||
159
tools/deck/hud/deckall.mjs
Normal file
159
tools/deck/hud/deckall.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
|
||||
export function buildDeckAll() {
|
||||
const allDeck = [];
|
||||
const allHud = entity({
|
||||
id: guid('all', 0),
|
||||
path: '/ui/DefaultGroup/DeckAllHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 16,
|
||||
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.78 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
allHud.jsonString.enable = false;
|
||||
allDeck.push(allHud);
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 1),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Panel',
|
||||
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: 1080, y: 800 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 2),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Title',
|
||||
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: 760, y: 54 }, pos: { x: 0, y: 380 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '모든 덱', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 3),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Close',
|
||||
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: 78, y: 52 }, pos: { x: 486, y: 380 } }),
|
||||
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const deckTabs = [
|
||||
{ key: 'Warrior', label: '전사', x: -210 },
|
||||
{ key: 'Thief', label: '도적', x: 0 },
|
||||
{ key: 'Mage', label: '마법사', x: 210 },
|
||||
];
|
||||
for (let i = 0; i < deckTabs.length; i++) {
|
||||
const tab = deckTabs[i];
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 10 + i),
|
||||
path: `/ui/DefaultGroup/DeckAllHud/${tab.key}Tab`,
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3 + i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 170, y: 46 }, pos: { x: tab.x, y: 318 } }),
|
||||
sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: tab.label, fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 4),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Empty',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
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: 40 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '덱이 없습니다', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 5),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Grid',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 980, y: 620 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
|
||||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||||
],
|
||||
}));
|
||||
const ALL_DECK_CARD_COUNT = 120;
|
||||
const ALL_DECK_CARD_W = 158;
|
||||
const ALL_DECK_CARD_H = 214;
|
||||
// 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조
|
||||
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
|
||||
const allBase = 6 + (i - 1) * 7;
|
||||
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
|
||||
const card = entity({
|
||||
id: guid('all2', allBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
|
||||
],
|
||||
});
|
||||
card.jsonString.enable = false;
|
||||
allDeck.push(card);
|
||||
const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W);
|
||||
for (const [tIdx, [suffix, cfg]] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
allDeck.push(entity({
|
||||
id: guid('all2', allBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: dOrder,
|
||||
components: [
|
||||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_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, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
allDeck.push(entity({
|
||||
id: guid('all2', allBase + 6),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: allDeckLayout.art.size, pos: allDeckLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
return allDeck;
|
||||
}
|
||||
122
tools/deck/hud/deckhud.mjs
Normal file
122
tools/deck/hud/deckhud.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
|
||||
export function buildDeckHud() {
|
||||
const hud = [];
|
||||
const add = (e) => hud.push(e);
|
||||
|
||||
add(entity({
|
||||
id: guid('hud', 0),
|
||||
path: '/ui/DefaultGroup/DeckHud',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
],
|
||||
}));
|
||||
|
||||
for (const pile of [
|
||||
{ key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.20, b: 0.25, a: 1 } },
|
||||
{ key: 'ExhaustPile', x: 430, label: '소멸 덱', count: '0', color: { r: 0.13, g: 0.13, b: 0.18, a: 1 } },
|
||||
{ key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } },
|
||||
]) {
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: `/ui/DefaultGroup/DeckHud/${pile.key}`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: pile.key === 'DrawPile' ? 0 : pile.key === 'ExhaustPile' ? 1 : 2,
|
||||
components: [
|
||||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: pile.color, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: pile.count, fontSize: 42, bold: true }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EndTurnButton',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 160 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '턴 종료', fontSize: 28, bold: true, color: GOLD, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 160 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
|
||||
return hud;
|
||||
}
|
||||
138
tools/deck/hud/deckinspect.mjs
Normal file
138
tools/deck/hud/deckinspect.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
|
||||
export function buildDeckInspect() {
|
||||
const inspect = [];
|
||||
const inspectHud = entity({
|
||||
id: guid('ins', 0),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 15,
|
||||
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.78 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
inspectHud.jsonString.enable = false;
|
||||
inspect.push(inspectHud);
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 1),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Panel',
|
||||
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: 1040, y: 760 }, pos: { x: 0, y: 10 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 2),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Title',
|
||||
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: 720, y: 54 }, pos: { x: 0, y: 350 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '\uB371 \uBCF4\uAE30', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 3),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Close',
|
||||
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: 78, y: 52 }, pos: { x: 466, y: 350 } }),
|
||||
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 4),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Empty',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
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: '\uCE74\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 5),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Grid',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 950, y: 610 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
|
||||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||||
],
|
||||
}));
|
||||
const INSPECT_CARD_COUNT = 60;
|
||||
const INSPECT_CARD_W = 158;
|
||||
const INSPECT_CARD_H = 214;
|
||||
// 카드 단위 엔티티는 v2 네임스페이스(ins2/all2/rwd2/shp2) — 자식 구성이 바뀌면 id를 통째로 새로 발급해야 함.
|
||||
// 구 id를 다른 path에 재사용하면 메이커 refresh의 id 기준 in-place 병합이 꼬여 자식이 소실됨 (P13 실측).
|
||||
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
|
||||
const insBase = 6 + (i - 1) * 7;
|
||||
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
|
||||
const card = entity({
|
||||
id: guid('ins2', insBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
|
||||
],
|
||||
});
|
||||
card.jsonString.enable = false;
|
||||
inspect.push(card);
|
||||
const inspectLayout = cardFaceLayout(INSPECT_CARD_W);
|
||||
for (const [tIdx, [suffix, cfg]] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
inspect.push(entity({
|
||||
id: guid('ins2', insBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: dOrder,
|
||||
components: [
|
||||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_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, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
inspect.push(entity({
|
||||
id: guid('ins2', insBase + 6),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: inspectLayout.art.size, pos: inspectLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
return inspect;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user