Compare commits
118 Commits
feature/ge
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5297922f99 | |||
| e3a75c33a3 | |||
| 0a040837d9 | |||
| da0d74f841 | |||
| 7f30803862 | |||
| 2fdd535939 | |||
| 1100cbeb08 | |||
| e14f19e4ed | |||
| 1a10444136 | |||
| 4d8fa0f40f | |||
| 9fd4b2d2e3 | |||
| 0def604f62 | |||
| a2e4f16402 | |||
| e8ea5e249d | |||
| 66985c2af6 | |||
| 1ecccb4ae7 | |||
| 985225dbd2 | |||
| f0b7704fc1 | |||
| 8628727bcc | |||
| 7db67e3ccd | |||
| 1847e2d9b2 | |||
| 5e2fd5db22 | |||
| 17200d47ec | |||
| 95d6155086 | |||
| de917f812d | |||
| 8a43ca91da | |||
| fc03d58ee7 | |||
| ead73b427e | |||
| d78049182b | |||
| 5f615e30e2 | |||
| 222ed92807 | |||
| 72750f3647 | |||
| 1291c52346 | |||
| 926733dbef | |||
| d7813f9912 | |||
| e6f351420b | |||
| b4a4560678 | |||
| 1e0b91294a | |||
| 8f8f17bd8f | |||
| 478fd1e5f0 | |||
| 0c1dfd3162 | |||
| 8d2e320d60 | |||
| 7b5e79bcf2 | |||
| 39356e5038 | |||
| 4878e5d8cc | |||
| 5f047ae41b | |||
| d83a377865 | |||
| 8292e26726 | |||
| 07ae56909a | |||
| f33018194f | |||
| a682baa5dc | |||
| 6d0ebde863 | |||
| 4ce87bec5d | |||
| 0cf714dca6 | |||
| fd00ed12d9 | |||
| 74a2106021 | |||
| a2044e20af | |||
| a3d5174b34 | |||
| 4f9be00ff2 | |||
| 24a79a309f | |||
| ba450f16b0 | |||
| 278007f908 | |||
| 16ebf304a5 | |||
| 5b7f7bb69f | |||
| 34531b184f | |||
| f6650a6c70 | |||
| acf295d56c | |||
| 9278c47901 | |||
| b2bf1bf4dd | |||
| 5da6e8f3aa | |||
| 71435a2c91 | |||
| f64e35668d | |||
| ba1651e52c | |||
| f8414a9c33 | |||
| 6344685052 | |||
| b0f1a0840c | |||
| c703bd9b4d | |||
| 96102dc41f | |||
| 8702d5209e | |||
| 74d4824a1c | |||
| bdea6a8c28 | |||
| 4fa0bc85c0 | |||
| db76870d4e | |||
| 491833b025 | |||
| 83de73c2c1 | |||
| b06ad8e8ee | |||
| 578f4416b2 | |||
| bc80b96875 | |||
| f2828deb19 | |||
| b549abc3b3 | |||
| 5b21e7f436 | |||
| b42d5fcf51 | |||
| 43530bee60 | |||
| 9b8884da81 | |||
| 2b3d77c588 | |||
| 3efb6993c7 | |||
| 5f8475d018 | |||
| afac34d7b5 | |||
| a917a6d82b | |||
| 9fba9b5aaa | |||
| 54d9632534 | |||
| c274322887 | |||
| 5900af087e | |||
| b0d3da2f39 | |||
| 6ef0ba48f6 | |||
| 1b1fce3d6e | |||
| c34a1126fb | |||
| 964cf7cc3d | |||
| 098571e9aa | |||
| ea832ad846 | |||
| 4288c4101b | |||
| 2bb7360a47 | |||
| a5388da2cc | |||
| 8ca48eca60 | |||
| eeca77df35 | |||
| 40e351333e | |||
| 0a83dea2d8 | |||
| 2f9c325c96 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,6 +7,8 @@
|
|||||||
# Claude Code 로컬 설정 — 단, 팀 공유 하네스 설정(settings.json)은 커밋 (RULES.md 참조)
|
# Claude Code 로컬 설정 — 단, 팀 공유 하네스 설정(settings.json)은 커밋 (RULES.md 참조)
|
||||||
.claude/*
|
.claude/*
|
||||||
!.claude/settings.json
|
!.claude/settings.json
|
||||||
|
# 개인 스킬(superpowers) 브레인스토밍/계획 산출물 — 로컬 전용, 협업 공유 X (프로젝트 설계 문서 docs/*.md 는 추적 유지)
|
||||||
|
docs/superpowers/
|
||||||
|
|
||||||
# === OS / 에디터 잡파일 ===
|
# === OS / 에디터 잡파일 ===
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
@@ -23,3 +25,5 @@ AGENTS.md
|
|||||||
Environment/
|
Environment/
|
||||||
McpScreenshots/
|
McpScreenshots/
|
||||||
*.log
|
*.log
|
||||||
|
# 메이커가 재편(reorg) 중 부모를 잃은 엔티티를 모아두는 임시 폴더 (잡파일)
|
||||||
|
Mislocated/
|
||||||
|
|||||||
142
Global/UIButton.model
Normal file
142
Global/UIButton.model
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "model://uibutton",
|
||||||
|
"ContentType": "x-mod/model",
|
||||||
|
"Content": "",
|
||||||
|
"Usage": 0,
|
||||||
|
"UsePublish": 1,
|
||||||
|
"UseService": 0,
|
||||||
|
"CoreVersion": "26.5.0.0",
|
||||||
|
"StudioVersion": "0.1.0.0",
|
||||||
|
"DynamicLoading": 0,
|
||||||
|
"ContentProto": {
|
||||||
|
"Use": "Json",
|
||||||
|
"Json": {
|
||||||
|
"Version": 1,
|
||||||
|
"Name": "UIButton",
|
||||||
|
"BaseModelId": null,
|
||||||
|
"Id": "uibutton",
|
||||||
|
"Components": [
|
||||||
|
"MOD.Core.UITransformComponent",
|
||||||
|
"MOD.Core.SpriteGUIRendererComponent"
|
||||||
|
],
|
||||||
|
"Properties": [
|
||||||
|
{
|
||||||
|
"Type": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.MODVector2, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Name": "RectSize",
|
||||||
|
"DisplayName": "RectSize",
|
||||||
|
"ShowInInspector": true,
|
||||||
|
"Link": {
|
||||||
|
"Target": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.UITransformComponent, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Property": "RectSize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.MODDataRef, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Name": "ImageRUID",
|
||||||
|
"DisplayName": "ImageRUID",
|
||||||
|
"ShowInInspector": true,
|
||||||
|
"Link": {
|
||||||
|
"Target": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.SpriteGUIRendererComponent, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Property": "ImageRUID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.MODColor, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Name": "Color",
|
||||||
|
"DisplayName": "Color",
|
||||||
|
"ShowInInspector": true,
|
||||||
|
"Link": {
|
||||||
|
"Target": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.SpriteGUIRendererComponent, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Property": "Color"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Values": [
|
||||||
|
{
|
||||||
|
"TargetType": "MOD.Core.UITransformComponent",
|
||||||
|
"Name": "anchoredPosition",
|
||||||
|
"ValueType": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.MODVector2, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TargetType": "MOD.Core.UITransformComponent",
|
||||||
|
"Name": "RectSize",
|
||||||
|
"ValueType": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.MODVector2, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"$type": "MOD.Core.MODVector2, MOD.Core",
|
||||||
|
"x": 200.0,
|
||||||
|
"y": 75.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TargetType": "MOD.Core.UITransformComponent",
|
||||||
|
"Name": "AlignmentOption",
|
||||||
|
"ValueType": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.AlignmentType, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TargetType": "MOD.Core.SpriteGUIRendererComponent",
|
||||||
|
"Name": "ImageRUID",
|
||||||
|
"ValueType": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.MODDataRef, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"$type": "MOD.Core.MODDataRef, MOD.Core",
|
||||||
|
"DataId": "cc3457b8e97b3e14f9d5c39ccdd640bf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TargetType": "MOD.Core.SpriteGUIRendererComponent",
|
||||||
|
"Name": "Color",
|
||||||
|
"ValueType": {
|
||||||
|
"$type": "MODNativeType",
|
||||||
|
"type": "MOD.Core.MODColor, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"$type": "MOD.Core.MODColor, MOD.Core",
|
||||||
|
"r": 1.0,
|
||||||
|
"g": 1.0,
|
||||||
|
"b": 1.0,
|
||||||
|
"a": 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"EventLinks": [],
|
||||||
|
"Children": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
{
|
{
|
||||||
"@type": "script.SlayDeckController",
|
"@type": "script.SlayDeckController",
|
||||||
"Enable": true,
|
"Enable": true,
|
||||||
"Energy": 0.0,
|
"Energy": 0,
|
||||||
"MaxEnergy": 3.0,
|
"MaxEnergy": 3,
|
||||||
"Turn": 0.0,
|
"Turn": 0,
|
||||||
"TweenEventId": 0.0
|
"TweenEventId": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@version": 1
|
"@version": 1
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -44,11 +44,13 @@ git pull
|
|||||||
```
|
```
|
||||||
slaymaple/
|
slaymaple/
|
||||||
├── data/ # 게임 데이터 단일 소스 (생성기가 읽어 주입). 맵은 정적 데이터 없음(절차 생성)
|
├── data/ # 게임 데이터 단일 소스 (생성기가 읽어 주입). 맵은 정적 데이터 없음(절차 생성)
|
||||||
│ ├── cards.json # 카드 122장(클래스·2차전직별 + 저주) + 클래스별 시작 덱
|
│ ├── cards.json # 카드 166장(1~3차 전직 계열별 + 저주) + 클래스별 시작 덱
|
||||||
│ ├── enemies.json # 적 12종(일반/정예/보스, 디버프 인텐트 포함)
|
│ ├── enemies.json # 적 18종(일반/정예/보스, 디버프 인텐트 포함)
|
||||||
│ ├── potions.json # 물약 6종 + 드랍률·슬롯·상점가
|
│ ├── potions.json # 물약 6종 + 드랍률·슬롯·상점가
|
||||||
│ ├── relics.json # 유물 19종(StS 효과 × 메이플 장비) + 시작 유물 + 풀
|
│ ├── relics.json # 유물 19종(StS 효과 × 메이플 장비) + 시작 유물 + 풀
|
||||||
│ ├── cardframes.json # 커스텀 카드 프레임 3종(전사/마법사/도적 × normal/unique/legend) + 보상 등급 가중치
|
│ ├── cardframes.json # 커스텀 카드 프레임 3종(전사/마법사/도적 × normal/unique/legend) + 보상 등급 가중치
|
||||||
|
│ ├── characters.json # 클래스별 초상화 RUID
|
||||||
|
│ ├── cards.xlsx # cards.json 왕복 편집용 엑셀(excel_to_cards.bat / cards_to_excel.bat)
|
||||||
│ └── camera.json # 맵별 카메라 설정값(줌·오프셋·고정 영역)
|
│ └── camera.json # 맵별 카메라 설정값(줌·오프셋·고정 영역)
|
||||||
├── Global/ # 월드 전역 설정 · 공용 모델 · 게임로직
|
├── Global/ # 월드 전역 설정 · 공용 모델 · 게임로직
|
||||||
│ ├── common.gamelogic # SlayDeckController 부착 지점 (산출물)
|
│ ├── common.gamelogic # SlayDeckController 부착 지점 (산출물)
|
||||||
@@ -71,15 +73,15 @@ slaymaple/
|
|||||||
│ ├── lobby.map # 로비 허브 맵 (마을 배경, NPC 4종, 전투 없음)
|
│ ├── lobby.map # 로비 허브 맵 (마을 배경, NPC 4종, 전투 없음)
|
||||||
│ └── map01.map ~ map05.map # 5막 전투/맵 노드 (공식 배경 + STS풍 우측 배치)
|
│ └── map01.map ~ map05.map # 5막 전투/맵 노드 (공식 배경 + STS풍 우측 배치)
|
||||||
├── tools/ # 결정적 생성기·도구 (주체별 폴더, 단일 소스)
|
├── tools/ # 결정적 생성기·도구 (주체별 폴더, 단일 소스)
|
||||||
│ ├── deck/ # gen-slaydeck.mjs(★게임 전체 생성: 카드/덱·전투·맵노드·상점·유물·로비·메뉴 UI + SlayDeckController + common) · gen-cardhand.mjs
|
│ ├── deck/ # gen-slaydeck.mjs(★컨트롤러+common 생성 오케스트레이터) · cb/(codeblock Lua 메서드 20모듈: boot·screens·combat·hand·npc·navigation·layout·shop·reward·soul 등) · lib/(공유 상수·데이터·헬퍼) · legacy/(옛 UI emit 휴면)
|
||||||
│ ├── map/ # gen-maps.mjs(맵 배경/타일) · gen-lobby-map.mjs(로비 맵+NPC) · gen-map-encounters.mjs(노드별 몬스터 그룹) · rogue-map.mjs(절차 생성 JS 미러)+test
|
│ ├── map/ # gen-maps.mjs(맵 배경/타일) · gen-lobby-map.mjs(로비 맵+NPC) · gen-map-encounters.mjs(노드별 몬스터 그룹) · rogue-map.mjs(절차 생성 JS 미러)+test
|
||||||
│ ├── camera/ # gen-camera.mjs(맵별 고정 카메라 codeblock)
|
│ ├── camera/ # gen-camera.mjs(맵별 고정 카메라 codeblock)
|
||||||
│ ├── player/ # gen-player-lock.mjs(전투맵 입력 잠금) · freeze-turn-player.mjs(모델 이동 정지) · gen-lobby-npc.mjs(LobbyNpc·LobbyMobility 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 정지)
|
│ ├── monster/ # gen-combat-monster.mjs(EnemyId 마커) · freeze-turn-monsters.mjs(필드 AI 정지)
|
||||||
│ ├── balance/ # sim-balance.mjs(전투 밸런스 몬테카를로 시뮬) · sim-balance.test.mjs
|
│ ├── balance/ # sim-balance.mjs(전투 밸런스 몬테카를로 시뮬) · sim-balance.test.mjs
|
||||||
│ ├── verify/ # count.mjs(산출물 카운트 검증 헬퍼 — 경로 내장)
|
│ ├── verify/ # count·uimap·cbgap(카운트/UIGroup 매핑/재연결 GAP) · cardkinds(카드 kind↔효과) · cbprops(미선언 self 대입) · cbset(메서드 집합 무손실) · diffcheck(바이트동일)
|
||||||
│ └── git/ # gitea-pr.mjs(UTF-8 안전 PR 생성/수정/머지 — RULES.md 참조)
|
│ └── git/ # gitea-pr.mjs(UTF-8 안전 PR 생성/수정/머지 — RULES.md 참조)
|
||||||
├── ui/ # UI 그룹 (DefaultGroup 8.3MB 산출물 / PopupGroup / ToastGroup)
|
├── ui/ # UIGroup 7종 — 메이커 저작(Default/Select/Lobby/Run/Deck/Popup/Toast)
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서
|
│ ├── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서
|
||||||
│ ├── ui-generation-structure.md # UI 생성 구조 문서
|
│ ├── ui-generation-structure.md # UI 생성 구조 문서
|
||||||
@@ -89,14 +91,22 @@ slaymaple/
|
|||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ **`map/*.map` · `ui/DefaultGroup.ui` · `*.codeblock` · `Global/*.gamelogic`는 생성 산출물**입니다 — 직접 편집하면 다음 재생성 때 사라집니다. 게임 변경은 `data/*.json` 또는 `tools/`의 생성기를 고친 뒤 재생성하세요(자세한 규칙은 [`RULES.md`](RULES.md)).
|
> ⚠️ **`map/*.map` · `SlayDeckController.codeblock` · `Global/common.gamelogic`는 생성 산출물**입니다 — 직접 편집하면 재생성 때 사라집니다. 게임 로직 변경은 `data/*.json`·`tools/`의 생성기를 고쳐 재생성하세요. **`ui/*.ui`는 메이커 저작**(생성기 미생성)이라 메이커에서만 편집합니다(자세한 규칙은 [`RULES.md`](RULES.md)).
|
||||||
> `.mcp.json`, `.codex/` 는 **Authorization 토큰이 포함**되어 있어 git에서 제외됩니다(`.gitignore`). 각자 로컬에서 직접 구성하세요.
|
> `.mcp.json`, `.codex/` 는 **Authorization 토큰이 포함**되어 있어 git에서 제외됩니다(`.gitignore`). 각자 로컬에서 직접 구성하세요.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 직업 컨셉
|
||||||
|
|
||||||
|
3직업 모두 Slay the Spire 2 차용 + 메이플 IP 재해석. 카드 덱 상세 설계는 [`docs/deck-concept.md`](docs/deck-concept.md) 참조.
|
||||||
|
|
||||||
|
- **⚔️ 전사 (탱커, Ironclad 차용, HP80)** — 2차 3종. **파이터**: 공격을 *연속*으로 내면 콤보가 쌓이고(비공격 카드 시 리셋) 콤보로 데미지 증가 = 브루저(콤보 어택·버서크·라이징 어택). **페이지**: 썬더/블리자드 **속성 차지** + 파워 가드. **스피어맨**: 피어스·아이언 월·하이퍼 바디 유지/관통형.
|
||||||
|
- **🗡️ 도적 (단검·독, Silent 차용, HP70)** — 표창 난사 / 독 / 교활·버림. **2차 어쌔신**(표창·독 압박·빠른 마무리)·**시프**(단검·드로우·연계) → **3차 헤르밋**(어쌔신 심화)·**시프 마스터**(시프 심화). 도적 계열만 132장(Silent 완역 포트 + 공식 스킬 아이콘).
|
||||||
|
- **🔮 법사 (약체·게이지, Defect 차용, HP70)** — 2차 3종. **위자드(불·독)**: 독을 묻히고 *독 걸린 적에 불 카드 → 추가 데미지*(독뎀 시너지). **위자드(썬·콜)**: 오브로 썬더(다중 공격)·콜드(빙결=취약+피해), 오브 획득·다중 소모 운용. **클레릭**: 오브 없이 회복·버프 + 언데드엔 힐로 공격하는 보조 힐러.
|
||||||
|
|
||||||
## 게임 프레임워크 현황
|
## 게임 프레임워크 현황
|
||||||
|
|
||||||
**StS2풍 덱빌더 로그라이크가 end-to-end로 완성**됐고, 이제 **로비 마을을 기점으로 반복 런**이 돕니다:
|
**StS2풍 덱빌더 로그라이크가 end-to-end로 완성**됐고, 이제 **로비 마을을 기점으로 반복 런**이 돕니다 (게임 시작 시 MainMenu 없이 바로 로비로 진입):
|
||||||
|
|
||||||
```
|
```
|
||||||
로비 맵(NPC 4종) → 모험가 NPC → 캐릭터 선택(전사/도적/마법사) → 절차 생성 맵(5막)
|
로비 맵(NPC 4종) → 모험가 NPC → 캐릭터 선택(전사/도적/마법사) → 절차 생성 맵(5막)
|
||||||
@@ -104,15 +114,16 @@ slaymaple/
|
|||||||
→ 런 클리어(승천 해금) → 로비 복귀(영혼 정산) → 다음 런 …
|
→ 런 클리어(승천 해금) → 로비 복귀(영혼 정산) → 다음 런 …
|
||||||
```
|
```
|
||||||
|
|
||||||
게임 전체는 `/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 미러).
|
게임 전체는 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작합니다. **UI는 메이커 저작**(7개 UIGroup: Default/Select/Lobby/Run/Deck/Popup/Toast)이고, 컨트롤러가 엔티티 경로(`/ui/<UIGroup>/<Hud>/...`)로 내용을 런타임 주입합니다. 생성기 `tools/deck/gen-slaydeck.mjs`는 **`SlayDeckController.codeblock` + `common.gamelogic`만 생성**(`.ui` 미접근, 결정적 출력 — `RULES.md` 참조). 게임 데이터는 **`data/*.json`**, 맵 구조는 **런타임 절차 생성**(`GenerateMap` Lua ↔ `tools/map/rogue-map.mjs` JS 미러).
|
||||||
|
|
||||||
### 구현된 기능 (배포 퀄리티 P1~P15, PR #34~#57)
|
### 구현된 기능 (배포 퀄리티 P1~P15+, PR #34~#104)
|
||||||
|
|
||||||
| 영역 | 내용 |
|
| 영역 | 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **로비 마을** | 전용 물리 맵 `lobby.map`(마을 배경). **NPC 4종 월드 엔티티** — 모험가(런 시작)·사서(카드 도감)·상인(영혼 상점)·안내원(게시판). 근접 시 머리 위 마크 + `↑`키 **또는 직접 클릭**으로 상호작용. **이동·공격 모션은 로비 맵에서만** 풀림(전투맵은 잠금), 카메라는 로비에서 **플레이어 추종**(전투맵은 고정) |
|
| **로비 마을** | 전용 물리 맵 `lobby.map`(마을 배경). **NPC 4종 월드 엔티티** — 모험가(런 시작)·사서(카드 도감)·상인(영혼 상점)·안내원(게시판). 근접 시 머리 위 마크 + `↑`키 **또는 직접 클릭**으로 상호작용. **이동·공격 모션은 로비 맵에서만** 풀림(전투맵은 잠금), 카메라는 로비에서 **플레이어 추종**(전투맵은 고정) |
|
||||||
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택, 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**2차 전직**] — 각 클래스 3종(전사→파이터/페이지/스피어맨, 법사→위자드불독/위자드썬콜/클레릭, 도적→Shiv/Poison/Trickster). 전용 카드는 해당 클래스 풀만 획득 |
|
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택(**초상화·직업 설명·선택 테두리 강조** 캐릭터 선택 UI), 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**전직**] — 전사→파이터/페이지/스피어맨, 법사→위자드(불·독)/위자드(썬·콜)/클레릭 (2차 3종씩), **도적→어쌔신·시프(2차) → 헤르밋·시프 마스터(3차)**. 전직 시 대표 카드 지급, 전용 카드는 해당 계열 풀만 획득 |
|
||||||
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬=위로 스윕). 카드 **122장** — kind **Attack/Skill/Power/Status**. 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
|
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬/파워=위로 스윕). 카드 **166장** — kind **Attack(59)/Skill(74)/Power(31)/Status(2)**. kind↔효과 정합성 정적 검증(`cardkinds.mjs`). 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
|
||||||
|
| **도적 카드 공용 효과** | 카드 효과를 **카드명 하드코딩 대신 `data/cards.json` 공용 필드**로 표현(재사용). **불가침**·**x-cost**(에너지 비례 피해/약화)·드로우 수 비례 데미지·**다음 스킬 반복**·**처치 보상/반복**·카드 설명 **키워드 하이라이트**·드로우 연동(`drawSkillBlock`·`drawPoison`)·독 버스트·랜덤 타깃 등. **Lua + JS 미러 양쪽 구현**. 필드 사전 [`docs/card-effect-fields.md`](docs/card-effect-fields.md) |
|
||||||
| **버프/디버프** | StS 표준 — **힘**(+N 영구)·**약화**(주는 피해 −25%)·**취약**(받는 피해 +50%)·**독**(매 행동 틱). 양방향(적 디버프 인텐트 포함), 인텐트는 최종 예상치 표시 |
|
| **버프/디버프** | StS 표준 — **힘**(+N 영구)·**약화**(주는 피해 −25%)·**취약**(받는 피해 +50%)·**독**(매 행동 틱). 양방향(적 디버프 인텐트 포함), 인텐트는 최종 예상치 표시 |
|
||||||
| **전투 연출** | 공격 이펙트·**몬스터 데미지 팝업(자릿수 스킨)**·드래그 타깃 마커·적 개별 차례·**공격/피격/독뎀 모션**(아바타 상태 전이·몬스터 hit 클립·런지/넉백) |
|
| **전투 연출** | 공격 이펙트·**몬스터 데미지 팝업(자릿수 스킨)**·드래그 타깃 마커·적 개별 차례·**공격/피격/독뎀 모션**(아바타 상태 전이·몬스터 hit 클립·런지/넉백) |
|
||||||
| **절차 생성 맵** | 막 시작마다 **경로 생성**(런마다 다름, **가로 진행**). 층 규칙: 1~2층 전투만 → 3층~ 상점/휴식 → 4층~ 엘리트/**유물 방** → 보스 수렴. 점선 경로·상태 4단·층 카운터. 노드 타입별 **몬스터 랜덤 구성**(일반 1~3 / 엘리트 / 보스) + intent 랜덤 행동 |
|
| **절차 생성 맵** | 막 시작마다 **경로 생성**(런마다 다름, **가로 진행**). 층 규칙: 1~2층 전투만 → 3층~ 상점/휴식 → 4층~ 엘리트/**유물 방** → 보스 수렴. 점선 경로·상태 4단·층 카운터. 노드 타입별 **몬스터 랜덤 구성**(일반 1~3 / 엘리트 / 보스) + intent 랜덤 행동 |
|
||||||
@@ -122,10 +133,10 @@ slaymaple/
|
|||||||
| **승천(Ascension)** | A1~A10 누적 모디파이어(적 강화·시작 HP 감소·보상 감소). UserDataStorage 유저별 영구 저장, 런 클리어 시 다음 단계 해금 |
|
| **승천(Ascension)** | A1~A10 누적 모디파이어(적 강화·시작 HP 감소·보상 감소). UserDataStorage 유저별 영구 저장, 런 클리어 시 다음 단계 해금 |
|
||||||
| **멀티 act** | **5막** 진행(보스 클리어→다음 막 텔레포트, 맵·인카운터 변경, 적 스케일 `1+(막-1)*0.45`), 5막 클리어 시 런 종료 |
|
| **멀티 act** | **5막** 진행(보스 클리어→다음 막 텔레포트, 맵·인카운터 변경, 적 스케일 `1+(막-1)*0.45`), 5막 클리어 시 런 종료 |
|
||||||
| **경제** | 화폐 표기 **메소**(코인 아이콘), 카드/유물/물약 메소 가격. 내부 식별자는 Gold 유지 |
|
| **경제** | 화폐 표기 **메소**(코인 아이콘), 카드/유물/물약 메소 가격. 내부 식별자는 Gold 유지 |
|
||||||
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트 |
|
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트(현 97종) |
|
||||||
|
|
||||||
> ⚠️ 수치(적 스탯·경제·승천 배율)는 1차 조정 상태입니다. 정밀 밸런싱은 `sim-balance.mjs`로 검증하며 진행합니다.
|
> ⚠️ 수치(적 스탯·경제·승천 배율)는 1차 조정 상태입니다. 정밀 밸런싱은 `sim-balance.mjs`로 검증하며 진행합니다.
|
||||||
> ℹ️ 도적(Silent) 카드 88장은 효과·프레임은 적용됐으나 **카드 아이콘(image/fx) 미할당** 상태입니다(전사·마법사 카드는 실 스킬 아이콘 적용 완료).
|
> ℹ️ 도적 계열 카드 132장은 STS Silent 완역 포트 + **공식 스킬 아이콘 적용 완료**, rogue 1차 + 어쌔신/시프(2차) + 헤르밋/시프 마스터(3차)로 재편. 남은 작업은 카드명 메이플 재서사·멀티플레이어 전제 카드 싱글 정리 — [`docs/deck-concept.md`](docs/deck-concept.md)·[`docs/bandit-card-audit.md`](docs/bandit-card-audit.md) 참조.
|
||||||
|
|
||||||
### 유용한 스크립트 호출
|
### 유용한 스크립트 호출
|
||||||
`/common` 엔티티(또는 Play Test 컨텍스트)에서:
|
`/common` 엔티티(또는 Play Test 컨텍스트)에서:
|
||||||
@@ -135,23 +146,34 @@ local c = _EntityService:GetEntityByPath("/common").SlayDeckController
|
|||||||
c:OnLobbyNpcInteract("run") -- 모험가(런 시작) / "codex"(도감) / "shop"(영혼상점) / "board"(게시판)
|
c:OnLobbyNpcInteract("run") -- 모험가(런 시작) / "codex"(도감) / "shop"(영혼상점) / "board"(게시판)
|
||||||
c:ShowLobby() -- 로비 맵 복귀 + 상태 초기화
|
c:ShowLobby() -- 로비 맵 복귀 + 상태 초기화
|
||||||
-- 런
|
-- 런
|
||||||
c:SelectClass("warrior") -- "warrior" / "bandit" / "magician"
|
c:SelectClass("warrior") -- "warrior" / "rogue" / "magician"
|
||||||
c:StartNewGame() -- 캐릭터 선택 → 런 시작(map01 텔레포트)
|
c:StartNewGame() -- 캐릭터 선택 → 런 시작(map01 텔레포트)
|
||||||
c:PickNode("r1c2") -- 맵 노드 선택(절차 생성 그리드 id) / "boss"
|
c:PickNode("r1c2") -- 맵 노드 선택(절차 생성 그리드 id) / "boss"
|
||||||
c:PlayCard(1) -- 손패 slot 카드 사용
|
c:PlayCard(1) -- 손패 slot 카드 사용
|
||||||
c:EndPlayerTurn() -- 턴 종료 → 적 턴 → 다음 턴
|
c:EndPlayerTurn() -- 턴 종료 → 적 턴 → 다음 턴
|
||||||
c:PickReward(1) -- 보상 카드 1택(0=건너뛰기)
|
c:PickReward(1) -- 보상 카드 1택(0=건너뛰기)
|
||||||
c:BuyCard(1) / c:BuyRelic() / c:BuyPotion() -- 상점 구매(메소)
|
c:BuyCard(1) / c:BuyRelic() / c:BuyPotion() -- 상점 구매(메소)
|
||||||
c:SetJob("fighter") -- 전직 (보스 보상 선택 화면)
|
c:SetJob("fighter") -- 전직 (보스 보상 화면) — 2차: fighter/page/spearman·firepoison/icelightning/cleric·assassin/thief, 3차: hermit/thiefmaster
|
||||||
c:AdjustAscension(1) -- 메뉴에서 승천 단계 +1
|
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`.
|
밸런스 검증: `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) 및 `docs/superpowers/specs/` 참조.
|
상세 설계는 [`docs/slaymaple_basic_framework.md`](docs/slaymaple_basic_framework.md) 및 `docs/superpowers/specs/` 참조.
|
||||||
|
|
||||||
|
### 디버그 단축키
|
||||||
|
|
||||||
|
개발·QA용 키보드 단축키. **전투 중**(런 활성 + 전투 진행 중)에만 동작합니다.
|
||||||
|
|
||||||
|
| 단축키 | 기능 |
|
||||||
|
|---|---|
|
||||||
|
| **Ctrl + Shift + C** | **카드 picker** — 직업 전체 카드 패널을 띄우고, 카드를 클릭하면 **즉시 손패에 추가**. 상단 탭(전사/도적/마법사)으로 직업별 카드 풀 전환. 카드 효과·메커니즘 즉석 테스트용 |
|
||||||
|
| **Ctrl + Shift + E** | **전체 회복 치트** — 체력·에너지를 최대치로 회복 |
|
||||||
|
|
||||||
|
> 카드 picker는 메이커 저작 UI `DeckUIGroup/DeckAllHud`(120 슬롯 그리드 + 직업 탭 3종)를 사용하고, 컨트롤러가 런타임에 카드 비주얼·버튼을 바인딩합니다. 구현: 키 바인딩 `tools/deck/cb/boot.mjs`, picker 로직 `tools/deck/cb/deckview.mjs`(`OpenDebugCardPicker`/`OnAllDeckCardButton`), 버튼 바인딩 `tools/deck/cb/deckturn.mjs`(`BindButtons`). 옛 picker UI 생성기 `tools/deck/legacy/hud/deckall.mjs`는 UI 메이커-저작 전환 후 **휴면**(Maker UI가 대체).
|
||||||
|
|
||||||
### 산출물 재생성
|
### 산출물 재생성
|
||||||
```bash
|
```bash
|
||||||
node tools/deck/gen-slaydeck.mjs # 게임 전체(UI·컨트롤러·common·맵 인카운터)
|
node tools/deck/gen-slaydeck.mjs # 컨트롤러+common (UI는 메이커 저작 — 미생성)
|
||||||
node tools/map/gen-maps.mjs # map01~05 배경/타일
|
node tools/map/gen-maps.mjs # map01~05 배경/타일
|
||||||
node tools/map/gen-lobby-map.mjs # 로비 맵 + NPC 배치
|
node tools/map/gen-lobby-map.mjs # 로비 맵 + NPC 배치
|
||||||
node tools/player/gen-lobby-npc.mjs # 로비 codeblock(LobbyNpc·LobbyMobility)
|
node tools/player/gen-lobby-npc.mjs # 로비 codeblock(LobbyNpc·LobbyMobility)
|
||||||
@@ -159,25 +181,31 @@ node tools/camera/gen-camera.mjs # 맵별 카메라
|
|||||||
node tools/player/gen-player-lock.mjs # 전투맵 입력 잠금
|
node tools/player/gen-player-lock.mjs # 전투맵 입력 잠금
|
||||||
node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
|
node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
|
||||||
```
|
```
|
||||||
> 산출물 검증은 내용 출력 없이 카운트만: `node tools/verify/count.mjs <ui|cb|common> <regex>...` (자세한 가드는 [`RULES.md`](RULES.md)).
|
> 산출물 검증은 내용 출력 없이 카운트만: `node tools/verify/count.mjs <ui|cb|common> <regex>...`. 정적 가드 — 카드 kind↔효과 `cardkinds.mjs` · 미선언 self 대입 `cbprops.mjs` · UI 경로 재연결 GAP `cbgap.mjs` · 리팩터 바이트동일 `diffcheck.mjs` (자세한 가드는 [`RULES.md`](RULES.md)).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 아키텍처 메모
|
## 아키텍처 메모
|
||||||
|
|
||||||
현재 게임 전체 로직이 `SlayDeckController` 단일 codeblock에 모여 있습니다. 초기 설계의 3분할(`SlayCardCatalog`/`SlayRunState`/`SlayCombatManager`)은 **기능적으로 모두 구현**됐으나 아직 한 컴포넌트 안에 있습니다. 맵 NPC·카메라·입력 잠금 등 **맵 단위 동작은 별도 codeblock**(LobbyNpc/LobbyMobility/MapCamera/PlayerLock/CombatMonster)으로 분리해 각 맵 루트/엔티티에 부착합니다. 카드/적/맵/유물/프레임/카메라 데이터는 `data/*.json`로 외부화돼 있습니다.
|
현재 게임 전체 로직이 `SlayDeckController` 단일 codeblock에 모여 있습니다. 초기 설계의 3분할(`SlayCardCatalog`/`SlayRunState`/`SlayCombatManager`)은 **기능적으로 모두 구현**됐으나 아직 한 컴포넌트 안에 있습니다. 맵 NPC·카메라·입력 잠금 등 **맵 단위 동작은 별도 codeblock**(LobbyNpc/LobbyMobility/MapCamera/PlayerLock/CombatMonster)으로 분리해 각 맵 루트/엔티티에 부착합니다. 카드/적/맵/유물/프레임/카메라 데이터는 `data/*.json`로 외부화돼 있습니다. **2026-06-17**: UI를 단일 `DefaultGroup`에서 7개 UIGroup(Select/Lobby/Run/Deck 등)으로 분리해 **메이커 저작으로 전환** — 생성기는 더 이상 `.ui`를 만들지 않고, 컨트롤러가 새 UIGroup 경로로 재연결됨(옛 UI emit `hud/*`·`gen-cardhand`는 `tools/deck/legacy/` 휴면). 재연결 무결성은 `tools/verify/cbgap.mjs`(GAP 0)로 검증.
|
||||||
|
|
||||||
> ⚠️ **전투 규칙과 맵 생성은 Lua(gen-slaydeck 내장)와 JS 미러(sim-balance/rogue-map)로 이중 구현**입니다. 한쪽을 고치면 반드시 다른 쪽도 동기화하고 테스트하세요(`RULES.md` §6).
|
> ⚠️ **전투 규칙과 맵 생성은 Lua(gen-slaydeck 내장)와 JS 미러(sim-balance/rogue-map)로 이중 구현**입니다. 한쪽을 고치면 반드시 다른 쪽도 동기화하고 테스트하세요(`RULES.md` §6).
|
||||||
|
> ⚠️ **카드 `kind`는 효과와 반드시 일치**해야 합니다 — 데미지=`Attack`, 방어/유틸=`Skill`, 지속효과=`Power`. 안 맞으면 런타임 에러 없이 *사용 불가/무효과 死카드*가 됩니다(2026-06-30 Defend·Rage 사고). 새 효과 필드는 `docs/card-effect-fields.md` 등록 + Lua/JS 양쪽 핸들러 구현. 정적 검증 `node tools/verify/cardkinds.mjs`(`RULES.md` §9). cb Lua 지역변수는 의미명 사용(`RULES.md` §8).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 향후 개선 계획 (후속 후보)
|
## 향후 개선 계획 (후속 후보)
|
||||||
- [x] 전투 루프 · 런 루프 · 절차 생성 맵 · 상점/휴식/유물 방 · 유물 19종 · 물약 · 버프/디버프 · Power · 전직(전사/법사/도적 2차) · 승천+개인 저장 · 전투 모션 · 커스텀 프레임 · **반복 런·로비 맵·NPC·영혼·메소·카메라 추종 (P1~P15 완료)**
|
- [x] 전투 루프 · 런 루프 · 절차 생성 맵 · 상점/휴식/유물 방 · 유물 19종 · 물약 · 버프/디버프 · Power · 전직(전사/법사/도적 2차) · 승천+개인 저장 · 전투 모션 · 커스텀 프레임 · **반복 런·로비 맵·NPC·영혼·메소·카메라 추종 (P1~P15 완료)**
|
||||||
- [ ] **도적 카드 아이콘** — Silent 88장에 실 스킬 아이콘(image/fx) 할당, 2차 전직 설명 한글화
|
- [x] **UI 메이커-저작 전환** — 단일 DefaultGroup → 7개 UIGroup 분리, 생성기 UI 저작 폐기(`tools/deck/legacy/`), 컨트롤러 경로 재연결(cbgap GAP 0) (2026-06-17)
|
||||||
|
- [x] **시작 로비 직행 · 캐릭터 선택 UI · 디버그 치트 · map01 로스터 (2026-06-18)** — 게임 시작 시 MainMenu 없이 곧장 로비 진입(MainMenu는 추후 싱글/멀티/종료 메뉴로 재지정); 캐릭터 선택 화면 초상화·직업 설명·선택 테두리·Art 클리핑(MaskComponent) 배선; 디버그 단축키 Ctrl+Shift+C(카드 picker)·Ctrl+Shift+E(체력+에너지 전체 회복); map01 몬스터 18종 로스터(랜덤 행동)
|
||||||
|
- [x] **컨트롤러 관심사별 모듈 분리 · 코드 규칙 (2026-06-26, #94)** — SlayDeckController를 `cb/*.mjs` 20모듈로 분리(런타임은 단일 codeblock 유지), 변수명 의미화, 검증 `cbset.mjs`(집합 무손실)·`cbprops.mjs`(미선언 self)
|
||||||
|
- [x] **도적 계열 대개편 + 3차 전직 · 카드 공용 효과 (2026-06-23~30, #82~#99)** — Silent 포트를 rogue 1차 + 어쌔신/시프(2차) + 헤르밋/시프 마스터(3차)로 재편, 카드 효과를 카드명 하드코딩 대신 `cards.json` 공용 필드로(`docs/card-effect-fields.md`), 카드 **166장**
|
||||||
|
- [x] **코드리뷰 버그수정 + kind↔효과 규칙 (2026-06-29~30, #96·#102)** — 게임버그 6·시뮬 충실도 3·설명 2 수정(Defend kind Attack→Skill·Rage Power→Attack 포함), kind↔효과 정적 검증 `cardkinds.mjs`, 카드 왕복 편집 엑셀(#93)
|
||||||
|
- [ ] **도적 카드명 재서사·설명 한글화** — Silent 직역 카드명을 어쌔신/시프 메이플 스킬명으로 재서사(아이콘은 적용 완료), 2·3차 전직 설명 한글화
|
||||||
- [ ] **런 이어하기** — 진행 중 런 직렬화 저장(UserDataStorage 확장, 메뉴 "이어하기" 활성화)
|
- [ ] **런 이어하기** — 진행 중 런 직렬화 저장(UserDataStorage 확장, 메뉴 "이어하기" 활성화)
|
||||||
- [ ] **카드 제거/업그레이드** — 상점 카드 제거 슬롯, 휴식 노드에서 카드 강화
|
- [ ] **카드 제거/업그레이드** — 상점 카드 제거 슬롯, 휴식 노드에서 카드 강화
|
||||||
- [ ] **이벤트 노드(?)** — 랜덤 텍스트 이벤트(선택지·리스크/리워드)
|
- [ ] **이벤트 노드(?)** — 랜덤 텍스트 이벤트(선택지·리스크/리워드)
|
||||||
- [ ] **3차 전직** — 후반 막 보상으로 확장
|
- [ ] **3차 전직 — 전사·법사 확장** (도적은 완료: 헤르밋·시프 마스터), 후반 막 보상으로
|
||||||
- [ ] **궁수 등 추가 클래스** — 캐릭터 선택 슬롯 확장
|
- [ ] **궁수 등 추가 클래스** — 캐릭터 선택 슬롯 확장
|
||||||
- [ ] **정밀 밸런싱** — 첫 인카운터 승률 완화·직업별 카드 효율 튜닝(`sim-balance.mjs` 리포트 기반)
|
- [ ] **정밀 밸런싱** — 첫 인카운터 승률 완화·직업별 카드 효율 튜닝(`sim-balance.mjs` 리포트 기반)
|
||||||
- [ ] **상점 보장 규칙** — 막당 상점 최소 1회 등장
|
- [ ] **상점 보장 규칙** — 막당 상점 최소 1회 등장
|
||||||
|
|||||||
30
RULES.md
30
RULES.md
@@ -11,8 +11,8 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
|
|||||||
|
|
||||||
| 산출물 (절대 Read/Edit 금지) | 크기 | 단일 소스 (여기만 편집) | 재생성 명령 |
|
| 산출물 (절대 Read/Edit 금지) | 크기 | 단일 소스 (여기만 편집) | 재생성 명령 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `ui/DefaultGroup.ui` | **~7.1MB** | `data/*.json` + `tools/deck/`(`gen-slaydeck.mjs`+`lib/`+`hud/`) | `node tools/deck/gen-slaydeck.mjs` |
|
| `ui/*.ui` (Default·Select·Lobby·Run·Deck·Popup·Toast UIGroup 7종) | 9KB~4.5MB | **메이커 저작 (생성기 미생성, 2026-06-17~)** — 메이커에서 시각 편집 | (없음) |
|
||||||
| `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | 〃 | 〃 |
|
| `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | `data/*.json` + `tools/deck/`(`gen-slaydeck.mjs`+`lib/`+`cb/`) | `node tools/deck/gen-slaydeck.mjs` |
|
||||||
| `Global/common.gamelogic` | ~1KB | 〃 | 〃 |
|
| `Global/common.gamelogic` | ~1KB | 〃 | 〃 |
|
||||||
| `map/map01.map`~`map05.map`, `map/lobby.map` | 각 ~210KB | `tools/map/`·`tools/monster/`·`tools/camera/`·`tools/player/` (↓ 보조 생성기) | 해당 생성기 |
|
| `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/CombatMonster.codeblock` | ~2KB | `tools/monster/gen-combat-monster.mjs` | `node tools/monster/gen-combat-monster.mjs` |
|
||||||
@@ -21,10 +21,11 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
|
|||||||
| `RootDesk/MyDesk/LobbyNpc.codeblock`·`LobbyMobility.codeblock` | 각 ~2-3KB | `tools/player/gen-lobby-npc.mjs` | `node tools/player/gen-lobby-npc.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` (패치) | 해당 생성기 |
|
| `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 시 토큰 폭발.
|
- `.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/*.ui`** — UI는 6개 UIGroup으로 메이커 저작)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발.
|
||||||
- 게임 로직·UI 수정 = **`tools/deck/gen-slaydeck.mjs`(오케스트레이터 + codeblock Lua) 또는 `data/*.json`(데이터)을 수정** → 재생성 → 산출물은 통째로 커밋.
|
- **게임 로직 수정** = `tools/deck/gen-slaydeck.mjs`(오케스트레이터) + `tools/deck/cb/*.mjs`(codeblock Lua) 또는 `data/*.json`(데이터) 수정 → 재생성(`SlayDeckController.codeblock`+`common.gamelogic`만, **`.ui` 미접근**) → 통째로 커밋. **UI 수정 = 메이커에서**(생성기는 UI를 안 만든다).
|
||||||
- **UI emit은 HUD별 모듈** `tools/deck/hud/*.mjs`(charselect·shop·combat·map·deckall·soulshop 등 16종), **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 순서 유지.
|
- **codeblock 메서드(Lua)는 관심사별 모듈** `tools/deck/cb/*.mjs`(boot·screens·npc·navigation·layout·combat·hand·deckview·items·map·shop 등 20종 — 화면전환=`screens`·NPC=`npc`·포지션=`navigation`(월드 텔레포트)/`layout`(UI 슬롯 배치). 새 메서드는 관심사에 맞는 모듈에 작성하고, 한 모듈이 비대해지면 분할한다. 횡단 관심사를 한 모듈에 몰아넣지 않는다). **공유분**: 상수·데이터·lua 테이블 = `tools/deck/lib/{ui-helpers,data,codeblock}.mjs`(cb가 import — `MAX_MONSTERS`=4 등). prop 103개는 오케스트레이터 `writeCodeblocks`에 유지. 특정 메서드 수정은 `cb/<name>.mjs`만(의존: orchestrator→cb→lib 단방향). **cb 모듈은 원본 메서드 순서 보존이 바이트동일 조건**. **UI emit(옛 `hud/*.mjs` 15종·`gen-cardhand.mjs`)은 `tools/deck/legacy/`로 이관 — 휴면(생성기 미사용)**: UI가 메이커 저작이라 생성기가 안 만든다. (롤백용 `legacy/upsert-ui.mjs`는 직접 실행 시에만 옛 `DefaultGroup.ui`를 재생성.)
|
||||||
- 리팩터 시 **출력 바이트-동일 검증**: `node tools/deck/gen-slaydeck.mjs` 후 `node tools/verify/diffcheck.mjs [ref]`(워킹트리 vs ref(기본 HEAD) 줄바꿈 정규화 비교 — 산출물 경로를 명령줄에 노출 안 해 deny 회피). 산출물 ` M`은 보통 autocrlf churn이니 `git checkout --`로 복원.
|
- 리팩터 시 **출력 바이트-동일 검증**: `node tools/deck/gen-slaydeck.mjs` 후 `node tools/verify/diffcheck.mjs [ref]`(워킹트리 vs ref(기본 HEAD) 줄바꿈 정규화 비교 — 산출물 경로를 명령줄에 노출 안 해 deny 회피). 산출물 ` M`은 보통 autocrlf churn이니 `git checkout --`로 복원.
|
||||||
|
- **UI 전면 메이커 저작 (2026-06-17~)**: 단일 `DefaultGroup`을 7개 UIGroup으로 분리 — `DefaultGroup`(MainMenu+월드조작), `SelectUIGroup`(charselect/job), `LobbyUIGroup`(lobby/board/soulshop), `RunUIGroup`(combat/map/shop/rest/treasure/reward/cardhand/deck), `DeckUIGroup`(덱 도감), `PopupGroup`·`ToastGroup`. 컨트롤러(`cb/*.mjs`)는 엔티티 **경로**(`/ui/<UIGroup>/<Hud>/...`)로 텍스트·이미지·표시숨김·상태기반 위치/크기/색을 **런타임 주입**(레이아웃=메이커, 내용=컨트롤러 — 메이커가 이 경로 유지 필수). 몬스터 슬롯 = `RunUIGroup/CombatHud/MonsterStatus{1..4}`(자식 Name·Hp·Intent·HpBarFill·Buffs·BlockBadge·TargetMarker; TargetFrame 없음). **부트 흐름**: `OnBeginPlay`→MainMenu→(`MainMenu/NewGameButton`)→로비→run NPC(`OnLobbyNpcInteract` id=="run")→charselect→런. **재연결 검증**: `node tools/verify/cbgap.mjs`(cb 참조 경로↔.ui GAP 0이어야) + 재생성 후 `git status -- ui/` 변경 0(생성기 .ui 미접근 증명). 섹션→UIGroup 일괄 remap 마이그레이션은 `tools/deck/reconnect-ui-paths.mjs`(멱등). UIGroup별 .ui 분포 확인은 `tools/verify/uimap.mjs`.
|
||||||
- **머지 충돌(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)**: 다른 브랜치가 단일체를 수정해 충돌나면, 그쪽 버전(`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` 외):
|
- **보조 생성기**(각자 자기 산출물의 단일 소스 — 위 표의 메인 `gen-slaydeck.mjs` 외):
|
||||||
- `tools/camera/gen-camera.mjs` → `MapCamera.codeblock` + map01~05 카메라 부착 (값 `data/camera.json`)
|
- `tools/camera/gen-camera.mjs` → `MapCamera.codeblock` + map01~05 카메라 부착 (값 `data/camera.json`)
|
||||||
@@ -36,7 +37,7 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
|
|||||||
- `tools/player/gen-player-lock.mjs` → `PlayerLock.codeblock` + map01~05 부착
|
- `tools/player/gen-player-lock.mjs` → `PlayerLock.codeblock` + map01~05 부착
|
||||||
- `tools/player/gen-lobby-npc.mjs` → `LobbyNpc.codeblock`·`LobbyMobility.codeblock`
|
- `tools/player/gen-lobby-npc.mjs` → `LobbyNpc.codeblock`·`LobbyMobility.codeblock`
|
||||||
- `tools/player/freeze-turn-player.mjs` → `Global/DefaultPlayer.model` 이동 0 고정
|
- `tools/player/freeze-turn-player.mjs` → `Global/DefaultPlayer.model` 이동 0 고정
|
||||||
- `tools/deck/gen-cardhand.mjs` → `DefaultGroup.ui` 카드핸드 보조 패처
|
- (옛 `tools/deck/gen-cardhand.mjs`·`hud/*.mjs`는 `tools/deck/legacy/`로 이관 — 휴면, UI 메이커 저작 전환)
|
||||||
|
|
||||||
## 2. 산출물 검증은 카운트로, 내용 출력 금지
|
## 2. 산출물 검증은 카운트로, 내용 출력 금지
|
||||||
|
|
||||||
@@ -64,6 +65,8 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
|
|||||||
- 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create <spec.json>` / `merge <번호>`.
|
- 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create <spec.json>` / `merge <번호>`.
|
||||||
- PR 제목과 본문은 한국어로 작성한다.
|
- PR 제목과 본문은 한국어로 작성한다.
|
||||||
- 산출물 재생성 커밋은 소스 변경 커밋과 분리하거나, 메시지에 "산출물 재생성"을 명시.
|
- 산출물 재생성 커밋은 소스 변경 커밋과 분리하거나, 메시지에 "산출물 재생성"을 명시.
|
||||||
|
- **PR 머지 후 브랜치 삭제**: 머지된 `feature/*`·`docs/*` 브랜치는 로컬·원격 모두 삭제한다. 삭제 전 `git merge-base --is-ancestor origin/<브랜치> origin/main`로 완전 머지 확인(종료코드 0=완전 머지 → 삭제 가능). main에 없는 커밋이 남은 브랜치와 `codex/*` 등 작업 중 브랜치는 보존한다.
|
||||||
|
- **⚠️ main 머지 충돌 시 "머지 전체 revert" 금지 (타인 작업 유실 방지)**: 작업 브랜치에 `git merge main`(또는 origin/main) 했다가 충돌·문제가 나도 **그 머지 커밋을 통째로 `git revert` 하지 말 것.** main에 먼저 들어간 타인의 작업이 collateral로 전부 사라진다. 대신 **소스 충돌만 해소**하고 산출물(codeblock 등)은 **재생성**한다. 충돌이 산출물뿐이면 `git checkout --theirs`/재생성으로 끝. (2026-06-30 사고: codex `#98/#99`가 main 머지 후 그 머지를 revert해 `#96`의 버그수정 11개를 전부 날림 → 다시 재통합해야 했다. 복구는 `git diff <pre-merge> <내브랜치> -- <소스> | git apply --3way` 로 소스만 재적용 후 재생성하면 codex 변경과 충돌 없이 양립.)
|
||||||
|
|
||||||
## 5. 메이커(MSW) 연동 주의
|
## 5. 메이커(MSW) 연동 주의
|
||||||
|
|
||||||
@@ -85,3 +88,18 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
|
|||||||
- UI 텍스트에서는 정수값인 숫자에 `.0`을 붙이지 않는다. `1.0/1.0`이 아니라 `1/1`처럼 표시한다.
|
- UI 텍스트에서는 정수값인 숫자에 `.0`을 붙이지 않는다. `1.0/1.0`이 아니라 `1/1`처럼 표시한다.
|
||||||
- 생성기 내 Lua UI 코드에서 number 또는 숫자 문자열을 텍스트에 붙일 때는 `FormatNumber` 같은 포맷 헬퍼를 우선 사용한다.
|
- 생성기 내 Lua UI 코드에서 number 또는 숫자 문자열을 텍스트에 붙일 때는 `FormatNumber` 같은 포맷 헬퍼를 우선 사용한다.
|
||||||
- 소수부가 플레이어에게 의미 있을 때만 소수점 표기를 유지한다.
|
- 소수부가 플레이어에게 의미 있을 때만 소수점 표기를 유지한다.
|
||||||
|
|
||||||
|
## 8. codeblock 변수명
|
||||||
|
|
||||||
|
- cb(`tools/deck/cb/*.mjs`)의 Lua 지역변수는 **의미가 드러나는 이름**으로 작성한다(`e`→`entity`, `n`→`count`, `m`→`monster`, `lp`→`localPlayer`, `s`→`soulPoints`, `tr`→`transform`). `a`/`b`/`c` 같은 무의미 단일문자 변수는 금지.
|
||||||
|
- 단, 순수 반복 인덱스 `i`/`j`/`r`/`c`는 관용상 허용한다.
|
||||||
|
- 새 cb 메서드를 작성하거나 기존 메서드를 손댈 때 이 규칙을 적용한다(대규모 일괄 개명은 별도 작업으로).
|
||||||
|
|
||||||
|
## 9. 카드 데이터 규칙 (kind ↔ 효과 일치)
|
||||||
|
|
||||||
|
새 카드를 추가/수정할 때 `data/cards.json`의 `kind`는 카드의 효과·사용 메커니즘과 **반드시 일치**해야 한다. 안 맞으면 카드가 **사용 불가**거나 **재생 시 아무 효과 없는 死카드**가 된다(런타임 에러도 안 나고 sim 테스트도 못 잡음 — 정적 검증 필수).
|
||||||
|
|
||||||
|
- **`ResolveCardDrop` 사용 라우팅이 kind별로 다름**: `Attack`=몬스터 위에 드롭(`FindMonsterAtTouch>0` 필요)·`Skill`/`Power`=위로 스윕(`ui.y>-180`)·`Status`=unplayable. → **block·디버프·드로우 등 유틸만 있고 데미지가 없는 카드를 `Attack`으로 두면 위로 스윕으로 사용할 수 없다**(2026-06-30 아이언 바디 사고: block만 있는 방어카드가 Attack이라 전사 시작덱 4장이 먹통 → Skill로 수정).
|
||||||
|
- **`PlayCard`의 `Power` 분기는 PlayerPowers 등록만 하고 `damage`/`aoe`를 무시**한다. → 데미지 카드=`Attack`, 방어/유틸=`Skill`, 지속효과=`Power`(단 `powerEffect` 또는 지속/온플레이 power 필드 — `turnStart*`·`dex`·`thorns`·`intangible`·`attackPoison`·`drawDamage`·`shivX`·`cardPlayed*` 등 — 이 있어야 함). Power인데 power 효과 필드가 없으면 死카드(2026-06-30 분노 사고: `damage:4/aoe`만 있어 Power 분기서 무시됨 → kind Power→Attack으로 기능화).
|
||||||
|
- 새 효과 필드는 `docs/card-effect-fields.md` 사전에 등록하고 Lua(`tools/deck/cb/*.mjs`) + JS 미러(`tools/balance/sim-balance.mjs`) **양쪽에 핸들러 구현**(§6). 한쪽만 있으면 게임↔시뮬 드리프트.
|
||||||
|
- **검증: `node tools/verify/cardkinds.mjs`** — kind↔효과 위반(Attack-무데미지 / Power-무효과 / 미지원 kind)을 정적 검출(이상 0 = exit 0). 카드 추가/수정 후 반드시 실행. (관련 가드: 미선언 `self.X` = `cbprops.mjs`, UI 경로 = `cbgap.mjs`, 이중구현 = `sim-balance.test.mjs`.)
|
||||||
|
|||||||
23
RootDesk/MyDesk/01_blue_background_clean_1920x1080.sprite
Normal file
23
RootDesk/MyDesk/01_blue_background_clean_1920x1080.sprite
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "sprite://ac448e909f89464898708ce232ab8b51",
|
||||||
|
"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/ac448e909f89464898708ce232ab8b51/639173152021849887",
|
||||||
|
"upload_hash": "CCC4771B9353971748EF9BEE32D57F15090CE62C4BA6446B11E7842FC7AFDF1F",
|
||||||
|
"name": "01_blue_background_clean_1920x1080",
|
||||||
|
"resource_guid": "ac448e909f89464898708ce232ab8b51",
|
||||||
|
"resource_version": "6a32dd82c325482f6e2bb455"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
RootDesk/MyDesk/Character select bg.sprite
Normal file
23
RootDesk/MyDesk/Character select bg.sprite
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "sprite://7629b520ced54d508b040681d06741fb",
|
||||||
|
"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/7629b520ced54d508b040681d06741fb/639172208899179951",
|
||||||
|
"upload_hash": "C84DCE36101CF3F05E74F93F9B416E7D08D8B78B699E22CF8A6784994115DDAE",
|
||||||
|
"name": "Character select bg",
|
||||||
|
"resource_guid": "7629b520ced54d508b040681d06741fb",
|
||||||
|
"resource_version": "6a316d1a3d5de2eb0c7d345b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
RootDesk/MyDesk/MapleTree.codeblock
Normal file
36
RootDesk/MyDesk/MapleTree.codeblock
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "codeblock://4a220aa8-e014-4c7b-8234-fff8c5c66686",
|
||||||
|
"ContentType": "x-mod/codeblock",
|
||||||
|
"Content": "",
|
||||||
|
"Usage": 0,
|
||||||
|
"UsePublish": 1,
|
||||||
|
"UseService": 0,
|
||||||
|
"CoreVersion": "26.5.0.0",
|
||||||
|
"StudioVersion": "0.1.0.0",
|
||||||
|
"DynamicLoading": 0,
|
||||||
|
"ContentProto": {
|
||||||
|
"Use": "Json",
|
||||||
|
"Json": {
|
||||||
|
"CoreVersion": {
|
||||||
|
"Major": 0,
|
||||||
|
"Minor": 2
|
||||||
|
},
|
||||||
|
"ScriptVersion": {
|
||||||
|
"Major": 1,
|
||||||
|
"Minor": 1
|
||||||
|
},
|
||||||
|
"Description": "",
|
||||||
|
"Id": "4a220aa8-e014-4c7b-8234-fff8c5c66686",
|
||||||
|
"Language": 1,
|
||||||
|
"Name": "MapleTree",
|
||||||
|
"Type": 1,
|
||||||
|
"Source": 0,
|
||||||
|
"Target": null,
|
||||||
|
"Properties": [],
|
||||||
|
"Methods": [],
|
||||||
|
"EntityEventHandlers": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
23
RootDesk/MyDesk/Thumnail.sprite
Normal file
23
RootDesk/MyDesk/Thumnail.sprite
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "sprite://41ad73da083d41b0ae30bf7b86794376",
|
||||||
|
"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/41ad73da083d41b0ae30bf7b86794376/639172145413258274",
|
||||||
|
"upload_hash": "CFC620F96E1621FEE5594456FC8A4157BC6EF0D3E7661C5543293200FD364A85",
|
||||||
|
"name": "Thumnail",
|
||||||
|
"resource_guid": "41ad73da083d41b0ae30bf7b86794376",
|
||||||
|
"resource_version": "6a31544d335c959bb11f45eb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
RootDesk/MyDesk/restBgImage.sprite
Normal file
23
RootDesk/MyDesk/restBgImage.sprite
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "sprite://477679b832b44e099a30e4905078dbcb",
|
||||||
|
"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/477679b832b44e099a30e4905078dbcb/639172226341792721",
|
||||||
|
"upload_hash": "3E30B07C24C4BC4E373CDEA653035146D2F50ACC6484F6E9DA34E6179BB38F15",
|
||||||
|
"name": "restBgImage",
|
||||||
|
"resource_guid": "477679b832b44e099a30e4905078dbcb",
|
||||||
|
"resource_version": "6a3173ea002bbe95706406b6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
RootDesk/MyDesk/shopBgImage.sprite
Normal file
23
RootDesk/MyDesk/shopBgImage.sprite
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "sprite://28f3b10ac0334fbfbf29677bf963c57a",
|
||||||
|
"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/28f3b10ac0334fbfbf29677bf963c57a/639172222414073214",
|
||||||
|
"upload_hash": "01BE0B58F480BA86DA1D18BFE25C01E1B27219A14FE2DCD73456A7A48553CF15",
|
||||||
|
"name": "shopBgImage",
|
||||||
|
"resource_guid": "28f3b10ac0334fbfbf29677bf963c57a",
|
||||||
|
"resource_version": "6a3172612c6a274be88a130e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
RootDesk/MyDesk/treasureBgImage.sprite
Normal file
23
RootDesk/MyDesk/treasureBgImage.sprite
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Id": "",
|
||||||
|
"GameId": "",
|
||||||
|
"EntryKey": "sprite://dd6193fd37da4b12bcdbcdcf2fbe8e40",
|
||||||
|
"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/dd6193fd37da4b12bcdbcdcf2fbe8e40/639172228832890845",
|
||||||
|
"upload_hash": "3EDD046B291806637ADD12A77BF94CF00BDD9F4F9912C132B14323D9DE5F297C",
|
||||||
|
"name": "treasureBgImage",
|
||||||
|
"resource_guid": "dd6193fd37da4b12bcdbcdcf2fbe8e40",
|
||||||
|
"resource_version": "6a3174e32a2802c06419f288"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
cards_to_excel.bat
Normal file
7
cards_to_excel.bat
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
chcp 65001 >nul
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\cards\cards_excel.ps1" export
|
||||||
|
echo.
|
||||||
|
echo Press any key to close this window.
|
||||||
|
pause >nul
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"unique": "f5def2e8022b4e59a17d3c16414034fe",
|
"unique": "f5def2e8022b4e59a17d3c16414034fe",
|
||||||
"legend": "cff71f2e472041ce80c6fbd296f42e2d"
|
"legend": "cff71f2e472041ce80c6fbd296f42e2d"
|
||||||
},
|
},
|
||||||
"bandit": {
|
"rogue": {
|
||||||
"normal": "9487b06867bc46269ed1d855420f457f",
|
"normal": "9487b06867bc46269ed1d855420f457f",
|
||||||
"unique": "b3081fb2fb1445fa90b12b01481a78ef",
|
"unique": "b3081fb2fb1445fa90b12b01481a78ef",
|
||||||
"legend": "c357d2daf31a489d95b8fa47e50dd879"
|
"legend": "c357d2daf31a489d95b8fa47e50dd879"
|
||||||
@@ -25,11 +25,13 @@
|
|||||||
"firepoison": "magician",
|
"firepoison": "magician",
|
||||||
"icelightning": "magician",
|
"icelightning": "magician",
|
||||||
"cleric": "magician",
|
"cleric": "magician",
|
||||||
"bandit": "bandit",
|
"curse": "rogue",
|
||||||
"curse": "bandit",
|
"shiv": "rogue",
|
||||||
"shiv": "bandit",
|
"rogue": "rogue",
|
||||||
"poisoner": "bandit",
|
"assassin": "rogue",
|
||||||
"trickster": "bandit"
|
"hermit": "rogue",
|
||||||
|
"thief": "rogue",
|
||||||
|
"thiefmaster": "rogue"
|
||||||
},
|
},
|
||||||
"rewardWeights": {
|
"rewardWeights": {
|
||||||
"normal": 70,
|
"normal": 70,
|
||||||
|
|||||||
1329
data/cards.json
1329
data/cards.json
File diff suppressed because it is too large
Load Diff
BIN
data/cards.xlsx
Normal file
BIN
data/cards.xlsx
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"portraits": {
|
"portraits": {
|
||||||
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
|
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
|
||||||
"magician": "3b9ea1f066a744bb859df47fef817277",
|
"magician": "3b9ea1f066a744bb859df47fef817277",
|
||||||
"bandit": "efa920e58d31426486ef974106e7dc8b"
|
"rogue": "efa920e58d31426486ef974106e7dc8b"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,65 @@
|
|||||||
{ "kind": "Attack", "value": 12 },
|
{ "kind": "Attack", "value": 12 },
|
||||||
{ "kind": "Attack", "value": 24 }
|
{ "kind": "Attack", "value": 24 }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"octopus": {
|
||||||
|
"name": "문어",
|
||||||
|
"maxHp": 15,
|
||||||
|
"intents": [
|
||||||
|
{ "kind": "Attack", "value": 5 },
|
||||||
|
{ "kind": "Attack", "value": 6 },
|
||||||
|
{ "kind": "Defend", "value": 4 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kapa_drake": {
|
||||||
|
"name": "카파 드레이크",
|
||||||
|
"maxHp": 24,
|
||||||
|
"intents": [
|
||||||
|
{ "kind": "Attack", "value": 9 },
|
||||||
|
{ "kind": "Attack", "value": 6 },
|
||||||
|
{ "kind": "Defend", "value": 6 },
|
||||||
|
{ "kind": "Attack", "value": 11 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"junior_neki": {
|
||||||
|
"name": "주니어 네키",
|
||||||
|
"maxHp": 18,
|
||||||
|
"intents": [
|
||||||
|
{ "kind": "Attack", "value": 6 },
|
||||||
|
{ "kind": "Attack", "value": 8 },
|
||||||
|
{ "kind": "Debuff", "effect": "weak", "value": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"junior_bugi": {
|
||||||
|
"name": "주니어 부기",
|
||||||
|
"maxHp": 20,
|
||||||
|
"intents": [
|
||||||
|
{ "kind": "Attack", "value": 7 },
|
||||||
|
{ "kind": "Defend", "value": 5 },
|
||||||
|
{ "kind": "Attack", "value": 9 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dile": {
|
||||||
|
"name": "다일",
|
||||||
|
"maxHp": 65,
|
||||||
|
"intents": [
|
||||||
|
{ "kind": "Attack", "value": 13 },
|
||||||
|
{ "kind": "Defend", "value": 9 },
|
||||||
|
{ "kind": "Attack", "value": 8 },
|
||||||
|
{ "kind": "Attack", "value": 16 },
|
||||||
|
{ "kind": "Debuff", "effect": "weak", "value": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mano": {
|
||||||
|
"name": "마노",
|
||||||
|
"maxHp": 80,
|
||||||
|
"intents": [
|
||||||
|
{ "kind": "Defend", "value": 12 },
|
||||||
|
{ "kind": "Attack", "value": 14 },
|
||||||
|
{ "kind": "Debuff", "effect": "vuln", "value": 1 },
|
||||||
|
{ "kind": "Attack", "value": 10 },
|
||||||
|
{ "kind": "AddCard", "card": "Wound", "count": 1 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"activeEnemy": "slime",
|
"activeEnemy": "slime",
|
||||||
|
|||||||
7
docs/attack-poison.md
Normal file
7
docs/attack-poison.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 공격 중독
|
||||||
|
|
||||||
|
`attackPoison`은 전투 동안 유지되는 공용 카드 효과 필드입니다.
|
||||||
|
|
||||||
|
- 공격 카드가 실제 체력 피해를 주면 대상에게 지정된 수치만큼 중독을 부여합니다.
|
||||||
|
- 광역 공격은 피해를 받은 각 적에게 중독을 부여합니다.
|
||||||
|
- 현재 Thief Master와 Hermit의 `베놈`이 이 효과를 사용합니다.
|
||||||
72
docs/bandit-card-audit.md
Normal file
72
docs/bandit-card-audit.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# 도적 카드 구성 및 밸런스 기록
|
||||||
|
|
||||||
|
도적 계보의 카드 역할, 직업 이동 금지 대상, 공용 효과 필드를 정리합니다.
|
||||||
|
|
||||||
|
## 직업별 컨셉
|
||||||
|
|
||||||
|
- `rogue`: 시작 카드, 1차 스킬, 기초 공격·회피·방어
|
||||||
|
- `thief`: 단검 난타, 교활, 버리기, 중독의 시작
|
||||||
|
- `thiefmaster`: 교활·버리기 연계 완성, 광역 난타, 중독 증폭
|
||||||
|
- `assassin`: 표창 생성, 표창 연속 공격, 표창 비용·피해 보조
|
||||||
|
- `hermit`: 표창 보존·광역화·지속 생성 등 표창 빌드 완성
|
||||||
|
|
||||||
|
Rogue 단계에서도 분기 방향을 미리 경험할 수 있도록 약한 입문 카드를 유지합니다.
|
||||||
|
|
||||||
|
- 중독: `PoisonedStab`
|
||||||
|
- 표창: `LeadingStrike`
|
||||||
|
- 교활: `Untouchable`
|
||||||
|
|
||||||
|
## 스킬 카드 고정
|
||||||
|
|
||||||
|
실제 직업 스킬을 바탕으로 추가한 아래 카드는 다른 차수나 계열로 이동하지 않습니다.
|
||||||
|
|
||||||
|
- Rogue: `DoubleStab`, `LuckySeven`, `Haste`, `DarkSight`, `FlashJump`, `NimbleBody`
|
||||||
|
- Thief: `SavageBlow` 포함 9장
|
||||||
|
- Thief Master: `EdgeCarnival` 포함 11장
|
||||||
|
- Assassin: `ShurikenBurst` 포함 10장
|
||||||
|
- Hermit: `TripleThrow` 포함 9장
|
||||||
|
|
||||||
|
나머지 비스킬 카드는 컨셉에 맞춰 상위 직업으로 이동할 수 있습니다. 상위 직업은 하위 직업 카드를 함께 사용하므로, 이동은 해당 분기의 보상 풀을 제한하는 역할을 합니다.
|
||||||
|
|
||||||
|
## 공용 효과 필드
|
||||||
|
|
||||||
|
- `poison`, `innate`, `playableWhenDrawPileEmpty`
|
||||||
|
- `retain`, `sly`, `discard`, `discardAll`, `addShiv`, `addShivPerDiscard`, `turnStartShiv`, `retainOne`
|
||||||
|
- `turnStartDraw`, `turnStartDiscard`
|
||||||
|
- `nextTurnBlock`, `nextTurnDraw`, `nextTurnKeepBlock`, `nextTurnAttackMultiplier`, `nextTurnCopies`, `nextTurnSelectHandCard`
|
||||||
|
- `damagePerOtherHandCard`, `damagePerAttackPlayedThisTurn`, `damagePerDiscardedThisTurn`, `damagePerSkillInHand`, `otherHandAtLeast`, `bonusHitsWhenOtherHandAtLeast`
|
||||||
|
- `gainEnergy`, `drawUntilHandSize`, `drawPerDiscarded`, `cardPlayedBlock`, `blockGainMultiplier`, `blockPerDamageDealtThisTurn`, `nextSkillCostZero`, `skillCostReductionThisTurn`
|
||||||
|
- `firstCardDamageBonus`
|
||||||
|
- `drawDamage`, `drawPoison`, `shivDamageBonus`, `firstShivDamageBonus`, `shivRetain`, `shivAoe`, `attackDamageVsWeakMultiplier`, `poisonHits`, `poisonRandomTargets`, `skillSlyOnPlay`, `extraPoisonTicks`, `poisonApplicationBurstEvery`, `poisonApplicationBurstDamage`
|
||||||
|
|
||||||
|
## 중복 제거 및 보정
|
||||||
|
|
||||||
|
- 삭제: `Mirage`, `Accuracy`, `PhantomBlades`, `Adrenaline`, `Afterimage`, `Accelerant`, `Envenom`, `Tracking`
|
||||||
|
- 이유: 상위 직업 스킬 카드와 효과가 같거나, 비용 대비 열세라 별도 선택지가 되지 못함
|
||||||
|
- `Anticipate`: 턴 종료 시 얻은 민첩을 잃도록 실제 효과와 설명을 일치시킴
|
||||||
|
- `Backstab`, `Assassinate`, `TheHunt`, `PiercingWail`: 설명에 있던 소멸을 실제 필드에 반영
|
||||||
|
- 2차 지급: Thief `DaggerAcceleration`, Assassin `JavelinAcceleration`
|
||||||
|
- 3차 지급: Thief Master `Venom`, Hermit `SpiritJavelin`
|
||||||
|
|
||||||
|
## 카드 효율 검증
|
||||||
|
|
||||||
|
`node tools/balance/card-efficiency.mjs --runs 1000`으로 도적 계열 카드 전체를 검증합니다.
|
||||||
|
|
||||||
|
- 각 직업의 기준 덱에서 같은 종류의 카드 한 장을 교체하고 동일 시드로 반복 전투합니다.
|
||||||
|
- 승률, 승리 시 체력, 전투 턴을 합친 점수를 같은 직업·희귀도 중앙값과 비교합니다.
|
||||||
|
- 0코스트 에너지 생성, 재사용 가능한 영구 능력치, 저비용 2배 증폭처럼 자동 플레이가 놓치기 쉬운 구조도 별도로 검사합니다.
|
||||||
|
- 교활, 조건부 중독, 카드 보존처럼 플레이 순서 의존성이 큰 효과는 자동 시뮬레이션 하위권만으로 상향하지 않습니다.
|
||||||
|
|
||||||
|
2026-07-01 검증 결과 구조적 위험은 0장입니다. 주요 조정은 `독맥 터뜨리기`, `메아리 칼자국`, `소리 없는 제압`, `그리드`, `그림자 속도전`, `스틸`, 두 계열의 `피지컬 트레이닝`, `마크 오브 어쌔신`, `자벨린 액셀레이션`, `비장의 패`에 반영했습니다.
|
||||||
|
|
||||||
|
비스킬 카드 78장의 메이플풍 표시 이름은 `docs/rogue-card-names.md`에서 관리합니다. 메이플 원본 스킬 카드 45장의 이름은 변경하지 않습니다.
|
||||||
|
|
||||||
|
## 5섹션 캠페인 검증
|
||||||
|
|
||||||
|
`node tools/balance/rogue-campaign.mjs --runs 5000 --reward-min 5`로 전체 런을 검증합니다.
|
||||||
|
|
||||||
|
- 섹션마다 일반전 4회, 엘리트 1회, 보스 1회를 진행합니다.
|
||||||
|
- 1섹션은 Rogue, 2섹션은 2차 직업, 3~5섹션은 3차 직업 카드 풀을 사용합니다.
|
||||||
|
- 실제 카드 보상 확률, 전직 지급 카드, 시작·획득 유물, 체력 유지와 휴식 회복을 반영합니다.
|
||||||
|
- 몬스터 배율은 `1.00 → 1.075 → 1.15 → 1.30 → 1.45`이며 런타임과 시뮬레이터가 같은 공용 상수를 사용합니다.
|
||||||
|
- 5,000회 결과: Thief Master 완주 2.9%, Hermit 완주 3.6%. 자동 플레이와 일부 공격형 유물 미구현을 감안한 보수적 결과입니다.
|
||||||
126
docs/card-effect-fields.md
Normal file
126
docs/card-effect-fields.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Card Effect Fields
|
||||||
|
|
||||||
|
This file tracks the shared data fields used by `data/cards.json`.
|
||||||
|
The goal is to keep card behavior reusable instead of hardcoding one-off card names.
|
||||||
|
|
||||||
|
## Damage
|
||||||
|
|
||||||
|
- `damage`: base attack damage
|
||||||
|
- `damagePerOtherHandCard`: bonus damage per other card in hand
|
||||||
|
- `damagePerAttackPlayedThisTurn`: bonus damage per attack played this turn
|
||||||
|
- `damagePerDiscardedThisTurn`: bonus damage per card discarded this turn
|
||||||
|
- `damagePerSkillInHand`: bonus damage per skill card in hand
|
||||||
|
- `damagePerCardDrawnThisCombat`: bonus damage per card drawn this combat
|
||||||
|
- `damagePerTurn`: damage applied at turn start
|
||||||
|
- `cardPlayedDamage`: damage when the card is played
|
||||||
|
- `cardPlayedRandomDamage`: random damage when the card is played
|
||||||
|
- `rewardOnKill`: gain bonus reward screens when the card kills
|
||||||
|
- `randomTargetEachHit`: choose a random alive enemy for each hit
|
||||||
|
- `repeatOnKill`: repeat the attack when it kills at least one enemy
|
||||||
|
- `firstCardDamageBonus`: bonus damage for the first card played this turn
|
||||||
|
- `drawDamage`: damage dealt when a card is drawn
|
||||||
|
- `blockPerDamageDealtThisTurn`: gain block equal to damage dealt this turn
|
||||||
|
- `shivDamageBonus`: bonus damage for all Shivs
|
||||||
|
- `firstShivDamageBonus`: bonus damage for the first Shiv each turn
|
||||||
|
- `attackDamageVsWeakMultiplier`: multiplier when the attack hits Weak targets
|
||||||
|
- `useAllEnergy`: treat the card as spending all available energy
|
||||||
|
- `xDamagePerEnergy`: scale attack damage by energy spent
|
||||||
|
- `xWeakPerEnergy`: scale Weak applied by energy spent
|
||||||
|
|
||||||
|
## Block and utility
|
||||||
|
|
||||||
|
- `block`: gain block
|
||||||
|
- `cardPlayedBlock`: gain block whenever a card is played
|
||||||
|
- `blockGainMultiplier`: multiplier for block gained this turn
|
||||||
|
- `hits`: multi-hit count
|
||||||
|
- `aoe`: hit all enemies
|
||||||
|
- `pierce`: ignore block
|
||||||
|
- `draw`: draw cards immediately
|
||||||
|
- `drawUntilHandSize`: draw until hand reaches a target size
|
||||||
|
- `drawSkillBlock`: gain block for each Skill drawn
|
||||||
|
- `drawPoison`: apply poison when a card is drawn
|
||||||
|
- `handCostZeroThisTurn`: make hand cards cost 0 this turn
|
||||||
|
- `drawDisabledThisTurn`: disable draw for the rest of the turn
|
||||||
|
- `heal`: heal immediately
|
||||||
|
- `gainEnergy`: gain energy immediately
|
||||||
|
- `strength`: gain Strength
|
||||||
|
- `dex`: gain Dexterity
|
||||||
|
- `thorns`: gain Thorns
|
||||||
|
- `selfVuln`: apply Vulnerable to self
|
||||||
|
- `extraPoisonTicks`: add extra poison ticks at enemy turn start
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- `weak`: apply Weak
|
||||||
|
- `vuln`: apply Vulnerable
|
||||||
|
- `poison`: apply Poison
|
||||||
|
- `poisonHits`: apply poison multiple times
|
||||||
|
- `poisonRandomTargets`: spread poison applications across random alive enemies
|
||||||
|
- `poisonIfTargetPoisoned`: apply poison only if the target is already poisoned
|
||||||
|
- `poisonApplicationBurstEvery`: trigger a burst every N poison applications
|
||||||
|
- `poisonApplicationBurstDamage`: burst damage when the poison application threshold is reached
|
||||||
|
- `skillSlyOnPlay`: make a played Skill card count as sly when it is later discarded
|
||||||
|
- `turnHandSlyCount`: mark up to N other Skill cards in hand as sly for this turn
|
||||||
|
- `attackPoison`: apply poison when attack damage is dealt
|
||||||
|
- `intangible`: reduce incoming damage to 1 for the duration
|
||||||
|
- `endTurnDexLoss`: lose Dexterity at end of turn
|
||||||
|
- `combatCostReductionOnPlay`: reduce this card's cost each time it is played this combat
|
||||||
|
- `enemyStrengthLossThisTurn`: reduce enemy Strength for the rest of the turn
|
||||||
|
- `affectsAllEnemies`: apply the card's debuffs to every alive enemy
|
||||||
|
- `removeEnemyBlock`: clear enemy block when the card resolves
|
||||||
|
- `removeEnemyArtifact`: consume enemy Artifact when the card resolves
|
||||||
|
|
||||||
|
`poison` deals damage at enemy turn start and then decreases by 1.
|
||||||
|
|
||||||
|
## Shivs and discard
|
||||||
|
|
||||||
|
- `discard`: discard a chosen number of cards from hand
|
||||||
|
- `discardAll`: discard the whole hand
|
||||||
|
- `drawPerDiscarded`: draw one extra card per discarded card
|
||||||
|
- `addShiv`: create Shiv cards
|
||||||
|
- `addShivPerDiscard`: create one Shiv per discarded card
|
||||||
|
- `shivRetain`: Shiv cards are retained at end of turn
|
||||||
|
- `shivAoe`: Shiv cards hit all enemies for the turn
|
||||||
|
- `sly`: trigger on discard
|
||||||
|
- `retain`: keep the card at end of turn
|
||||||
|
|
||||||
|
## Powers and turn effects
|
||||||
|
|
||||||
|
- `powerEffect: "strengthPerTurn"`
|
||||||
|
- `powerEffect: "energyPerTurn"`
|
||||||
|
- `powerEffect: "blockPerTurn"`
|
||||||
|
- `powerEffect: "poisonPerTurn"`
|
||||||
|
- `powerEffect: "damagePerTurn"`
|
||||||
|
- `powerEffect: "retainOne"`
|
||||||
|
- `turnStartShiv`: create Shivs at turn start
|
||||||
|
- `turnStartDraw`: draw cards at turn start
|
||||||
|
- `turnStartDiscard`: discard cards at turn start
|
||||||
|
|
||||||
|
## Next turn planning
|
||||||
|
|
||||||
|
- `nextTurnBlock`: gain block next turn
|
||||||
|
- `nextTurnDraw`: draw extra cards next turn
|
||||||
|
- `nextTurnKeepBlock`: keep block next turn
|
||||||
|
- `nextTurnAttackMultiplier`: attack multiplier next turn
|
||||||
|
- `nextTurnCopies`: copy a chosen card next turn
|
||||||
|
- `nextTurnSelectHandCard`: choose a card from the current hand for next turn copies
|
||||||
|
- `nextTurnSelectPrompt`: prompt text for selection UI
|
||||||
|
- `nextSkillRepeatCount`: repeat the next Skill's effect
|
||||||
|
- `nextSkillCostZero`: make the next Skill cost 0
|
||||||
|
- `skillCostReductionThisTurn`: reduce Skill costs this turn
|
||||||
|
|
||||||
|
## Misc
|
||||||
|
|
||||||
|
- `innate`: place the card in the opening hand
|
||||||
|
- `playableWhenDrawPileEmpty`: only playable when the draw pile is empty
|
||||||
|
- `exhaust`: exhaust after use
|
||||||
|
- `unplayable`: cannot be played
|
||||||
|
- `curse`: curse card
|
||||||
|
- `token`: token card
|
||||||
|
- `endTurnDamage`: damage if the card remains in hand at end of turn
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Prefer shared fields over card-specific branches.
|
||||||
|
- Reuse the same field name for the same behavior.
|
||||||
|
- Add a new shared field before adding more special-case card logic.
|
||||||
5
docs/card-play-damage.md
Normal file
5
docs/card-play-damage.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 카드 사용 시 피해
|
||||||
|
|
||||||
|
`cardPlayedDamage`는 카드를 사용할 때마다 현재 대상에게 체력을 직접 깎는 공용 효과입니다. 방어도는 무시하고, 같은 필드를 다른 카드에도 그대로 붙여 재사용할 수 있습니다.
|
||||||
|
|
||||||
|
`cardPlayedRandomDamage`는 같은 시점에 살아 있는 적 하나를 랜덤으로 골라 체력을 직접 깎습니다. `Strangle`과 `SerpentForm` 같은 카드가 이 계열을 씁니다.
|
||||||
39
docs/codex-workflow.md
Normal file
39
docs/codex-workflow.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Codex Workflow
|
||||||
|
|
||||||
|
이 저장소에서 작업할 때는 토큰과 변경량을 아끼는 쪽을 기본으로 둔다.
|
||||||
|
|
||||||
|
## 작업 원칙
|
||||||
|
|
||||||
|
- 이미 확인한 사실은 다시 읽지 않는다.
|
||||||
|
- 같은 내용을 통째로 지우고 새로 쓰지 않는다.
|
||||||
|
- 수정은 가능한 한 `apply_patch`로 섹션 단위만 한다.
|
||||||
|
- 문서는 전체 재작성보다 부분 수정으로 유지한다.
|
||||||
|
- 카드 구현은 한 번에 하나씩, 공용 필드 우선으로 넣는다.
|
||||||
|
- 새 기능은 `데이터 1곳 + 런타임 1곳 + 테스트 1곳` 순서로 맞춘다.
|
||||||
|
|
||||||
|
## 읽기 원칙
|
||||||
|
|
||||||
|
- 파일은 필요한 것만 읽는다.
|
||||||
|
- 비슷한 파일은 병렬로 한 번에 확인한다.
|
||||||
|
- 같은 정보를 여러 번 요약하지 않는다.
|
||||||
|
|
||||||
|
## 쓰기 원칙
|
||||||
|
|
||||||
|
- 공용으로 표현 가능한 효과는 카드 전용 분기로 만들지 않는다.
|
||||||
|
- 같은 의미의 효과는 같은 필드 이름을 쓴다.
|
||||||
|
- 문서는 카드별 상태표와 공용 필드 사전을 분리해서 유지한다.
|
||||||
|
- 카드 `kind`는 효과와 맞춘다 — 데미지 카드=`Attack`, block·유틸만 있으면=`Skill`, 지속효과=`Power`(`powerEffect` 또는 power 필드 필수). 안 맞으면 사용 불가/死카드가 된다(Power 분기는 damage/aoe 무시, Attack은 몬스터 드롭 라우팅).
|
||||||
|
- 새 효과 필드는 Lua(`cb/*.mjs`)와 JS 미러(`tools/balance/sim-balance.mjs`) 양쪽에 구현한다(한쪽만 = 게임↔시뮬 드리프트).
|
||||||
|
|
||||||
|
## 응답 원칙
|
||||||
|
|
||||||
|
- 중간 보고는 짧게 한다.
|
||||||
|
- 바뀐 점과 남은 점만 말한다.
|
||||||
|
- 불필요한 재설명은 줄인다.
|
||||||
|
|
||||||
|
## 검증·통합 원칙
|
||||||
|
|
||||||
|
- 카드/cb 변경 후 검증 스위트를 돌린다: `node tools/verify/cardkinds.mjs`(kind↔효과)·`cbprops.mjs`(미선언 `self.X` 필드)·`cbgap.mjs`(UI 경로) + `node --test tools/balance/sim-balance.test.mjs`(이중구현 미러). 이상 0을 확인한 뒤 산출물을 갱신한다.
|
||||||
|
- 작업 브랜치에 `main`을 머지했다가 충돌·문제가 나도 그 머지 커밋을 통째로 `git revert`하지 않는다 — main에 먼저 들어간 타인 작업이 collateral로 사라진다(2026-06-30 `#98/#99`가 `#96` 11개 수정을 이렇게 날린 사고). 소스 충돌만 해소하고 산출물(codeblock 등)은 재생성한다.
|
||||||
|
- 하네스 규칙의 최종 권위는 `RULES.md`(§1 산출물 읽기/수정 금지·§4 git/PR·§6 이중구현 동기화·§9 카드 kind)이고, codex 전용 하드룰은 `docs/codex-working-rules.md`다. 작업 전 둘 다 따른다.
|
||||||
|
|
||||||
11
docs/codex-working-rules.md
Normal file
11
docs/codex-working-rules.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Codex Working Rules
|
||||||
|
|
||||||
|
1. 사용자가 특정 클래스만 수정하라고 했으면 그 클래스 외의 데이터, 설명문, 밸런스 문구는 건드리지 않는다.
|
||||||
|
2. 기존 한글 텍스트는 요청이 없으면 임의로 바꾸지 않는다.
|
||||||
|
3. 전직 구조를 바꿀 때는 실제 직업명만 사용한다. 임의의 내부 분류명이나 새 직업명을 사용자-facing 구조에 추가하지 않는다.
|
||||||
|
4. 대량 치환 전에 수정 대상 파일과 범위를 먼저 확인하고, 원본 문자열이 깨진 상태면 치환 작업을 진행하지 않는다.
|
||||||
|
5. 생성기 파일을 크게 수정할 때는 `node --check`와 생성기 실행으로 문법을 먼저 검증한 뒤 산출물을 갱신한다.
|
||||||
|
6. 작업 브랜치에 `main`을 머지했다가 충돌·문제가 나도 **그 머지 커밋을 통째로 `git revert`하지 않는다** — main에 먼저 들어간 타인 작업이 collateral로 사라진다(2026-06-30 `#98/#99`가 `#96`의 버그수정 11개를 이렇게 전부 날림). 소스 충돌만 해소하고 산출물(codeblock 등)은 재생성한다. (RULES §4)
|
||||||
|
7. 카드 `kind`는 효과와 일치시킨다 — 데미지 카드=`Attack`, block·유틸만 있으면=`Skill`, 지속효과=`Power`(`powerEffect` 또는 power 필드 필수). 안 맞으면 사용 불가/死카드가 된다(2026-06-30 아이언 바디=Attack인데 block만, 분노=Power인데 damage만 → 둘 다 먹통). 카드 추가/수정 후 `node tools/verify/cardkinds.mjs`로 검증(이상 0 = exit 0). (RULES §9)
|
||||||
|
8. 카드/cb 변경 후 검증 스위트를 돌린다: `node tools/verify/cardkinds.mjs`(kind↔효과)·`cbprops.mjs`(미선언 `self.X` 필드)·`cbgap.mjs`(UI 경로) + `node --test tools/balance/sim-balance.test.mjs`(이중구현 미러). 새 효과 필드는 Lua(`cb/*.mjs`)와 JS 미러(`tools/balance/sim-balance.mjs`) **양쪽**에 구현(한쪽만 = 게임↔시뮬 드리프트). (RULES §6)
|
||||||
|
9. 하네스 규칙의 권위는 `RULES.md`다 — 작업 전 RULES.md(§1 산출물 읽기/수정 금지·§4 git/PR·§6 이중구현 동기화·§9 카드 kind)를 읽고 따른다.
|
||||||
86
docs/deck-concept.md
Normal file
86
docs/deck-concept.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# SlayMaple 덱 컨셉 & 직업 스킬셋
|
||||||
|
|
||||||
|
> SlayMaple 카드 덱의 **직업별 컨셉 · 메이플 스킬셋 · Slay the Spire 2 차용 매칭** 설계 문서.
|
||||||
|
> 원칙: 카드 한 장 = **STS2 메커니즘(뼈대) + 메이플 스킬(외형)**. STS 고유 *표현*(카드명·아트·UI)은 모방 금지, *메커니즘*만 차용(IP 해석 심사 대비).
|
||||||
|
> 수치(데미지·코스트·등급)는 `tools/balance/sim-balance.mjs`로 검증. 본 문서는 *어떤 스킬을 어떤 카드로* 만들지의 설계도.
|
||||||
|
|
||||||
|
기준: 메이플 = **클래식(빅뱅 이전)** 스킬 외형, STS = **Slay the Spire 2**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 직업 ↔ STS2 매칭 요약
|
||||||
|
|
||||||
|
| 직업 | 컨셉 | STS2 차용 |
|
||||||
|
|---|---|---|
|
||||||
|
| ⚔️ 전사 | 단단한 탱커/브루저 | The Ironclad (힘·방어·소멸) |
|
||||||
|
| 🗡️ 도적 | 단검 난사 / 독 | The Silent (표창·독·교활) |
|
||||||
|
| 🔮 법사 | 약체 + 게이지 운용 | The Defect (오브·집중) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚔️ 전사 (Warrior) — HP80 · 탱커
|
||||||
|
|
||||||
|
방어를 쌓고 버티다 역공하는 브루저. Ironclad의 두 축을 2차 전직으로 분화.
|
||||||
|
|
||||||
|
### 파이터 — 콤보 브루저형 탱커
|
||||||
|
- **콤보 규칙**: 공격 카드를 **연속으로** 사용하면 콤보가 쌓인다. **방어·파워 등 비공격(Skill/Power) 카드를 사용하면 콤보가 리셋(0)** 된다.
|
||||||
|
- 콤보가 쌓일수록 **데미지 증가 버프(힘 계열)** 를 받는다 → 방어를 포기하고 공격을 몰아칠수록 강해지는 리스크/리워드.
|
||||||
|
- 차용: Ironclad 힘 스택/Demon Form + 콤보. 메이플 외형: 콤보 어택·분노·브랜디시.
|
||||||
|
|
||||||
|
### 페이지 — 방어 축적 → 바디 슬램 카운터
|
||||||
|
- **위협**(전체 적 약화+취약 디버프)로 버티며 **방어도를 축적**(아이언 월 등 + 방어 유지 retain).
|
||||||
|
- **바디 슬램**: 현재 방어도에 비례한 피해로 카운터. 파워 가드(반사) 보조.
|
||||||
|
- 차용: Ironclad 방어 빌드(Barricade+Entrench→Body Slam). 메이플 외형: 위협·아이언 월·파워 가드.
|
||||||
|
|
||||||
|
### 스피어맨 — 유지/리치형
|
||||||
|
- 하이퍼 바디(최대 HP↑)·아이언 월(방어 유지)·창 리치 광역. 공격 스케일(파이터)·방어 카운터(페이지)와 구분되는 지속 탱.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗡️ 도적 (Thief) — HP70 · 단검/독
|
||||||
|
|
||||||
|
Slay the Spire *Silent* 차용. **형(codex)이 Silent 88장 완역 포트 + 스킬 아이콘 적용 완료.** 3대 아키타입:
|
||||||
|
|
||||||
|
- **표창(Shiv) 난사**: 0코스트 표창 토큰 대량 생성 → 공격마다 연계. (정밀=표창 피해↑, 칼날 부채=표창 전체화)
|
||||||
|
- **독(Poison)**: 중독 중첩 → 매턴 틱뎀. (유독 가스·발병·촉진제·독 바르기)
|
||||||
|
- **교활(Sly)·버림(discard)**: 버려질 때 무료 발동, 얇은 덱 빠른 순환.
|
||||||
|
|
||||||
|
### 2차 갈래
|
||||||
|
- **어쌔신** — 표창 난사 + 크리 / 흡혈(드레인) 중심.
|
||||||
|
- **시프** — 단검 난타(새비지 블로우 = 다단히트) + 독 / 버림 중심.
|
||||||
|
|
||||||
|
> 남은 작업: 카드명이 STS 직역(무력화·배신·아드레날린 등) → **어쌔신/시프 메이플 스킬명으로 재서사** + 멀티플레이어 전제 카드(측면 공격·비열함·추적) 싱글 출품용 정리.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 법사 (Magician) — HP70 · 약체/게이지
|
||||||
|
|
||||||
|
몸은 약하나 **게이지(오브) 운용**으로 다중 공격·화력 집중. **오브 메커니즘은 위자드(불/독·썬/콜)에만 적용**, 클레릭은 별도 보조 컨셉.
|
||||||
|
|
||||||
|
### 위자드(불/독) — 독 + 불 시너지
|
||||||
|
- **독을 묻히는 스킬**(포이즌 브레스 등)으로 대상에 독을 부여(독뎀 = DoT).
|
||||||
|
- **독이 묻은 적에게 불 카드(파이어 애로우 등)를 쓰면 추가 데미지** — *독뎀 상수* 보너스(독 스택/상수 비례).
|
||||||
|
- 즉 "독 깔기 → 불로 폭발"의 2단 콤보. 불·독 오브로 운용.
|
||||||
|
|
||||||
|
### 위자드(썬/콜) — 오브 운용(썬더 다중 / 콜드 빙결)
|
||||||
|
- **오브로 썬더·콜드를 보유**. **썬더 = 다중 공격 특화**(AoE·다단). **콜드 = 빙결 부여**(빙결 = *취약과 동일 효과* 를 데미지와 함께).
|
||||||
|
- **오브를 사용하는 만큼 오브를 획득하거나 다중 소모**하는 방식 — 오브 수급/소비 운용이 핵심.
|
||||||
|
|
||||||
|
### 클레릭 — 보조(회복·버프) · 오브 없음
|
||||||
|
- **회복 스킬**(힐)과 **각종 버프**(블레스 등) 중심의 서포트.
|
||||||
|
- **언데드 계열 몬스터에게는 힐로 공격** 가능 — 보조 힐러 정체성.
|
||||||
|
|
||||||
|
> ⚠️ 위자드 오브/게이지·전사 콤보 스택·바디 슬램·독뎀 시너지는 **신규 메커니즘** — `tools/deck/gen-slaydeck.mjs`(전투 규칙) + `tools/balance/sim-balance.mjs`(JS 미러) 양쪽 구현·동기화 필요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 차용 경계 (IP 심사 대비)
|
||||||
|
|
||||||
|
- 차용 OK = **메커니즘**(콤보 스택·방어→피해 전환·독+불 시너지·오브 게이지·빙결=취약 등 시스템).
|
||||||
|
- 모방 금지 = STS 고유 **표현**(카드명·아트·UI 직접 사용).
|
||||||
|
- 만점 루트 = STS2 메커니즘을 **메이플 스킬·외형으로 완전 재서사화**.
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
|
||||||
|
- 카드 데이터 단일 소스: `data/cards.json` (현 122장: 전사 18·마법사 14·도적 88 + Shiv·저주)
|
||||||
|
- 메이플 스킬 외형 매핑·STS2 캐릭터 상세는 박재오 개인 위키 `프로젝트-메이플-덱빌딩-스킬구성` / `프로젝트-메이플-STS2-차용-덱컨셉` 참조.
|
||||||
8
docs/draw-count.md
Normal file
8
docs/draw-count.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 전투 드로우 누적
|
||||||
|
|
||||||
|
`damagePerCardDrawnThisCombat`은 이번 전투 동안 실제로 뽑힌 카드 수를 기준으로 공격력을 올리는 공용 필드입니다.
|
||||||
|
|
||||||
|
적용 예시:
|
||||||
|
|
||||||
|
- `Murder`: 이번 전투 동안 뽑은 카드 1장당 피해량이 1 증가
|
||||||
|
|
||||||
22
docs/draw-skill-block.md
Normal file
22
docs/draw-skill-block.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 드로우 연동 효과
|
||||||
|
|
||||||
|
드로우 결과를 받아 후속 효과를 처리하는 공용 패턴을 정리합니다.
|
||||||
|
|
||||||
|
## 현재 구현
|
||||||
|
|
||||||
|
- `draw`: 카드를 뽑음
|
||||||
|
- `drawUntilHandSize`: 손패가 지정 수치가 될 때까지 뽑음
|
||||||
|
- `drawSkillBlock`: 이번 카드로 뽑힌 카드 중 스킬 카드마다 방어도를 얻음
|
||||||
|
|
||||||
|
## 동작 방식
|
||||||
|
|
||||||
|
- 드로우 함수는 이번에 뽑힌 카드 ID 목록을 반환합니다.
|
||||||
|
- 카드 효과는 그 목록을 보고 조건을 판정합니다.
|
||||||
|
- 그래서 `EscapePlan` 같은 카드뿐 아니라, 나중에 같은 규칙이 필요한 카드에도 같은 필드를 붙이면 됩니다.
|
||||||
|
|
||||||
|
## 예시
|
||||||
|
|
||||||
|
- `EscapePlan`
|
||||||
|
- `draw = 1`
|
||||||
|
- `drawSkillBlock = 3`
|
||||||
|
|
||||||
5
docs/intangible.md
Normal file
5
docs/intangible.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 불가침
|
||||||
|
|
||||||
|
`intangible`는 카드를 사용할 때 플레이어에게 불가침 수치를 부여하는 공용 필드입니다. 불가침이 남아 있는 동안 받는 피해는 1로 줄어들고, 턴이 끝날 때 1씩 감소합니다.
|
||||||
|
|
||||||
|
`endTurnDexLoss`는 그 카드가 활성화된 동안 매 턴 종료 시 민첩을 잃게 만드는 공용 필드입니다. `WraithForm` 같은 카드가 이 조합을 사용합니다.
|
||||||
12
docs/next-skill-repeat.md
Normal file
12
docs/next-skill-repeat.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Next Skill Repeat
|
||||||
|
|
||||||
|
`nextSkillRepeatCount`는 다음에 사용하는 스킬 카드의 효과를 추가 횟수만큼 다시 적용하는 공용 필드입니다.
|
||||||
|
|
||||||
|
현재 구현은 카드가 발동할 때 이 수치를 전역 상태에 누적해 두고, 다음 스킬 카드가 실제로 사용되면 그 효과를 같은 카드에 대해 다시 한 번 이상 적용합니다. 카드 종류는 고정하지 않았기 때문에, 같은 필드를 다른 카드에도 그대로 붙일 수 있습니다.
|
||||||
|
|
||||||
|
예시:
|
||||||
|
|
||||||
|
- `Burst`
|
||||||
|
- `nextSkillRepeatCount = 1`
|
||||||
|
- 다음 스킬을 한 번 더 적용
|
||||||
|
|
||||||
5
docs/reward-on-kill.md
Normal file
5
docs/reward-on-kill.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 처치 보상
|
||||||
|
|
||||||
|
`rewardOnKill`은 해당 카드가 적을 처치했을 때 전투 보상 화면을 한 번 더 이어서 보여주는 공용 필드입니다. 현재 보상 UI는 3장 선택을 유지하고, 보상 화면만 추가로 한 번 더 열립니다.
|
||||||
|
|
||||||
|
`TheHunt`는 이 규칙을 사용합니다. 같은 패턴이 필요한 다른 카드에도 그대로 붙일 수 있습니다.
|
||||||
97
docs/rogue-card-names.md
Normal file
97
docs/rogue-card-names.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 도적 비스킬 카드 이름
|
||||||
|
|
||||||
|
메이플스토리 원본 스킬을 바탕으로 만든 카드 45장은 이름을 고정합니다.
|
||||||
|
아래 78장은 다른 직업의 스킬명을 점유하지 않도록 도적 계열의 독자적인 이름으로 변경했습니다.
|
||||||
|
|
||||||
|
## Rogue
|
||||||
|
|
||||||
|
- `Neutralize`: 무력화 -> 커닝식 견제
|
||||||
|
- `SilentStrike`: 타격 -> 초보 도적의 칼끝
|
||||||
|
- `Survivor`: 생존자 -> 골목길 생존술
|
||||||
|
- `SilentDefend`: 수비 -> 낡은 가죽 방패
|
||||||
|
- `Slice`: 칼질 -> 짧은 단검질
|
||||||
|
- `PoisonedStab`: 독 찌르기 -> 초록 독단검
|
||||||
|
- `SuckerPunch`: 불의의 일격 -> 골목 기습
|
||||||
|
- `LeadingStrike`: 선제 타격 -> 초보 표창 던지기
|
||||||
|
- `Anticipate`: 예측 -> 럭키 예감
|
||||||
|
- `Deflect`: 튕겨내기 -> 단검 쳐내기
|
||||||
|
- `Backflip`: 공중제비 -> 커닝 곡예
|
||||||
|
- `DodgeAndRoll`: 구르기 -> 골목 구르기
|
||||||
|
- `Untouchable`: 범접 불가 -> 연막 속 숨기
|
||||||
|
- `Backstab`: 배신 -> 그림자 등찌르기
|
||||||
|
- `EscapePlan`: 탈출구 -> 비상용 연막탄
|
||||||
|
|
||||||
|
## Thief
|
||||||
|
|
||||||
|
- `DaggerSpray`: 단검 분사 -> 단검비
|
||||||
|
- `DaggerThrow`: 단검 투척 -> 비도 투척
|
||||||
|
- `FollowThrough`: 완수 -> 연달아 찌르기
|
||||||
|
- `FlickFlack`: 재주넘기 -> 커닝 난무
|
||||||
|
- `Prepared`: 예비 -> 비장의 패
|
||||||
|
- `PiercingWail`: 귀를 찢는 비명 -> 골목의 살기
|
||||||
|
- `DeadlyPoison`: 맹독 -> 맹독 조제
|
||||||
|
- `Snakebite`: 뱀 물기 -> 독니 단검
|
||||||
|
- `PreciseCut`: 정밀한 베기 -> 급소 절개
|
||||||
|
- `Finisher`: 마무리 -> 마지막 칼끝
|
||||||
|
- `MementoMori`: 메멘토 모리 -> 사신의 장부
|
||||||
|
- `Strangle`: 목 조르기 -> 그림자 올가미
|
||||||
|
- `Dash`: 돌진 -> 뒷골목 돌파
|
||||||
|
- `CalculatedGamble`: 계산된 도박 -> 메소 건 승부
|
||||||
|
- `Expose`: 들춰내기 -> 약점 들추기
|
||||||
|
- `Acrobatics`: 곡예 -> 지붕 위 곡예
|
||||||
|
- `HandTrick`: 손기술 -> 재빠른 손놀림
|
||||||
|
- `Expertise`: 전문성 -> 노련한 단검술
|
||||||
|
- `BubbleBubble`: 차오르는 독 -> 독액 농축
|
||||||
|
- `Blur`: 흐릿함 -> 흐린 잔영
|
||||||
|
- `LegSweep`: 다리 걸기 -> 발목 베기
|
||||||
|
- `Reflex`: 반사신경 -> 찰나의 반응
|
||||||
|
- `Tactician`: 전략가 -> 골목길 책략
|
||||||
|
- `WellLaidPlans`: 괜찮은 전략 -> 빈틈없는 작전
|
||||||
|
- `Footwork`: 발놀림 -> 사뿐한 발놀림
|
||||||
|
- `NoxiousFumes`: 유독 가스 -> 숨막히는 독연기
|
||||||
|
|
||||||
|
## Thief Master
|
||||||
|
|
||||||
|
- `BouncingFlask`: 탄성 플라스크 -> 통통 독병
|
||||||
|
- `Haze`: 아지랑이 -> 보랏빛 독연기
|
||||||
|
- `Outbreak`: 발병 -> 독맥 터뜨리기
|
||||||
|
- `Speedster`: 스피드스터 -> 그림자 속도전
|
||||||
|
- `GrandFinale`: 대단원의 막 -> 커닝의 대단원
|
||||||
|
- `Assassinate`: 암살 -> 어둠 속 급소
|
||||||
|
- `EchoingSlash`: 메아리 참격 -> 메아리 칼자국
|
||||||
|
- `Murder`: 살해 -> 쌓여가는 살의
|
||||||
|
- `Malaise`: 불쾌 -> 기운 빼는 독
|
||||||
|
- `ShadowStep`: 그림자 걸음 -> 그림자 발자국
|
||||||
|
- `Shadowmeld`: 그림자 은신 -> 연막 속 은신
|
||||||
|
- `CorrosiveWave`: 부식성 파도 -> 부식 독물결
|
||||||
|
- `Burst`: 폭주 -> 연속 술수
|
||||||
|
- `KnifeTrap`: 칼날 함정 -> 숨은 칼날덫
|
||||||
|
- `BulletTime`: 불릿 타임 -> 멈춘 듯한 순간
|
||||||
|
- `Nightmare`: 악몽 -> 검은 꿈
|
||||||
|
- `ToolsOfTheTrade`: 작업 도구 -> 도적의 연장통
|
||||||
|
- `MasterPlanner`: 설계의 대가 -> 작전의 달인
|
||||||
|
- `SerpentForm`: 구렁이의 형상 -> 독사의 몸놀림
|
||||||
|
- `Abrasive`: 연마 -> 거친 숫돌질
|
||||||
|
- `Suppress`: 진압 -> 소리 없는 제압
|
||||||
|
- `WraithForm`: 유령의 형상 -> 유령 같은 몸놀림
|
||||||
|
|
||||||
|
## Assassin
|
||||||
|
|
||||||
|
- `Ricochet`: 도탄 -> 통통 튀는 표창
|
||||||
|
- `BladeDance`: 검무 -> 표창 별무리
|
||||||
|
- `CloakAndDagger`: 망토와 단검 -> 망토 속 별
|
||||||
|
- `Skewer`: 꼬챙이 -> 꿰뚫는 표창
|
||||||
|
- `Flechettes`: 프레췌 -> 표창 셈법
|
||||||
|
- `Pounce`: 덮치기 -> 어둠을 가르는 도약
|
||||||
|
- `Predator`: 천적 -> 표창 끝의 추격
|
||||||
|
- `Pinpoint`: 정밀 사격 -> 한 점 겨냥
|
||||||
|
- `HiddenDaggers`: 숨겨진 표창 -> 숨겨둔 표창
|
||||||
|
- `UpMySleeve`: 비책 -> 소매 속 표창
|
||||||
|
- `InfiniteBlades`: 무한의 검날 -> 끝없는 표창통
|
||||||
|
- `TheHunt`: 사냥 -> 커닝 현상금
|
||||||
|
- `StormOfSteel`: 강철의 폭풍 -> 쇠별 폭풍
|
||||||
|
|
||||||
|
## Hermit
|
||||||
|
|
||||||
|
- `BladeOfInk`: 잉크 칼날 -> 먹빛 표창
|
||||||
|
- `FanOfKnives`: 칼날 부채 -> 사방 표창비
|
||||||
106
docs/superpowers/plans/2026-06-16-charselect-maker-pilot.md
Normal file
106
docs/superpowers/plans/2026-06-16-charselect-maker-pilot.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Phase 2 — 캐릭터 선택 메이커 저작 파일럿 구현 계획
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans 로 태스크 단위 구현.
|
||||||
|
|
||||||
|
**Goal:** charselect를 생성중단→stock화(메이커 편집)하고, 캐릭터 이미지를 컨트롤러가 런타임 경로 주입(ClassPortraits)하도록 바꿔 패턴 (b)를 검증한다.
|
||||||
|
|
||||||
|
**Architecture:** ① 이미지 런타임 주입 추가(ClassPortraits + luaCharsTable + RenderCharacterSelect) → ② charselect 생성 중단(GENERATED_UI_SECTIONS/emit 제거 → 기존 엔티티 stock 보존). 컨트롤러는 경로 구동 유지.
|
||||||
|
|
||||||
|
**Tech Stack:** Node ESM 생성기, MSW Lua. 검증 = **count(동작 검증)** + 메이커 플레이테스트(바이트동일 아님 — codeblock·ui 의도적 변경).
|
||||||
|
|
||||||
|
**의존:** Phase 1b(#71) 위 스택(`feature/charselect-maker-pilot`). #70·#71 머지 후 main 리타겟.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 검증 메모
|
||||||
|
Phase 2는 codeblock·ui를 **의도적으로 변경**(diffcheck-IDENTICAL 아님). 게이트:
|
||||||
|
- `node tools/deck/gen-slaydeck.mjs` 성공(throw 없음).
|
||||||
|
- `node tools/verify/count.mjs cb ClassPortraits 'ImageRUID = self.ClassPortraits'` → 주입 코드 존재.
|
||||||
|
- `node tools/verify/count.mjs ui CharacterSelectHud/WarriorButton/Art` → charselect 엔티티 ui 잔류(stock).
|
||||||
|
- 미러 테스트 무영향(회귀 확인차 실행).
|
||||||
|
- **최종**: 사용자 메이커 플레이테스트.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: `luaCharsTable()` 신설 (lib/data.mjs)
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/lib/data.mjs`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `luaNodeIconsTable`(:78-81) 바로 뒤에 추가:
|
||||||
|
```js
|
||||||
|
function luaCharsTable() {
|
||||||
|
const rows = Object.entries(CHARS.portraits).map(([c, ruid]) => `\t${c} = ${luaStr(ruid)},`).join('\n');
|
||||||
|
return `self.ClassPortraits = {\n${rows}\n}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **Step 2:** `export { ... }`에 `luaCharsTable` 추가.
|
||||||
|
- [ ] **Step 3:** 커밋(아직 미사용 — import 시 검증).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: ClassPortraits 시드 + prop
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/cb/boot.mjs`, `tools/deck/cb/run.mjs`, `tools/deck/gen-slaydeck.mjs`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `cb/boot.mjs`·`cb/run.mjs`의 import에 `luaCharsTable` 추가(`luaNodeIconsTable` 옆, `from '../lib/data.mjs'`).
|
||||||
|
- [ ] **Step 2:** `cb/boot.mjs:8`(`${luaNodeIconsTable()}`) 다음 줄에 `${luaCharsTable()}` 추가. `cb/run.mjs:34` 동일.
|
||||||
|
- [ ] **Step 3:** `gen-slaydeck.mjs:311`(`prop('any', 'NodeIcons'),`) 다음 줄에 `prop('any', 'ClassPortraits'),` 추가.
|
||||||
|
- [ ] **Step 4:** `node tools/deck/gen-slaydeck.mjs` 성공 + `node tools/verify/count.mjs cb ClassPortraits` → ≥2(시드 2회).
|
||||||
|
- [ ] **Step 5:** 산출물 churn 복원(`git checkout --`) — codeblock은 이 시점 변경됨(ClassPortraits 추가)이므로 **복원 안 함**, ui/common만 churn이면 복원. 커밋(소스 + 재생성 codeblock 분리 또는 함께 "산출물 재생성" 명시).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: RenderCharacterSelect 이미지 런타임 주입
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/cb/charselect.mjs:13`(RenderCharacterSelect)
|
||||||
|
|
||||||
|
- [ ] **Step 1:** RenderCharacterSelect 본문 **맨 앞**에 3 Art 주입 추가(Python 치환 — 실탭). classId: Warrior→warrior, Mage→magician, Thief→bandit:
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
(기존 border/status 로직 앞에 prepend. RenderCharacterSelect는 ShowCharacterSelect/SelectClass에서 호출 → 열림·선택 시 멱등 주입.)
|
||||||
|
- [ ] **Step 2:** `node tools/deck/gen-slaydeck.mjs` + `node tools/verify/count.mjs cb 'ImageRUID = self.ClassPortraits'` → 3.
|
||||||
|
- [ ] **Step 3:** 커밋(소스 + 재생성 codeblock).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: charselect 생성 중단 → stock
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/lib/ui-helpers.mjs`, `tools/deck/gen-slaydeck.mjs`; Delete `tools/deck/hud/charselect.mjs`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `lib/ui-helpers.mjs`의 `GENERATED_UI_SECTIONS`·`UI_APPEND_ORDER` 두 배열에서 `'CharacterSelectHud',` 줄 제거(2곳).
|
||||||
|
- [ ] **Step 2:** `gen-slaydeck.mjs`에서 `import { buildCharSelect } from './hud/charselect.mjs';`(:38)와 `emit('CharacterSelectHud', buildCharSelect());`(:229) 제거.
|
||||||
|
- [ ] **Step 3:** `git rm tools/deck/hud/charselect.mjs` (부트스트랩 완료, git 이력에 레퍼런스 잔존).
|
||||||
|
- [ ] **Step 4:** `node tools/deck/gen-slaydeck.mjs` 성공 + `node tools/verify/count.mjs ui CharacterSelectHud/WarriorButton/Art` → **>0**(charselect 엔티티가 stock으로 ui에 잔류). `git status`로 ui 변경 확인(charselect가 생성→stock 전환, 위치 이동 가능 — 정상).
|
||||||
|
- [ ] **Step 5:** 커밋(소스 + 재생성 산출물, 메시지에 "charselect 생성 중단·stock화" 명시).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 마무리 — RULES·경로계약·회귀·PR
|
||||||
|
|
||||||
|
**Files:** Modify `RULES.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** RULES §1에 한 줄: charselect는 **메이커 저작(stock)**이라 생성 안 함 — 컨트롤러가 `ClassPortraits`로 이미지 런타임 주입, 메이커 편집 시 §스펙 경로 유지. (다른 화면은 여전히 hud/cb 생성.)
|
||||||
|
- [ ] **Step 2:** 회귀: `node --test tools/balance/sim-balance.test.mjs` · `node --test tools/map/rogue-map.test.mjs` (exit 0).
|
||||||
|
- [ ] **Step 3:** push → PR(`node tools/git/gitea-pr.mjs create <spec.json>`, base=`feature/cb-modularization`, 한국어).
|
||||||
|
- [ ] **Step 4:** **사용자 메이커 플레이테스트**(워크스페이스 reload 후): 로비→직업선택→3 이미지 컨트롤러 주입 표시→클릭 금색테두리·Status→시작 그 직업→**메이커에서 카드 위치 이동·저장 후 `node gen-slaydeck` 재생성해도 charselect 유지**(stock 비파괴) 확인. 이미지 비표시 시 ClassPortraits 시드/주입 경로 점검.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
- **스펙 커버리지**: ①stock화(T4) ②런타임주입(T1-3: luaCharsTable·시드·prop·RenderCharacterSelect) ③경로구동 유지(무변경) ④경로계약(T5·스펙). 누락 없음.
|
||||||
|
- **플레이스홀더**: luaCharsTable·주입 Lua·제거 라인 구체. 검증=count+playtest(바이트동일 아님 명시).
|
||||||
|
- **타입 일관성**: `self.ClassPortraits`(prop)↔`luaCharsTable`(self.ClassPortraits=)↔RenderCharacterSelect 참조 일치. classId Warrior→warrior/Mage→magician/Thief→bandit 일관.
|
||||||
|
- **순서**: 추가(주입 T1-3) 먼저 → 중단(stock T4). 중단 전엔 생성+주입 공존(무해), 중단 후 stock+주입.
|
||||||
|
- **리스크**: 메이커 경로 변경 시 계약 깨짐(isvalid 가드로 크래시 방지·해당부 미동작). stock 전환 시 ui 위치 이동(렌더 무관).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Phase 2 — 캐릭터 선택 메이커 저작 파일럿 설계
|
||||||
|
|
||||||
|
작성일: 2026-06-16
|
||||||
|
브랜치: `feature/charselect-maker-pilot` (Phase 1b `feature/cb-modularization`/PR #71 위에 스택)
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
하이브리드 UI 로드맵의 **패턴 (b)**(메이커 시각 편집) 검증 파일럿. **캐릭터 선택 화면**을 "생성기 소유 → 메이커 소유"로 이관한다:
|
||||||
|
- **레이아웃**(패널·카드 위치·버튼)은 메이커에서 시각 편집(생성기가 안 덮음).
|
||||||
|
- **동적 내용**(캐릭터 이미지·선택 테두리·상태 텍스트)은 `SlayDeckController`가 런타임에 **경로로 주입** = 컨트롤러 내용주입.
|
||||||
|
|
||||||
|
성공 시 Phase 3에서 상점·전체덱 등으로 확장.
|
||||||
|
|
||||||
|
## 현재 구조 (조사 결과)
|
||||||
|
|
||||||
|
- charselect는 **생성 섹션**: `lib/ui-helpers.mjs`의 `GENERATED_UI_SECTIONS`(:17)·`UI_APPEND_ORDER`(:35)에 `'CharacterSelectHud'` 포함. `hud/charselect.mjs`의 `buildCharSelect()`가 엔티티 emit, `upsertUi`가 `emit('CharacterSelectHud', buildCharSelect())`.
|
||||||
|
- **이미지 = 생성 시 주입**: `hud/charselect.mjs:86` `sprite({ dataId: CHARS.portraits[cls.classId], … })`. 런타임 주입 아님.
|
||||||
|
- **컨트롤러는 경로 구동**: `cb/charselect.mjs`의 `RenderCharacterSelect`(각 `{Warrior,Mage,Thief}Button`의 `SpriteGUIRendererComponent.Color`로 선택 테두리 + Status 텍스트), `SelectClass`, `StartNewGame`. 바인딩은 `cb/state.mjs`의 `BindMenuButtons`(경로로 WarriorButton·BackButton·StartButton 등). 표시 토글은 `ShowState`(경로). **이미지 주입은 없음.**
|
||||||
|
- **런타임 시드 모델**: `self.CardFrames`를 `${luaFramesTable()}`로 OnBeginPlay(cb/boot)·StartRun(cb/run)에서 주입 → `ClassPortraits`의 모델.
|
||||||
|
- `upsertUi` 동작: 기존 `.ui` 로드 → 생성 섹션 엔티티 필터아웃 → emit 섹션 재추가. **생성 섹션에서 빠지면 `isGeneratedUiEntity=false`라 필터 안 됨 → 기존 엔티티 보존(stock)**.
|
||||||
|
|
||||||
|
## 상세 설계
|
||||||
|
|
||||||
|
### ① 생성 중단 → stock화 (generate-once-then-stop)
|
||||||
|
- `lib/ui-helpers.mjs` `GENERATED_UI_SECTIONS`·`UI_APPEND_ORDER`에서 `'CharacterSelectHud'` 제거.
|
||||||
|
- `gen-slaydeck.mjs`(upsertUi)에서 `emit('CharacterSelectHud', buildCharSelect())` + 관련 import 제거. `hud/charselect.mjs`는 **삭제**(부트스트랩 완료 — git 이력에 레퍼런스 남음).
|
||||||
|
- 효과: 현재 `DefaultGroup.ui`의 charselect 엔티티가 그대로 **stock**으로 보존 → 메이커 시각 편집 가능, 재생성에 안 덮임.
|
||||||
|
|
||||||
|
### ② 이미지 런타임 주입 (컨트롤러 내용주입 = 패턴 b 핵심)
|
||||||
|
- `lib/data.mjs`에 `luaCharsTable()` 신설(`data/characters.json`의 `portraits` 시드, `luaFramesTable`/`luaNodeIconsTable` 패턴; `self.ClassPortraits = { warrior="…", magician="…", bandit="…" }`).
|
||||||
|
- 주입 지점: `cb/boot.mjs` OnBeginPlay·`cb/run.mjs` StartRun에 `${luaCharsTable()}`(CardFrames 시드 옆) + prop `ClassPortraits`(any) 선언.
|
||||||
|
- `cb/charselect.mjs` `RenderCharacterSelect`에 이미지 주입 추가: 각 `{key}Button/Art` 엔티티의 `SpriteGUIRendererComponent.ImageRUID`를 `self.ClassPortraits[classId]`로 설정(경로별 isvalid 가드). → 메이커 레이아웃(빈/임의 Art)이어도 컨트롤러가 올바른 이미지 채움. **characters.json 데이터 구동 유지.**
|
||||||
|
|
||||||
|
### ③ 경로 구동 유지 (무변경)
|
||||||
|
- 선택 테두리·Status·버튼 바인딩(`RenderCharacterSelect` 색/텍스트·`SelectClass`·`BindMenuButtons`·`StartNewGame`·`ShowState`)은 이미 경로 기반 → 변경 없음.
|
||||||
|
|
||||||
|
### ④ 엔티티 경로 계약 (docs 명시)
|
||||||
|
메이커 편집 시 아래 경로 유지 필수(컨트롤러가 이 경로로 구동; 누락 시 isvalid 가드로 무시되되 그 부분 동작 안 함):
|
||||||
|
```
|
||||||
|
/ui/DefaultGroup/CharacterSelectHud (루트, ShowState 토글)
|
||||||
|
/OpaqueBackdrop /Title /Status
|
||||||
|
/WarriorButton (+ /Art ← 이미지 주입, /NameBanner, /Name)
|
||||||
|
/ThiefButton (+ /Art, /NameBanner, /Name)
|
||||||
|
/MageButton (+ /Art, /NameBanner, /Name)
|
||||||
|
/StartButton /BackButton
|
||||||
|
```
|
||||||
|
(#67로 DeckButton 제거됨.) classId 매핑: Warrior→warrior, Thief→bandit, Mage→magician.
|
||||||
|
|
||||||
|
## 검증 (동작 — 바이트동일 아님)
|
||||||
|
- 생성기: charselect 제거 후 `node tools/deck/gen-slaydeck.mjs` → **charselect 외 산출물 무영향**(`diffcheck`로 codeblock·common 확인; ui는 charselect 섹션만 stock으로 잔류·다른 섹션 동일). charselect 엔티티가 ui에 존재(`count.mjs`).
|
||||||
|
- 메이커 플레이테스트: 로비→직업선택→**3 이미지가 컨트롤러 주입으로 표시**→클릭 시 금색테두리·Status→시작 시 그 직업으로 런→**메이커에서 카드 위치 이동 후 재생성해도 유지** 확인.
|
||||||
|
|
||||||
|
## 범위 밖
|
||||||
|
- 상점·전체덱 등 다른 화면(Phase 3).
|
||||||
|
- 새 UIGroup(.ui) 분리(경로·ShowState 재작업 큼) — DefaultGroup 내 stock으로 충분.
|
||||||
|
- 게임 규칙·다른 화면 변경.
|
||||||
|
|
||||||
|
## 리스크
|
||||||
|
- stock 전환 시 charselect 엔티티의 `.ui` 내 직렬화 위치 이동 가능 → 렌더는 경로/displayOrder 기반이라 무관하나 플레이테스트로 확인.
|
||||||
|
- 메이커가 경로를 바꾸면 계약 깨짐 → 경로 표로 가드. isvalid 가드로 크래시는 방지.
|
||||||
|
- 의존: Phase 1b(cb/charselect·boot·run) 위 스택. #70·#71 머지 후 main 리타겟.
|
||||||
|
|
||||||
|
## 변경 파일 요약
|
||||||
|
| 파일 | 변경 |
|
||||||
|
|---|---|
|
||||||
|
| `tools/deck/lib/ui-helpers.mjs` | `GENERATED_UI_SECTIONS`·`UI_APPEND_ORDER`에서 CharacterSelectHud 제거 |
|
||||||
|
| `tools/deck/gen-slaydeck.mjs` | upsertUi에서 charselect emit·import 제거 |
|
||||||
|
| `tools/deck/hud/charselect.mjs` | **삭제** |
|
||||||
|
| `tools/deck/lib/data.mjs` | `luaCharsTable()` 신설 |
|
||||||
|
| `tools/deck/cb/boot.mjs`·`cb/run.mjs` | `${luaCharsTable()}` 시드 + ClassPortraits prop |
|
||||||
|
| `tools/deck/cb/charselect.mjs` | `RenderCharacterSelect`에 Art ImageRUID 주입 |
|
||||||
|
| `docs/...charselect 경로 계약` | 경로 표(이 스펙 §④) |
|
||||||
|
| `ui/DefaultGroup.ui`·codeblock | 재생성(charselect는 stock 잔류) |
|
||||||
14
docs/x-cost.md
Normal file
14
docs/x-cost.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# X 코스트 카드
|
||||||
|
|
||||||
|
`useAllEnergy`는 카드가 사용될 때 남은 에너지를 전부 쓰는 공용 필드입니다.
|
||||||
|
|
||||||
|
연동 필드:
|
||||||
|
|
||||||
|
- `xDamagePerEnergy`: 에너지 1당 피해량
|
||||||
|
- `xWeakPerEnergy`: 에너지 1당 약화량
|
||||||
|
|
||||||
|
적용 예시:
|
||||||
|
|
||||||
|
- `Skewer`: 남은 에너지 전부를 써서 `8 * energy` 피해
|
||||||
|
- `Malaise`: 남은 에너지 전부를 써서 약화 부여
|
||||||
|
|
||||||
7
excel_to_cards.bat
Normal file
7
excel_to_cards.bat
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
chcp 65001 >nul
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\cards\cards_excel.ps1" import
|
||||||
|
echo.
|
||||||
|
echo Press any key to close this window.
|
||||||
|
pause >nul
|
||||||
12477
map/lobby.map
12477
map/lobby.map
File diff suppressed because it is too large
Load Diff
1712
map/map01.map
1712
map/map01.map
File diff suppressed because it is too large
Load Diff
246
tools/balance/card-efficiency.mjs
Normal file
246
tools/balance/card-efficiency.mjs
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import {
|
||||||
|
PLAYER_HP,
|
||||||
|
loadData,
|
||||||
|
mulberry32,
|
||||||
|
simulateCombat,
|
||||||
|
} from './sim-balance.mjs';
|
||||||
|
|
||||||
|
const ROGUE_CLASSES = new Set(['rogue', 'thief', 'thiefmaster', 'assassin', 'hermit']);
|
||||||
|
|
||||||
|
const CONTEXT_DECKS = {
|
||||||
|
rogue: [
|
||||||
|
'SilentStrike', 'SilentStrike', 'SilentStrike', 'SilentStrike',
|
||||||
|
'SilentDefend', 'SilentDefend', 'SilentDefend', 'SilentDefend',
|
||||||
|
'Neutralize', 'Survivor', 'DoubleStab', 'Backflip',
|
||||||
|
],
|
||||||
|
thief: [
|
||||||
|
'SilentStrike', 'SilentStrike', 'SilentStrike',
|
||||||
|
'SilentDefend', 'SilentDefend', 'SilentDefend',
|
||||||
|
'Neutralize', 'Survivor', 'SavageBlow', 'DaggerAcceleration',
|
||||||
|
'DeadlyPoison', 'Acrobatics',
|
||||||
|
],
|
||||||
|
thiefmaster: [
|
||||||
|
'SilentStrike', 'SilentStrike',
|
||||||
|
'SilentDefend', 'SilentDefend',
|
||||||
|
'Survivor', 'SavageBlow', 'DaggerAcceleration', 'DeadlyPoison',
|
||||||
|
'Acrobatics', 'EdgeCarnival', 'PickPocket', 'Venom',
|
||||||
|
],
|
||||||
|
assassin: [
|
||||||
|
'SilentStrike', 'SilentStrike', 'SilentStrike',
|
||||||
|
'SilentDefend', 'SilentDefend', 'SilentDefend',
|
||||||
|
'Neutralize', 'Survivor', 'LeadingStrike', 'BladeDance',
|
||||||
|
'JavelinAcceleration', 'JavelinMastery',
|
||||||
|
],
|
||||||
|
hermit: [
|
||||||
|
'SilentStrike', 'SilentStrike',
|
||||||
|
'SilentDefend', 'SilentDefend',
|
||||||
|
'Survivor', 'LeadingStrike', 'BladeDance', 'JavelinAcceleration',
|
||||||
|
'JavelinMastery', 'TripleThrow', 'SpiritJavelin', 'SkilledJavelin',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENCOUNTER_SCALE = {
|
||||||
|
rogue: { hp: 1.9, attack: 1.5 },
|
||||||
|
thief: { hp: 2.2, attack: 1.6 },
|
||||||
|
assassin: { hp: 2.25, attack: 1.65 },
|
||||||
|
thiefmaster: { hp: 2.4, attack: 1.5 },
|
||||||
|
hermit: { hp: 2.6, attack: 1.65 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const median = (values) => {
|
||||||
|
if (values.length === 0) return 0;
|
||||||
|
const sorted = values.slice().sort((a, b) => a - b);
|
||||||
|
const middle = Math.floor(sorted.length / 2);
|
||||||
|
return sorted.length % 2 === 1
|
||||||
|
? sorted[middle]
|
||||||
|
: (sorted[middle - 1] + sorted[middle]) / 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateContextDecks(cards) {
|
||||||
|
for (const [classId, deck] of Object.entries(CONTEXT_DECKS)) {
|
||||||
|
for (const cardId of deck) {
|
||||||
|
if (!cards[cardId]) throw new Error(`${classId} 효율 기준 덱에 없는 카드: ${cardId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function outcomeScore(result) {
|
||||||
|
if (result.draw) return -60;
|
||||||
|
if (!result.win) return -100 - result.turns;
|
||||||
|
return 100 + (result.playerHpRemaining / PLAYER_HP) * 30 - result.turns * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaledEncounter(data, classId) {
|
||||||
|
const scale = ENCOUNTER_SCALE[classId];
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
monsters: data.monsters.map((monster) => ({
|
||||||
|
...monster,
|
||||||
|
maxHp: Math.round(monster.maxHp * scale.hp),
|
||||||
|
intents: monster.intents.map((intent) => intent.kind === 'Attack'
|
||||||
|
? { ...intent, value: Math.round(intent.value * scale.attack) }
|
||||||
|
: { ...intent }),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function simulateDeck(baseData, deck, runs, seed, trackedCardId = null) {
|
||||||
|
let wins = 0;
|
||||||
|
let totalTurns = 0;
|
||||||
|
let totalHp = 0;
|
||||||
|
let totalScore = 0;
|
||||||
|
let totalPlays = 0;
|
||||||
|
for (let i = 0; i < runs; i++) {
|
||||||
|
const stats = {};
|
||||||
|
const rng = mulberry32((seed + Math.imul(i + 1, 0x9e3779b1)) >>> 0);
|
||||||
|
const result = simulateCombat({ ...baseData, starterDeck: deck }, rng, stats);
|
||||||
|
if (result.win) {
|
||||||
|
wins++;
|
||||||
|
totalHp += result.playerHpRemaining;
|
||||||
|
}
|
||||||
|
totalTurns += result.turns;
|
||||||
|
totalScore += outcomeScore(result);
|
||||||
|
if (trackedCardId && stats[trackedCardId]) totalPlays += stats[trackedCardId].plays;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
winRate: wins / runs,
|
||||||
|
avgTurns: totalTurns / runs,
|
||||||
|
avgHpOnWin: wins > 0 ? totalHp / wins : 0,
|
||||||
|
score: totalScore / runs,
|
||||||
|
avgPlays: totalPlays / runs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function replacementIndex(deck, cards, candidate) {
|
||||||
|
const preferredKind = candidate.kind === 'Attack' ? 'Attack' : 'Skill';
|
||||||
|
const preferred = deck.findIndex((id) => cards[id]?.kind === preferredKind);
|
||||||
|
if (preferred >= 0) return preferred;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function structuralRisks(card) {
|
||||||
|
const risks = [];
|
||||||
|
const cost = card.cost || 0;
|
||||||
|
const exhaust = card.exhaust === true;
|
||||||
|
const permanentDex = Math.max(0, (card.dex || 0) - (card.endTurnDexLoss || 0));
|
||||||
|
const permanentStats = (card.strength || 0) + permanentDex + (card.thorns || 0);
|
||||||
|
const generatedCards = (card.addShiv || 0) + (card.addShivPerDiscard ? 1 : 0);
|
||||||
|
|
||||||
|
if (cost === 0 && !exhaust && (card.gainEnergy || 0) > 0) {
|
||||||
|
risks.push('0코스트 비소멸 카드가 에너지를 생성');
|
||||||
|
}
|
||||||
|
if (cost === 0 && !exhaust && (card.draw || 0) >= 2 && generatedCards > 0) {
|
||||||
|
risks.push('0코스트 비소멸 카드가 2장 이상 드로우하면서 카드를 생성');
|
||||||
|
}
|
||||||
|
if (card.kind !== 'Power' && !exhaust && permanentStats > 0) {
|
||||||
|
risks.push('재사용 가능한 카드가 영구 능력치를 누적');
|
||||||
|
}
|
||||||
|
if (card.kind === 'Power' && (card.attackDamageVsWeakMultiplier || 0) >= 2 && cost <= 1) {
|
||||||
|
risks.push('저비용 지속 효과가 공격 피해를 2배 이상 증폭');
|
||||||
|
}
|
||||||
|
if ((card.poisonApplicationBurstEvery || 0) > 0) {
|
||||||
|
const burstPerApplication = (card.poisonApplicationBurstDamage || 0) / card.poisonApplicationBurstEvery;
|
||||||
|
if (burstPerApplication > 3 && cost <= 1) {
|
||||||
|
risks.push('저비용 독 누적 폭발 피해가 부여 1회당 3을 초과');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cost === 0 && !exhaust && (card.block || 0) + (card.nextTurnBlock || 0) >= 8) {
|
||||||
|
risks.push('0코스트 비소멸 카드의 현재·다음 턴 방어 합계가 8 이상');
|
||||||
|
}
|
||||||
|
if (cost === 0 && !exhaust && (card.blockPerDamageDealtThisTurn || 0) >= 1) {
|
||||||
|
risks.push('0코스트 비소멸 카드가 이번 턴 누적 피해 전부를 방어로 전환');
|
||||||
|
}
|
||||||
|
if (!exhaust && (card.gainEnergy || 0) > 0 && (card.gainEnergy || 0) >= cost && (card.draw || 0) > 0 && generatedCards > 0) {
|
||||||
|
risks.push('에너지 손실 없이 드로우와 카드 생성을 동시에 수행');
|
||||||
|
}
|
||||||
|
if (!exhaust && (card.skillCostReductionThisTurn || 0) > 0 && (card.gainEnergy || 0) > 0 && (card.gainEnergy || 0) >= cost && (card.draw || 0) > 0) {
|
||||||
|
risks.push('에너지 손실 없이 드로우하고 이번 턴 스킬 비용까지 감소');
|
||||||
|
}
|
||||||
|
return risks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditCardEfficiency({ runs = 300, seed = 20260701 } = {}) {
|
||||||
|
const data = loadData();
|
||||||
|
const cards = data.cards;
|
||||||
|
validateContextDecks(cards);
|
||||||
|
|
||||||
|
const baselines = {};
|
||||||
|
for (const [classId, deck] of Object.entries(CONTEXT_DECKS)) {
|
||||||
|
baselines[classId] = simulateDeck(scaledEncounter(data, classId), deck, runs, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const [id, card] of Object.entries(cards)) {
|
||||||
|
if (!ROGUE_CLASSES.has(card.class)) continue;
|
||||||
|
const deck = CONTEXT_DECKS[card.class].slice();
|
||||||
|
deck[replacementIndex(deck, cards, card)] = id;
|
||||||
|
const result = simulateDeck(scaledEncounter(data, card.class), deck, runs, seed, id);
|
||||||
|
rows.push({
|
||||||
|
id,
|
||||||
|
name: card.name,
|
||||||
|
classId: card.class,
|
||||||
|
rarity: card.rarity,
|
||||||
|
kind: card.kind,
|
||||||
|
cost: card.cost || 0,
|
||||||
|
delta: result.score - baselines[card.class].score,
|
||||||
|
...result,
|
||||||
|
risks: structuralRisks(card),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const peers = rows.filter((other) => other.classId === row.classId && other.rarity === row.rarity);
|
||||||
|
row.peerMedianDelta = median(peers.map((peer) => peer.delta));
|
||||||
|
row.peerGap = row.delta - row.peerMedianDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { runs, seed, baselines, rows };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent(value) {
|
||||||
|
return `${(value * 100).toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatEfficiencyReport(report) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`도적 카드 효율 검증: 카드 ${report.rows.length}장, 카드당 ${report.runs}회`);
|
||||||
|
lines.push('기준 덱:');
|
||||||
|
for (const [classId, baseline] of Object.entries(report.baselines)) {
|
||||||
|
lines.push(` ${classId}: 승률 ${formatPercent(baseline.winRate)}, 평균 ${baseline.avgTurns.toFixed(2)}턴, 승리 HP ${baseline.avgHpOnWin.toFixed(1)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const risky = report.rows.filter((row) => row.risks.length > 0);
|
||||||
|
lines.push('');
|
||||||
|
lines.push(`구조적 위험 ${risky.length}장:`);
|
||||||
|
for (const row of risky) {
|
||||||
|
lines.push(` ${row.name}(${row.id}, ${row.classId}): ${row.risks.join(' / ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
lines.push('동급 대비 효율 상위:');
|
||||||
|
for (const row of report.rows.slice().sort((a, b) => b.peerGap - a.peerGap).slice(0, 10)) {
|
||||||
|
lines.push(` ${row.name}(${row.id}): 중앙값 대비 +${row.peerGap.toFixed(1)}, 승률 ${formatPercent(row.winRate)}, 평균 사용 ${row.avgPlays.toFixed(2)}회`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
lines.push('동급 대비 효율 하위:');
|
||||||
|
for (const row of report.rows.slice().sort((a, b) => a.peerGap - b.peerGap).slice(0, 10)) {
|
||||||
|
lines.push(` ${row.name}(${row.id}): 중앙값 대비 ${row.peerGap.toFixed(1)}, 승률 ${formatPercent(row.winRate)}, 평균 사용 ${row.avgPlays.toFixed(2)}회`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
let runs = 300;
|
||||||
|
let seed = 20260701;
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--runs') runs = Number.parseInt(args[++i], 10);
|
||||||
|
else if (args[i] === '--seed') seed = Number.parseInt(args[++i], 10);
|
||||||
|
}
|
||||||
|
const report = auditCardEfficiency({ runs, seed });
|
||||||
|
console.log(formatEfficiencyReport(report));
|
||||||
|
if (report.rows.some((row) => row.risks.length > 0)) process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && process.argv[1].endsWith('card-efficiency.mjs')) main();
|
||||||
30
tools/balance/card-efficiency.test.mjs
Normal file
30
tools/balance/card-efficiency.test.mjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { structuralRisks } from './card-efficiency.mjs';
|
||||||
|
|
||||||
|
test('0코스트 에너지 생성 카드를 위험으로 분류', () => {
|
||||||
|
const risks = structuralRisks({ cost: 0, kind: 'Skill', gainEnergy: 1 });
|
||||||
|
assert.ok(risks.some((risk) => risk.includes('에너지를 생성')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('재사용 가능한 영구 능력치 스킬을 위험으로 분류', () => {
|
||||||
|
const risks = structuralRisks({ cost: 1, kind: 'Skill', strength: 1, dex: 1 });
|
||||||
|
assert.ok(risks.some((risk) => risk.includes('영구 능력치')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('소멸하거나 파워인 능력치 카드는 허용', () => {
|
||||||
|
assert.deepEqual(structuralRisks({ cost: 1, kind: 'Skill', strength: 1, exhaust: true }), []);
|
||||||
|
assert.deepEqual(structuralRisks({ cost: 1, kind: 'Power', dex: 1 }), []);
|
||||||
|
assert.deepEqual(structuralRisks({ cost: 0, kind: 'Skill', dex: 2, endTurnDexLoss: 2 }), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('저비용 2배 피해 증폭을 위험으로 분류', () => {
|
||||||
|
const risks = structuralRisks({ cost: 1, kind: 'Power', attackDamageVsWeakMultiplier: 2 });
|
||||||
|
assert.ok(risks.some((risk) => risk.includes('2배')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('0코스트 누적 피해 전체 방어 전환을 위험으로 분류', () => {
|
||||||
|
const risks = structuralRisks({ cost: 0, kind: 'Skill', blockPerDamageDealtThisTurn: 1 });
|
||||||
|
assert.ok(risks.some((risk) => risk.includes('누적 피해')));
|
||||||
|
assert.deepEqual(structuralRisks({ cost: 0, kind: 'Skill', blockPerDamageDealtThisTurn: 0.5 }), []);
|
||||||
|
});
|
||||||
314
tools/balance/rogue-campaign.mjs
Normal file
314
tools/balance/rogue-campaign.mjs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { mulberry32, rarityForRoll, simulateCombat } from './sim-balance.mjs';
|
||||||
|
import { ACT_DIFFICULTY_MULTIPLIERS } from '../deck/lib/codeblock.mjs';
|
||||||
|
|
||||||
|
const cardsData = JSON.parse(readFileSync('data/cards.json', 'utf8'));
|
||||||
|
const enemiesData = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
|
||||||
|
const relicsData = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
||||||
|
|
||||||
|
const PLAYER_MAX_HP = 70;
|
||||||
|
const REST_HEAL = 30;
|
||||||
|
const SECTION_COUNT = 5;
|
||||||
|
const NORMAL_FIGHTS = 4;
|
||||||
|
export const DEFAULT_SECTION_MULTIPLIERS = ACT_DIFFICULTY_MULTIPLIERS;
|
||||||
|
const COMBAT_POOL = ['orange_mushroom', 'green_mushroom', 'pig', 'blue_mushroom', 'red_snail', 'stump'];
|
||||||
|
const ELITE_POOL = ['mushmom', 'modified_snail'];
|
||||||
|
const BOSS_POOL = ['king_slime', 'slime_boss'];
|
||||||
|
|
||||||
|
const JOBS = {
|
||||||
|
thief: { tier2: 'thief', tier3: 'thiefmaster', tier2Starter: 'DaggerAcceleration', tier3Starter: 'Venom' },
|
||||||
|
assassin: { tier2: 'assassin', tier3: 'hermit', tier2Starter: 'JavelinAcceleration', tier3Starter: 'SpiritJavelin' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const LINEAGES = {
|
||||||
|
rogue: ['rogue'],
|
||||||
|
thief: ['rogue', 'thief'],
|
||||||
|
thiefmaster: ['rogue', 'thief', 'thiefmaster'],
|
||||||
|
assassin: ['rogue', 'assassin'],
|
||||||
|
hermit: ['rogue', 'assassin', 'hermit'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const pick = (rng, values) => values[Math.floor(rng() * values.length)];
|
||||||
|
|
||||||
|
export function campaignJobAtSection(branch, section) {
|
||||||
|
if (section <= 1) return 'rogue';
|
||||||
|
if (section === 2) return JOBS[branch].tier2;
|
||||||
|
return JOBS[branch].tier3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playableClassesForJob(job) {
|
||||||
|
return LINEAGES[job] || [job];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scaleEnemy(enemy, section, rng = () => 0, scaleStep = null) {
|
||||||
|
const multiplier = scaleStep == null
|
||||||
|
? (DEFAULT_SECTION_MULTIPLIERS[section - 1] || DEFAULT_SECTION_MULTIPLIERS.at(-1))
|
||||||
|
: 1 + (section - 1) * scaleStep;
|
||||||
|
const offset = enemy.intents.length > 0 ? Math.floor(rng() * enemy.intents.length) : 0;
|
||||||
|
const rotatedIntents = enemy.intents.map((_, index) => enemy.intents[(index + offset) % enemy.intents.length]);
|
||||||
|
return {
|
||||||
|
...enemy,
|
||||||
|
maxHp: Math.floor(enemy.maxHp * multiplier),
|
||||||
|
intents: rotatedIntents.map((intent) => ({
|
||||||
|
...intent,
|
||||||
|
value: intent.kind === 'Debuff' || intent.value == null
|
||||||
|
? intent.value
|
||||||
|
: Math.floor(intent.value * multiplier),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEncounter(kind, section, rng, scaleStep) {
|
||||||
|
const ids = [];
|
||||||
|
if (kind === 'normal') {
|
||||||
|
const count = 1 + Math.floor(rng() * 3);
|
||||||
|
for (let i = 0; i < count; i++) ids.push(pick(rng, COMBAT_POOL));
|
||||||
|
} else if (kind === 'elite') {
|
||||||
|
ids.push(pick(rng, ELITE_POOL));
|
||||||
|
const extra = Math.floor(rng() * 3);
|
||||||
|
for (let i = 0; i < extra; i++) ids.push(pick(rng, COMBAT_POOL));
|
||||||
|
} else {
|
||||||
|
ids.push(pick(rng, BOSS_POOL));
|
||||||
|
}
|
||||||
|
return ids.map((id) => scaleEnemy(enemiesData.enemies[id], section, rng, scaleStep));
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseCardValue(card) {
|
||||||
|
const hits = card.hits || 1;
|
||||||
|
const targets = card.aoe ? 1.7 : 1;
|
||||||
|
let value = (card.damage || 0) * hits * targets;
|
||||||
|
value += (card.block || 0) + (card.nextTurnBlock || 0) * 0.7;
|
||||||
|
value += (card.poison || 0) * (card.poisonHits || 1) * (card.affectsAllEnemies ? 2 : 1) * 1.5;
|
||||||
|
value += (card.draw || 0) * 4 + (card.gainEnergy || 0) * 5;
|
||||||
|
value += (card.addShiv || 0) * 4;
|
||||||
|
value += (card.strength || 0) * 6 + (card.dex || 0) * 5;
|
||||||
|
value += (card.weak || 0) * 3 + (card.vuln || 0) * 4;
|
||||||
|
value += (card.intangible || 0) * 12;
|
||||||
|
value += (card.turnStartShiv || 0) * 8 + (card.shivDamageBonus || 0) * 4;
|
||||||
|
value += (card.cardPlayedBlock || 0) * 8 + (card.attackPoison || 0) * 8;
|
||||||
|
value += (card.powerEffect ? 7 : 0) + (card.retain ? 2 : 0) + (card.sly ? 3 : 0);
|
||||||
|
value += (card.damagePerDiscardedThisTurn || 0) * 2;
|
||||||
|
value += (card.damagePerAttackPlayedThisTurn || 0) * 2;
|
||||||
|
value += (card.firstShivDamageBonus || 0) * 2;
|
||||||
|
value -= (card.cost || 0) * 5;
|
||||||
|
if (card.exhaust) value -= 2;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function branchCardValue(card, branch, deck, id) {
|
||||||
|
let value = baseCardValue(card);
|
||||||
|
if (branch === 'thief') {
|
||||||
|
value += (card.poison || 0) * 1.5 + (card.attackPoison || 0) * 8;
|
||||||
|
value += card.sly ? 5 : 0;
|
||||||
|
value += (card.discard || 0) * 2 + (card.drawPerDiscarded || 0) * 4;
|
||||||
|
value += (card.poisonApplicationBurstDamage || 0) * 1.5;
|
||||||
|
} else {
|
||||||
|
value += (card.addShiv || 0) * 3 + (card.turnStartShiv || 0) * 8;
|
||||||
|
value += (card.shivDamageBonus || 0) * 6 + (card.firstShivDamageBonus || 0) * 3;
|
||||||
|
value += card.shivAoe ? 12 : 0;
|
||||||
|
value += card.shivRetain ? 5 : 0;
|
||||||
|
}
|
||||||
|
const copies = deck.filter((cardId) => cardId === id).length;
|
||||||
|
value -= copies * (card.kind === 'Power' ? 10 : 3);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewardPool(job) {
|
||||||
|
const classes = new Set(playableClassesForJob(job));
|
||||||
|
return Object.entries(cardsData.cards)
|
||||||
|
.filter(([, card]) => classes.has(card.class) && card.token !== true && card.unplayable !== true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function offerReward(job, branch, deck, rng, minimumValue) {
|
||||||
|
const pool = rewardPool(job);
|
||||||
|
const choices = [];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const rarity = rarityForRoll(1 + Math.floor(rng() * 100));
|
||||||
|
const bucket = pool.filter(([, card]) => card.rarity === rarity);
|
||||||
|
choices.push(pick(rng, bucket.length > 0 ? bucket : pool));
|
||||||
|
}
|
||||||
|
choices.sort((a, b) => branchCardValue(b[1], branch, deck, b[0]) - branchCardValue(a[1], branch, deck, a[0]));
|
||||||
|
const [id, card] = choices[0];
|
||||||
|
if (branchCardValue(card, branch, deck, id) >= minimumValue) deck.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function relicModifiers(state) {
|
||||||
|
const result = {
|
||||||
|
playerStartBlock: 0,
|
||||||
|
playerStrength: 0,
|
||||||
|
playerThorns: 0,
|
||||||
|
energyBonus: 0,
|
||||||
|
openingDrawBonus: 0,
|
||||||
|
healOnAttack: 0,
|
||||||
|
};
|
||||||
|
for (const id of state.relics) {
|
||||||
|
const relic = relicsData.relics[id];
|
||||||
|
if (!relic) continue;
|
||||||
|
if (relic.hook === 'combatStart' && relic.effect === 'block') result.playerStartBlock += relic.value;
|
||||||
|
else if (relic.hook === 'combatStart' && relic.effect === 'strength') result.playerStrength += relic.value;
|
||||||
|
else if (relic.hook === 'turnStart' && relic.effect === 'energy') result.energyBonus += relic.value;
|
||||||
|
else if (relic.hook === 'combatStart' && relic.effect === 'draw') result.openingDrawBonus += relic.value;
|
||||||
|
else if (relic.effect === 'thorns') result.playerThorns += relic.value;
|
||||||
|
else if (relic.effect === 'healOnAttack') result.healOnAttack += relic.value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function healFromRelics(state, hook) {
|
||||||
|
for (const id of state.relics) {
|
||||||
|
const relic = relicsData.relics[id];
|
||||||
|
if (!relic || relic.hook !== hook) continue;
|
||||||
|
if (relic.effect === 'heal') state.hp = Math.min(state.maxHp, state.hp + relic.value);
|
||||||
|
else if (relic.effect === 'healOnWin') state.hp = Math.min(state.maxHp, state.hp + relic.value);
|
||||||
|
else if (relic.effect === 'healIfLow' && state.hp <= state.maxHp * 0.5) state.hp = Math.min(state.maxHp, state.hp + relic.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function acquireRelic(state, rng) {
|
||||||
|
const available = relicsData.relicPool.filter((id) => !state.relics.includes(id));
|
||||||
|
if (available.length === 0) return;
|
||||||
|
const id = pick(rng, available);
|
||||||
|
state.relics.push(id);
|
||||||
|
const relic = relicsData.relics[id];
|
||||||
|
if (relic?.effect === 'maxHp') {
|
||||||
|
state.maxHp += relic.value;
|
||||||
|
state.hp += relic.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fight(state, branch, kind, section, rng, options) {
|
||||||
|
const monsters = buildEncounter(kind, section, rng, options.scaleStep);
|
||||||
|
healFromRelics(state, 'combatStart');
|
||||||
|
const result = simulateCombat({
|
||||||
|
cards: cardsData.cards,
|
||||||
|
starterDeck: state.deck,
|
||||||
|
monsters,
|
||||||
|
playerHp: state.hp,
|
||||||
|
playerMaxHp: state.maxHp,
|
||||||
|
smartPlayer: true,
|
||||||
|
...relicModifiers(state),
|
||||||
|
}, rng);
|
||||||
|
state.hp = result.playerHpRemaining;
|
||||||
|
state.turns += result.turns;
|
||||||
|
if (!result.win) return false;
|
||||||
|
healFromRelics(state, 'combatEnd');
|
||||||
|
if (kind !== 'boss') offerReward(state.job, branch, state.deck, rng, options.minimumRewardValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function simulateCampaign(branch, rng, {
|
||||||
|
restHeal = REST_HEAL,
|
||||||
|
sectionHeal = 0,
|
||||||
|
scaleStep = null,
|
||||||
|
minimumRewardValue = 10,
|
||||||
|
} = {}) {
|
||||||
|
if (!JOBS[branch]) throw new Error(`지원하지 않는 도적 분기: ${branch}`);
|
||||||
|
const state = {
|
||||||
|
hp: PLAYER_MAX_HP,
|
||||||
|
maxHp: PLAYER_MAX_HP,
|
||||||
|
deck: cardsData.starterDecks.rogue.slice(),
|
||||||
|
job: 'rogue',
|
||||||
|
turns: 0,
|
||||||
|
sectionCleared: 0,
|
||||||
|
diedAt: '',
|
||||||
|
hpAfterSections: [],
|
||||||
|
relics: [relicsData.startingRelic],
|
||||||
|
};
|
||||||
|
const options = { scaleStep, minimumRewardValue };
|
||||||
|
|
||||||
|
for (let section = 1; section <= SECTION_COUNT; section++) {
|
||||||
|
state.job = campaignJobAtSection(branch, section);
|
||||||
|
for (let fightIndex = 1; fightIndex <= NORMAL_FIGHTS; fightIndex++) {
|
||||||
|
if (!fight(state, branch, 'normal', section, rng, options)) {
|
||||||
|
state.diedAt = `${section}-normal`;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.hp = Math.min(state.maxHp, state.hp + restHeal);
|
||||||
|
if (!fight(state, branch, 'elite', section, rng, options)) {
|
||||||
|
state.diedAt = `${section}-elite`;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
acquireRelic(state, rng);
|
||||||
|
if (!fight(state, branch, 'boss', section, rng, options)) {
|
||||||
|
state.diedAt = `${section}-boss`;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
state.sectionCleared = section;
|
||||||
|
state.hpAfterSections.push(state.hp);
|
||||||
|
if (section === 1) state.deck.push(JOBS[branch].tier2Starter);
|
||||||
|
if (section === 2) state.deck.push(JOBS[branch].tier3Starter);
|
||||||
|
if (section >= 3) acquireRelic(state, rng);
|
||||||
|
if (section < SECTION_COUNT) state.hp = Math.min(state.maxHp, state.hp + sectionHeal);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runCampaignBatch(branch, runs = 1000, seed = 20260701, options = {}) {
|
||||||
|
const sectionReached = Array(SECTION_COUNT).fill(0);
|
||||||
|
const sectionClears = Array(SECTION_COUNT).fill(0);
|
||||||
|
const deaths = {};
|
||||||
|
let fullClears = 0;
|
||||||
|
let totalDeckSize = 0;
|
||||||
|
let totalFinalHp = 0;
|
||||||
|
let totalTurns = 0;
|
||||||
|
for (let i = 0; i < runs; i++) {
|
||||||
|
const rng = mulberry32((seed + Math.imul(i + 1, 0x9e3779b1)) >>> 0);
|
||||||
|
const result = simulateCampaign(branch, rng, options);
|
||||||
|
for (let section = 0; section < SECTION_COUNT; section++) {
|
||||||
|
if (result.sectionCleared >= section) sectionReached[section]++;
|
||||||
|
if (result.sectionCleared >= section + 1) sectionClears[section]++;
|
||||||
|
}
|
||||||
|
if (result.sectionCleared === SECTION_COUNT) {
|
||||||
|
fullClears++;
|
||||||
|
totalFinalHp += result.hp;
|
||||||
|
}
|
||||||
|
if (result.diedAt) deaths[result.diedAt] = (deaths[result.diedAt] || 0) + 1;
|
||||||
|
totalDeckSize += result.deck.length;
|
||||||
|
totalTurns += result.turns;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
branch,
|
||||||
|
runs,
|
||||||
|
fullClearRate: fullClears / runs,
|
||||||
|
avgFinalHp: fullClears > 0 ? totalFinalHp / fullClears : 0,
|
||||||
|
avgDeckSize: totalDeckSize / runs,
|
||||||
|
avgTurns: totalTurns / runs,
|
||||||
|
sectionConditionalClearRates: sectionClears.map((clears, index) => sectionReached[index] > 0 ? clears / sectionReached[index] : 0),
|
||||||
|
sectionReachRates: sectionReached.map((reached) => reached / runs),
|
||||||
|
deaths,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCampaignReport(result) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`${result.branch} 캠페인 ${result.runs}회`);
|
||||||
|
lines.push(` 전체 클리어 ${(result.fullClearRate * 100).toFixed(1)}%, 클리어 HP ${result.avgFinalHp.toFixed(1)}, 평균 덱 ${result.avgDeckSize.toFixed(1)}장`);
|
||||||
|
result.sectionConditionalClearRates.forEach((rate, index) => {
|
||||||
|
lines.push(` 섹션 ${index + 1}: 도달 ${(result.sectionReachRates[index] * 100).toFixed(1)}%, 도달자 클리어 ${(rate * 100).toFixed(1)}%`);
|
||||||
|
});
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
let runs = 1000;
|
||||||
|
let seed = 20260701;
|
||||||
|
let restHeal = REST_HEAL;
|
||||||
|
let sectionHeal = 0;
|
||||||
|
let scaleStep = null;
|
||||||
|
let minimumRewardValue = 10;
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--runs') runs = Number.parseInt(args[++i], 10);
|
||||||
|
else if (args[i] === '--seed') seed = Number.parseInt(args[++i], 10);
|
||||||
|
else if (args[i] === '--rest-heal') restHeal = Number.parseInt(args[++i], 10);
|
||||||
|
else if (args[i] === '--section-heal') sectionHeal = Number.parseInt(args[++i], 10);
|
||||||
|
else if (args[i] === '--scale-step') scaleStep = Number.parseFloat(args[++i]);
|
||||||
|
else if (args[i] === '--reward-min') minimumRewardValue = Number.parseFloat(args[++i]);
|
||||||
|
}
|
||||||
|
for (const branch of ['thief', 'assassin']) {
|
||||||
|
console.log(formatCampaignReport(runCampaignBatch(branch, runs, seed, { restHeal, sectionHeal, scaleStep, minimumRewardValue })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && process.argv[1].endsWith('rogue-campaign.mjs')) main();
|
||||||
28
tools/balance/rogue-campaign.test.mjs
Normal file
28
tools/balance/rogue-campaign.test.mjs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
campaignJobAtSection,
|
||||||
|
playableClassesForJob,
|
||||||
|
scaleEnemy,
|
||||||
|
} from './rogue-campaign.mjs';
|
||||||
|
|
||||||
|
test('도적 전직 시점: 1섹션 Rogue, 2섹션 2차, 3섹션부터 3차', () => {
|
||||||
|
assert.equal(campaignJobAtSection('thief', 1), 'rogue');
|
||||||
|
assert.equal(campaignJobAtSection('thief', 2), 'thief');
|
||||||
|
assert.equal(campaignJobAtSection('thief', 3), 'thiefmaster');
|
||||||
|
assert.equal(campaignJobAtSection('assassin', 2), 'assassin');
|
||||||
|
assert.equal(campaignJobAtSection('assassin', 5), 'hermit');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('3차 직업은 자기 계보 카드만 사용', () => {
|
||||||
|
assert.deepEqual(playableClassesForJob('thiefmaster'), ['rogue', 'thief', 'thiefmaster']);
|
||||||
|
assert.deepEqual(playableClassesForJob('hermit'), ['rogue', 'assassin', 'hermit']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('섹션 난이도는 3차 이후 더 빠르게 증가', () => {
|
||||||
|
const enemy = { maxHp: 100, intents: [{ kind: 'Attack', value: 10 }, { kind: 'Debuff', value: 2 }] };
|
||||||
|
const scaled = scaleEnemy(enemy, 3, () => 0);
|
||||||
|
assert.equal(scaled.maxHp, 114);
|
||||||
|
assert.equal(scaled.intents[0].value, 11);
|
||||||
|
assert.equal(scaled.intents[1].value, 2);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
|
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
|
||||||
// ⚠️ 전투 규칙은 tools/deck/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것.
|
// ⚠️ 전투 규칙은 tools/deck/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것.
|
||||||
// (데이터는 data/*.json 공유, 규칙 로직은 JS로 중복 재현)
|
// (데이터는 data/*.json 공유, 규칙 로직은 JS로 중복 재현)
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
@@ -27,6 +27,16 @@ export function shuffle(arr, rng) {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareCombatDrawPile(deck, cards) {
|
||||||
|
const rest = [];
|
||||||
|
const innate = [];
|
||||||
|
for (const id of deck) {
|
||||||
|
if (cards[id]?.innate === true) innate.push(id);
|
||||||
|
else rest.push(id);
|
||||||
|
}
|
||||||
|
return rest.concat(innate);
|
||||||
|
}
|
||||||
|
|
||||||
// 공격 피해 공식 — Lua CalcPlayerAttack(힘·약화) + DealDamageToTarget(취약)과 동기화.
|
// 공격 피해 공식 — Lua CalcPlayerAttack(힘·약화) + DealDamageToTarget(취약)과 동기화.
|
||||||
// floor((base + str) * (weak>0 ? 0.75 : 1)) → floor(... * (vulnOnTarget>0 ? 1.5 : 1))
|
// 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
|
// 보상 카드 등급 추첨 (Lua OfferReward 미러) — roll ∈ 1..100, normal 70 / unique 25 / legend 5
|
||||||
@@ -44,6 +54,11 @@ export function calcAttack(base, str, weak, vulnOnTarget) {
|
|||||||
return dmg;
|
return dmg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calcEnemyAttack(base, str, weak, vulnOnTarget, strengthLoss = 0) {
|
||||||
|
// Lua EnemyActStep 동기화: 힘 손실은 (value+str) 전체에서 차감(음수 힘 허용), 최종 calcAttack이 0 클램프.
|
||||||
|
return calcAttack(base, str - strengthLoss, weak, vulnOnTarget);
|
||||||
|
}
|
||||||
|
|
||||||
// 방어 우선 차감 후 hp 적용 → { hp, block }
|
// 방어 우선 차감 후 hp 적용 → { hp, block }
|
||||||
export function applyDamage(hp, block, amount) {
|
export function applyDamage(hp, block, amount) {
|
||||||
let dmg = amount;
|
let dmg = amount;
|
||||||
@@ -70,17 +85,64 @@ export function loadData() {
|
|||||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDecks.warrior, monsters };
|
return { cards: cardsData.cards, starterDeck: cardsData.starterDecks.warrior, monsters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canPlayCardNow(card, ctx = {}) {
|
||||||
|
if (!card) return false;
|
||||||
|
if (card.playableWhenDrawPileEmpty === true && (ctx.drawPileCount || 0) > 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
||||||
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
|
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
|
||||||
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
|
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
|
||||||
export function chooseAction(hand, cards, energy) {
|
export function chooseAction(hand, cards, energy, ctx = {}) {
|
||||||
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id] && cards[x.id].cost <= energy && !cards[x.id].unplayable);
|
const entries = hand.map((id, i) => ({ id, i })).filter((x) => {
|
||||||
|
const card = cards[x.id];
|
||||||
|
if (!card || card.unplayable || !canPlayCardNow(card, ctx)) return false;
|
||||||
|
let effectiveCost = card.cost || 0;
|
||||||
|
if (ctx.handCostZeroThisTurn === true) effectiveCost = 0;
|
||||||
|
else if (card.useAllEnergy === true) effectiveCost = 1;
|
||||||
|
else if (card.kind === 'Skill') {
|
||||||
|
if (ctx.nextSkillCostZero === true) effectiveCost = 0;
|
||||||
|
else effectiveCost = Math.max(0, effectiveCost - (ctx.skillCostReductionThisTurn || 0));
|
||||||
|
}
|
||||||
|
if (ctx.combatCardCostReduction && ctx.combatCardCostReduction[x.id] != null) {
|
||||||
|
effectiveCost = Math.max(0, effectiveCost - ctx.combatCardCostReduction[x.id]);
|
||||||
|
}
|
||||||
|
return card.useAllEnergy === true ? true : effectiveCost <= energy;
|
||||||
|
});
|
||||||
const powers = entries.filter((x) => cards[x.id].kind === 'Power');
|
const powers = entries.filter((x) => cards[x.id].kind === 'Power');
|
||||||
const attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
|
const attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
|
||||||
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
|
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 effectiveCost = (x) => {
|
||||||
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(cards[x.id].cost, 1);
|
const card = cards[x.id];
|
||||||
|
let cost = card.cost || 0;
|
||||||
|
if (ctx.handCostZeroThisTurn === true) cost = 0;
|
||||||
|
else if (card.useAllEnergy === true) cost = 1;
|
||||||
|
else if (card.kind === 'Skill') {
|
||||||
|
if (ctx.nextSkillCostZero === true) cost = 0;
|
||||||
|
else cost = Math.max(0, cost - (ctx.skillCostReductionThisTurn || 0));
|
||||||
|
}
|
||||||
|
if (ctx.combatCardCostReduction && ctx.combatCardCostReduction[x.id] != null) {
|
||||||
|
cost = Math.max(0, cost - ctx.combatCardCostReduction[x.id]);
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
};
|
||||||
|
const dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(effectiveCost(x), 1);
|
||||||
|
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(effectiveCost(x), 1);
|
||||||
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
|
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
|
||||||
|
if ((ctx.incomingDamage || 0) > (ctx.currentBlock || 0)) {
|
||||||
|
const defensive = entries.filter((x) => {
|
||||||
|
const card = cards[x.id];
|
||||||
|
return (card.block || 0) > 0 || (card.intangible || 0) > 0 || (card.enemyStrengthLossThisTurn || 0) > 0;
|
||||||
|
});
|
||||||
|
if (defensive.length) {
|
||||||
|
return bestBy(defensive, (x) => {
|
||||||
|
const card = cards[x.id];
|
||||||
|
const protection = (card.block || 0) + (card.intangible || 0) * 15 + (card.enemyStrengthLossThisTurn || 0) * 2;
|
||||||
|
return protection / Math.max(effectiveCost(x), 1);
|
||||||
|
}).i;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (powers.length) return powers[0].i;
|
if (powers.length) return powers[0].i;
|
||||||
if (attacks.length) return bestBy(attacks, dmgEff).i;
|
if (attacks.length) return bestBy(attacks, dmgEff).i;
|
||||||
if (skills.length) return bestBy(skills, blkEff).i;
|
if (skills.length) return bestBy(skills, blkEff).i;
|
||||||
@@ -105,31 +167,157 @@ function bump(s, cost, dmg, blk) {
|
|||||||
// 반환: { win, turns, playerHpRemaining, draw? }
|
// 반환: { win, turns, playerHpRemaining, draw? }
|
||||||
export function simulateCombat(data, rng, stats) {
|
export function simulateCombat(data, rng, stats) {
|
||||||
const { cards, starterDeck, monsters } = data;
|
const { cards, starterDeck, monsters } = data;
|
||||||
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: PLAYER_HP };
|
const playerMaxHp = data.playerMaxHp || PLAYER_HP;
|
||||||
let drawPile = shuffle(starterDeck, rng);
|
const startingPlayerHp = Math.min(data.playerHp ?? playerMaxHp, playerMaxHp);
|
||||||
|
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: startingPlayerHp };
|
||||||
|
let drawPile = prepareCombatDrawPile(shuffle(starterDeck, rng), cards);
|
||||||
let discard = [];
|
let discard = [];
|
||||||
const exhaust = [];
|
const exhaust = [];
|
||||||
let hand = [];
|
let hand = [];
|
||||||
let pHp = PLAYER_HP, pBlock = 0;
|
let pHp = startingPlayerHp, pBlock = data.playerStartBlock || 0;
|
||||||
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0;
|
let pStr = data.playerStrength || 0, pDex = 0, pThorns = data.playerThorns || 0, pWeak = 0, pVuln = 0, pIntangible = 0;
|
||||||
|
let blockGainMultiplier = 1;
|
||||||
|
let handCostZeroThisTurn = false;
|
||||||
|
let drawDisabledThisTurn = false;
|
||||||
|
let nextSkillCostZero = false;
|
||||||
|
let nextSkillRepeatCount = 0;
|
||||||
|
let skillCostReductionThisTurn = 0;
|
||||||
|
const combatCardCostReduction = {};
|
||||||
|
let nextTurnBlock = 0, nextTurnDraw = 0, nextTurnKeepBlock = false;
|
||||||
|
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
|
||||||
|
let nextTurnAddCards = [];
|
||||||
|
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
|
||||||
|
let turnCardsPlayedThisTurn = 0;
|
||||||
|
let damageDealtThisTurn = 0;
|
||||||
|
let shivFirstDamageBonusUsed = false;
|
||||||
|
let drawDamageThisTurn = 0;
|
||||||
|
let drawPoisonThisTurn = 0;
|
||||||
|
let shivAoeThisCombat = false;
|
||||||
|
const skillSlyOnPlayCards = new Set();
|
||||||
|
const turnSkillSlyCards = new Set();
|
||||||
|
let poisonApplicationsThisCombat = 0;
|
||||||
|
let enemyStrengthLossThisTurn = 0;
|
||||||
|
let cardsDrawnThisCombat = 0;
|
||||||
|
let bonusRewardScreens = 0;
|
||||||
|
let activeKillReward = 0;
|
||||||
|
let energy = 0;
|
||||||
const powers = [];
|
const powers = [];
|
||||||
const mob = monsters.map((m) => ({
|
const mob = monsters.map((m) => ({
|
||||||
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: 0, weak: 0, vuln: 0, poison: 0,
|
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: m.str || 0, weak: 0, vuln: 0, poison: 0, artifact: m.artifact || 0,
|
||||||
intents: m.intents, intentIdx: 0, alive: true,
|
intents: m.intents, intentIdx: 0, alive: true,
|
||||||
}));
|
}));
|
||||||
let turns = 0;
|
let turns = 0;
|
||||||
|
|
||||||
|
const aliveMonsters = () => mob.filter((m) => m.alive);
|
||||||
|
const countAliveMonsters = () => aliveMonsters().length;
|
||||||
|
const randomAliveMonster = () => {
|
||||||
|
const alive = aliveMonsters();
|
||||||
|
if (!alive.length) return null;
|
||||||
|
return alive[Math.floor(rng() * alive.length)];
|
||||||
|
};
|
||||||
|
const expectedIncomingDamage = () => mob.filter((m) => m.alive).reduce((total, m) => {
|
||||||
|
if (!m.intents || m.intents.length === 0) return total;
|
||||||
|
const expected = m.intents.reduce((sum, intent) => {
|
||||||
|
if (intent.kind !== 'Attack') return sum;
|
||||||
|
let amount = calcEnemyAttack(intent.value, m.str, m.weak, pVuln, enemyStrengthLossThisTurn);
|
||||||
|
if (pIntangible > 0 && amount > 1) amount = 1;
|
||||||
|
return sum + amount;
|
||||||
|
}, 0) / m.intents.length;
|
||||||
|
return total + expected;
|
||||||
|
}, 0);
|
||||||
|
const removeEnemyBlock = (target) => {
|
||||||
|
if (target) target.block = 0;
|
||||||
|
};
|
||||||
|
const removeEnemyArtifact = (target) => {
|
||||||
|
if (target) target.artifact = 0;
|
||||||
|
};
|
||||||
|
const applyMonsterWeak = (target, amount) => {
|
||||||
|
if (!target || !amount || amount <= 0) return;
|
||||||
|
if (target.artifact > 0) { target.artifact--; return; }
|
||||||
|
target.weak += amount;
|
||||||
|
};
|
||||||
|
const applyMonsterVuln = (target, amount) => {
|
||||||
|
if (!target || !amount || amount <= 0) return;
|
||||||
|
if (target.artifact > 0) { target.artifact--; return; }
|
||||||
|
target.vuln += amount;
|
||||||
|
};
|
||||||
|
const applyPoisonToMonster = (target, amount) => {
|
||||||
|
if (!target || !target.alive || !amount || amount <= 0) return;
|
||||||
|
if (target.artifact > 0) { target.artifact--; return; }
|
||||||
|
target.poison += amount;
|
||||||
|
poisonApplicationsThisCombat += 1;
|
||||||
|
const burstEvery = powerFieldTotal('poisonApplicationBurstEvery');
|
||||||
|
const burstDamage = powerFieldTotal('poisonApplicationBurstDamage');
|
||||||
|
if (burstEvery > 0 && burstDamage > 0 && poisonApplicationsThisCombat % burstEvery === 0) {
|
||||||
|
for (const m of mob) {
|
||||||
|
if (!m.alive) continue;
|
||||||
|
const r = applyDamage(m.hp, m.block, burstDamage);
|
||||||
|
m.hp = r.hp; m.block = r.block;
|
||||||
|
if (burstDamage > 0) damageDealtThisTurn += burstDamage;
|
||||||
|
if (m.hp <= 0) m.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dealDamageToMonster = (target, amount, pierce = false) => {
|
||||||
|
if (!target || !target.alive) return false;
|
||||||
|
let dmg = amount;
|
||||||
|
const effectiveStr = Math.max(0, target.str - enemyStrengthLossThisTurn);
|
||||||
|
dmg = calcAttack(dmg, effectiveStr, target.weak, 0);
|
||||||
|
if (target.vuln > 0) dmg = Math.floor(dmg * 1.5);
|
||||||
|
if (target.block > 0 && !pierce) {
|
||||||
|
const absorbed = Math.min(target.block, dmg);
|
||||||
|
target.block -= absorbed;
|
||||||
|
dmg -= absorbed;
|
||||||
|
}
|
||||||
|
target.hp -= dmg;
|
||||||
|
if (dmg > 0) {
|
||||||
|
const attackPoison = powerFieldTotal('attackPoison');
|
||||||
|
if (attackPoison > 0) applyPoisonToMonster(target, attackPoison);
|
||||||
|
}
|
||||||
|
if (target.hp <= 0) {
|
||||||
|
target.hp = 0;
|
||||||
|
target.alive = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
function draw(n) {
|
function draw(n) {
|
||||||
|
const drawn = [];
|
||||||
|
if (drawDisabledThisTurn === true) return drawn;
|
||||||
for (let k = 0; k < n; k++) {
|
for (let k = 0; k < n; k++) {
|
||||||
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
|
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
|
||||||
if (drawPile.length === 0) break;
|
if (drawPile.length === 0) break;
|
||||||
const card = drawPile.pop();
|
const card = drawPile.pop();
|
||||||
|
drawn.push(card);
|
||||||
|
cardsDrawnThisCombat++;
|
||||||
|
const drawDamage = powerFieldTotal('drawDamage') + drawDamageThisTurn;
|
||||||
|
const drawPoison = powerFieldTotal('drawPoison') + drawPoisonThisTurn;
|
||||||
|
if ((drawDamage > 0 || drawPoison > 0) && mob.some((m) => m.alive)) {
|
||||||
|
for (const m of mob) {
|
||||||
|
if (!m.alive) continue;
|
||||||
|
let dmg = drawDamage;
|
||||||
|
if (m.vuln > 0) dmg = Math.floor(dmg * 1.5);
|
||||||
|
if (m.block > 0) {
|
||||||
|
const absorbed = Math.min(m.block, dmg);
|
||||||
|
m.block -= absorbed;
|
||||||
|
dmg -= absorbed;
|
||||||
|
}
|
||||||
|
if (drawPoison > 0) applyPoisonToMonster(m, drawPoison);
|
||||||
|
if (dmg > 0) {
|
||||||
|
m.hp -= dmg;
|
||||||
|
damageDealtThisTurn += dmg;
|
||||||
|
}
|
||||||
|
if (m.hp <= 0) { m.hp = 0; m.alive = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
|
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
|
||||||
if (hand.length >= 10) {
|
if (hand.length >= 10) {
|
||||||
discard.push(card);
|
discard.push(card);
|
||||||
triggerSly(card);
|
triggerSly(card);
|
||||||
} else hand.push(card);
|
} else hand.push(card);
|
||||||
}
|
}
|
||||||
|
return drawn;
|
||||||
}
|
}
|
||||||
function addCardsToHand(id, n) {
|
function addCardsToHand(id, n) {
|
||||||
for (let k = 0; k < n; k++) {
|
for (let k = 0; k < n; k++) {
|
||||||
@@ -137,69 +325,306 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
else hand.push(id);
|
else hand.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function addBlock(base) {
|
||||||
|
let amount = base || 0;
|
||||||
|
if (amount > 0) amount += pDex;
|
||||||
|
if (blockGainMultiplier > 1) amount *= blockGainMultiplier;
|
||||||
|
if (amount < 0) amount = 0;
|
||||||
|
pBlock += amount;
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
function smartDiscardIndex() {
|
||||||
|
if (hand.length === 0) return -1;
|
||||||
|
if (data.smartPlayer !== true) return hand.length - 1;
|
||||||
|
const ranked = hand.map((id, index) => {
|
||||||
|
const card = cards[id] || {};
|
||||||
|
const isSly = card.sly === true || skillSlyOnPlayCards.has(id) || turnSkillSlyCards.has(id);
|
||||||
|
const utility = (card.damage || 0) * (card.hits || 1)
|
||||||
|
+ (card.block || 0)
|
||||||
|
+ (card.draw || 0) * 4
|
||||||
|
+ (card.addShiv || 0) * 4
|
||||||
|
+ (card.poison || 0) * 2;
|
||||||
|
return { index, isSly, unplayable: card.unplayable === true, tooExpensive: (card.cost || 0) > energy, utility };
|
||||||
|
});
|
||||||
|
ranked.sort((a, b) => Number(b.isSly) - Number(a.isSly)
|
||||||
|
|| Number(b.unplayable) - Number(a.unplayable)
|
||||||
|
|| Number(b.tooExpensive) - Number(a.tooExpensive)
|
||||||
|
|| a.utility - b.utility
|
||||||
|
|| a.index - b.index);
|
||||||
|
return ranked[0].index;
|
||||||
|
}
|
||||||
|
function discardForTurnStart(n) {
|
||||||
|
const cnt = Math.min(n, hand.length);
|
||||||
|
for (let i = 0; i < cnt; i++) {
|
||||||
|
const idx = data.smartPlayer === true ? smartDiscardIndex() : hand
|
||||||
|
.map((id, k) => ({ id, k, card: cards[id] }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ac = a.card?.cost || 0;
|
||||||
|
const bc = b.card?.cost || 0;
|
||||||
|
if (ac !== bc) return ac - bc;
|
||||||
|
const ad = a.card?.damage || 0;
|
||||||
|
const bd = b.card?.damage || 0;
|
||||||
|
if (ad !== bd) return ad - bd;
|
||||||
|
return a.k - b.k;
|
||||||
|
})[0]?.k;
|
||||||
|
if (idx == null) break;
|
||||||
|
discardHandCard(idx, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function countOtherHandSkills(currentId) {
|
||||||
|
let n = 0;
|
||||||
|
let skippedSelf = false;
|
||||||
|
for (const id of hand) {
|
||||||
|
if (!skippedSelf && id === currentId) { skippedSelf = true; continue; }
|
||||||
|
if (cards[id]?.kind === 'Skill') n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function attackBaseForCard(id, c) {
|
||||||
|
let base = c.damage || 0;
|
||||||
|
const otherHand = Math.max(0, hand.length - 1);
|
||||||
|
if (c.damagePerOtherHandCard) base += otherHand * c.damagePerOtherHandCard;
|
||||||
|
if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn;
|
||||||
|
if (c.damagePerDiscardedThisTurn) base += turnDiscardedCards * c.damagePerDiscardedThisTurn;
|
||||||
|
if (c.damagePerSkillInHand) base += countOtherHandSkills(id) * c.damagePerSkillInHand;
|
||||||
|
if (c.damagePerCardDrawnThisCombat) base += cardsDrawnThisCombat * c.damagePerCardDrawnThisCombat;
|
||||||
|
if (c.kind === 'Attack' && turnCardsPlayedThisTurn === 0 && c.firstCardDamageBonus) base += c.firstCardDamageBonus;
|
||||||
|
if (c.class === 'shiv') {
|
||||||
|
if (powerFieldTotal('shivDamageBonus') > 0) base += powerFieldTotal('shivDamageBonus');
|
||||||
|
if (!shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) base += powerFieldTotal('firstShivDamageBonus');
|
||||||
|
}
|
||||||
|
if (base < 0) base = 0;
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
function queueNextTurnAddCard(id, n) {
|
||||||
|
if (!id || !n || n <= 0) return;
|
||||||
|
const entry = nextTurnAddCards.find((x) => x.cardId === id);
|
||||||
|
if (entry) entry.amount += n;
|
||||||
|
else nextTurnAddCards.push({ cardId: id, amount: n });
|
||||||
|
}
|
||||||
|
function queueNextTurnEffects(c) {
|
||||||
|
if (!c) return;
|
||||||
|
if (c.nextTurnBlock) nextTurnBlock += c.nextTurnBlock;
|
||||||
|
if (c.nextTurnDraw) nextTurnDraw += c.nextTurnDraw;
|
||||||
|
if (c.nextTurnKeepBlock === true) nextTurnKeepBlock = true;
|
||||||
|
if (c.nextTurnAttackMultiplier && c.nextTurnAttackMultiplier > 0) nextTurnAttackMultiplier *= c.nextTurnAttackMultiplier;
|
||||||
|
}
|
||||||
|
function queueSelectedReserve(c) {
|
||||||
|
if (!c?.nextTurnSelectHandCard || !c.nextTurnCopies || hand.length === 0) return;
|
||||||
|
const choice = hand
|
||||||
|
.map((id, i) => ({ id, i, card: cards[id] }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ak = a.card?.kind === 'Attack' ? 3 : a.card?.kind === 'Skill' ? 2 : 1;
|
||||||
|
const bk = b.card?.kind === 'Attack' ? 3 : b.card?.kind === 'Skill' ? 2 : 1;
|
||||||
|
if (bk !== ak) return bk - ak;
|
||||||
|
const ad = a.card?.damage || 0;
|
||||||
|
const bd = b.card?.damage || 0;
|
||||||
|
if (bd !== ad) return bd - ad;
|
||||||
|
return a.i - b.i;
|
||||||
|
})[0];
|
||||||
|
if (choice?.id) queueNextTurnAddCard(choice.id, c.nextTurnCopies);
|
||||||
|
}
|
||||||
const aliveList = () => mob.filter((m) => m.alive);
|
const aliveList = () => mob.filter((m) => m.alive);
|
||||||
|
function powerFieldTotal(field) {
|
||||||
|
let total = 0;
|
||||||
|
for (const pid of powers) {
|
||||||
|
const pc = cards[pid];
|
||||||
|
if (pc?.[field] != null) total += pc[field];
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
function resolveCardEffects(id, c, costSpent, recordStats = true) {
|
function resolveCardEffects(id, c, costSpent, recordStats = true) {
|
||||||
const alive = aliveList();
|
const alive = aliveList();
|
||||||
let dmg = 0;
|
let dmg = 0;
|
||||||
let blockGained = 0;
|
let blockGained = 0;
|
||||||
if (c.kind === 'Attack') {
|
if (c.blockGainMultiplier && c.blockGainMultiplier > 0) blockGainMultiplier *= c.blockGainMultiplier;
|
||||||
if (alive.length && c.damage) {
|
if (c.nextSkillCostZero === true) nextSkillCostZero = true;
|
||||||
const target = chooseTarget(alive, calcAttack(c.damage || 0, pStr, pWeak, 0));
|
if (c.nextSkillRepeatCount && c.nextSkillRepeatCount > 0) nextSkillRepeatCount += c.nextSkillRepeatCount;
|
||||||
if (c.weak) target.weak += c.weak;
|
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
|
||||||
if (c.vuln) target.vuln += c.vuln;
|
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
|
||||||
const hitN = c.hits || 1;
|
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
|
||||||
let totalNv = 0;
|
if (c.drawDamage && c.kind !== 'Power') drawDamageThisTurn += c.drawDamage;
|
||||||
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
|
if (c.drawPoison && c.kind !== 'Power') drawPoisonThisTurn += c.drawPoison;
|
||||||
dmg = totalNv;
|
if (c.shivAoe === true && c.kind !== 'Power') shivAoeThisCombat = true;
|
||||||
if (c.aoe === true) {
|
if (c.skillSlyOnPlay === true && c.kind === 'Skill') skillSlyOnPlayCards.add(id);
|
||||||
for (const m2 of aliveList()) {
|
if (c.turnHandSlyCount && c.turnHandSlyCount > 0) {
|
||||||
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
let picked = 0;
|
||||||
const r2 = applyDamage(m2.hp, m2.block, d2);
|
for (const hid of hand) {
|
||||||
m2.hp = r2.hp; m2.block = r2.block;
|
if (hid === id) continue;
|
||||||
if (m2.hp <= 0) m2.alive = false;
|
const hc = cards[hid];
|
||||||
}
|
if (hc?.kind === 'Skill' && !turnSkillSlyCards.has(hid) && !skillSlyOnPlayCards.has(hid) && hc.sly !== true) {
|
||||||
} else {
|
turnSkillSlyCards.add(hid);
|
||||||
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
picked++;
|
||||||
if (c.pierce === true) {
|
if (picked >= c.turnHandSlyCount) break;
|
||||||
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; }
|
}
|
||||||
|
const xEnergy = costSpent || 0;
|
||||||
|
if (c.kind === 'Attack') {
|
||||||
|
if (alive.length && (c.damage || c.xDamagePerEnergy)) {
|
||||||
|
const baseDamage = c.xDamagePerEnergy ? xEnergy * c.xDamagePerEnergy : attackBaseForCard(id, c);
|
||||||
|
const bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast)
|
||||||
|
? c.bonusHitsWhenOtherHandAtLeast : 0;
|
||||||
|
const hitN = (c.hits || 1) + bonusHits;
|
||||||
|
let useAoe = c.aoe === true;
|
||||||
|
if (c.class === 'shiv' && shivAoeThisCombat === true) useAoe = true;
|
||||||
|
if (c.class === 'shiv' && !shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) {
|
||||||
|
shivFirstDamageBonusUsed = true;
|
||||||
|
}
|
||||||
|
const perHit = calcAttack(baseDamage || 0, pStr, pWeak, 0) * turnAttackMultiplier;
|
||||||
|
const dealToTarget = (target, amount) => {
|
||||||
|
if (!target || !target.alive) return { killed: false, dealt: 0 };
|
||||||
|
let dealt = amount;
|
||||||
|
if (target.vuln > 0) dealt = Math.floor(dealt * 1.5);
|
||||||
|
if (target.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) {
|
||||||
|
dealt = Math.floor(dealt * c.attackDamageVsWeakMultiplier);
|
||||||
|
}
|
||||||
|
if (c.pierce === true) {
|
||||||
|
target.hp -= dealt;
|
||||||
|
if (target.hp < 0) target.hp = 0;
|
||||||
|
} else {
|
||||||
|
const r = applyDamage(target.hp, target.block, dealt);
|
||||||
|
target.hp = r.hp; target.block = r.block;
|
||||||
|
}
|
||||||
|
const attackPoison = powerFieldTotal('attackPoison');
|
||||||
|
if (dealt > 0 && attackPoison > 0) applyPoisonToMonster(target, attackPoison);
|
||||||
|
let killed = false;
|
||||||
|
if (target.hp <= 0) {
|
||||||
|
target.alive = false;
|
||||||
|
killed = true;
|
||||||
|
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
|
||||||
|
}
|
||||||
|
return { killed, dealt };
|
||||||
|
};
|
||||||
|
const resolveAttackRound = () => {
|
||||||
|
let roundKilled = false;
|
||||||
|
let roundDamage = 0;
|
||||||
|
if (useAoe === true) {
|
||||||
|
for (const m2 of aliveList()) {
|
||||||
|
const r2 = dealToTarget(m2, perHit);
|
||||||
|
roundDamage += r2.dealt;
|
||||||
|
if (r2.killed) roundKilled = true;
|
||||||
|
}
|
||||||
|
} else if (c.randomTargetEachHit === true) {
|
||||||
|
for (let h = 0; h < hitN; h++) {
|
||||||
|
const target = randomAliveMonster();
|
||||||
|
if (!target) break;
|
||||||
|
const r = dealToTarget(target, perHit);
|
||||||
|
roundDamage += r.dealt;
|
||||||
|
if (r.killed) roundKilled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const preview = perHit;
|
||||||
|
const target = chooseTarget(aliveList(), preview);
|
||||||
|
if (target) {
|
||||||
|
if (c.weak) applyMonsterWeak(target, c.weak);
|
||||||
|
if (c.vuln) applyMonsterVuln(target, c.vuln);
|
||||||
|
const totalNv = perHit * hitN;
|
||||||
|
const r = dealToTarget(target, totalNv);
|
||||||
|
roundDamage += r.dealt;
|
||||||
|
if (r.killed) roundKilled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dmg += roundDamage;
|
||||||
|
damageDealtThisTurn += roundDamage;
|
||||||
|
return roundKilled;
|
||||||
|
};
|
||||||
|
let roundKilled = false;
|
||||||
|
do {
|
||||||
|
roundKilled = resolveAttackRound();
|
||||||
|
} while (c.repeatOnKill === true && roundKilled === true && countAliveMonsters() > 0);
|
||||||
|
}
|
||||||
|
if (c.block) blockGained = addBlock(c.block);
|
||||||
} else if (c.kind === 'Power') {
|
} else if (c.kind === 'Power') {
|
||||||
if (recordStats) powers.push(id);
|
if (recordStats) powers.push(id);
|
||||||
} else {
|
} else {
|
||||||
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
|
if (c.block) blockGained = addBlock(c.block);
|
||||||
if ((c.weak || c.vuln || c.poison) && alive.length) {
|
const weakAmount = (c.weak || 0) + (c.xWeakPerEnergy || 0) * xEnergy;
|
||||||
const target = chooseTarget(alive, 0);
|
const vulnAmount = c.vuln || 0;
|
||||||
if (c.weak) target.weak += c.weak;
|
if ((weakAmount || vulnAmount || c.poison || c.removeEnemyBlock || c.removeEnemyArtifact || c.enemyStrengthLossThisTurn) && alive.length) {
|
||||||
if (c.vuln) target.vuln += c.vuln;
|
const targets = c.affectsAllEnemies === true ? aliveList() : [chooseTarget(alive, 0)];
|
||||||
if (c.poison) target.poison += c.poison;
|
if (c.enemyStrengthLossThisTurn && c.enemyStrengthLossThisTurn > 0) {
|
||||||
|
enemyStrengthLossThisTurn += c.enemyStrengthLossThisTurn;
|
||||||
|
}
|
||||||
|
for (const target of targets) {
|
||||||
|
if (!target || !target.alive) continue;
|
||||||
|
if (c.removeEnemyBlock === true) removeEnemyBlock(target);
|
||||||
|
if (c.removeEnemyArtifact === true) removeEnemyArtifact(target);
|
||||||
|
if (weakAmount) applyMonsterWeak(target, weakAmount);
|
||||||
|
if (vulnAmount) applyMonsterVuln(target, vulnAmount);
|
||||||
|
if (c.poison) {
|
||||||
|
if (c.poisonIfTargetPoisoned !== true || target.poison > 0) {
|
||||||
|
const poisonHits = c.poisonHits || 1;
|
||||||
|
for (let i = 0; i < poisonHits; i++) {
|
||||||
|
const target2 = c.poisonRandomTargets === true
|
||||||
|
? alive[Math.floor(rng() * alive.length)]
|
||||||
|
: target;
|
||||||
|
if (target2) applyPoisonToMonster(target2, c.poison);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (c.strength) pStr += c.strength;
|
if (c.strength) pStr += c.strength;
|
||||||
if (c.dex) pDex += c.dex;
|
if (c.dex) pDex += c.dex;
|
||||||
if (c.thorns) pThorns += c.thorns;
|
if (c.thorns) pThorns += c.thorns;
|
||||||
if (c.selfVuln) pVuln += c.selfVuln;
|
if (c.selfVuln) pVuln += c.selfVuln;
|
||||||
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
if (c.heal) pHp = Math.min(pHp + c.heal, playerMaxHp);
|
||||||
if (c.draw) draw(c.draw);
|
if (c.gainEnergy) energy += c.gainEnergy;
|
||||||
|
activeKillReward = c.rewardOnKill || 0;
|
||||||
|
if (c.intangible) pIntangible += c.intangible;
|
||||||
|
queueNextTurnEffects(c);
|
||||||
|
turnCardsPlayedThisTurn++;
|
||||||
|
let drawnCards = [];
|
||||||
|
if (c.draw) drawnCards = drawnCards.concat(draw(c.draw));
|
||||||
|
if (c.drawUntilHandSize) {
|
||||||
|
const need = c.drawUntilHandSize - Math.max(0, hand.length - 1);
|
||||||
|
if (need > 0) drawnCards = drawnCards.concat(draw(need));
|
||||||
|
}
|
||||||
|
if (c.drawSkillBlock && c.drawSkillBlock > 0) {
|
||||||
|
for (const drawnId of drawnCards) {
|
||||||
|
if (cards[drawnId]?.kind === 'Skill') blockGained += addBlock(c.drawSkillBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
|
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
|
||||||
|
if (c.cardPlayedDamage && alive.length) {
|
||||||
|
const target = chooseTarget(aliveList(), 0);
|
||||||
|
if (target && target.alive) {
|
||||||
|
target.hp -= c.cardPlayedDamage;
|
||||||
|
dmg += c.cardPlayedDamage;
|
||||||
|
damageDealtThisTurn += c.cardPlayedDamage;
|
||||||
|
if (target.hp <= 0) target.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.cardPlayedRandomDamage && alive.length) {
|
||||||
|
const pool = aliveList();
|
||||||
|
if (pool.length) {
|
||||||
|
const target = pool[Math.floor(rng() * pool.length)];
|
||||||
|
if (target) {
|
||||||
|
target.hp -= c.cardPlayedRandomDamage;
|
||||||
|
dmg += c.cardPlayedRandomDamage;
|
||||||
|
damageDealtThisTurn += c.cardPlayedRandomDamage;
|
||||||
|
if (target.hp <= 0) target.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.blockPerDamageDealtThisTurn && c.blockPerDamageDealtThisTurn > 0 && c.kind !== 'Power') {
|
||||||
|
blockGained += addBlock(Math.max(0, damageDealtThisTurn * c.blockPerDamageDealtThisTurn));
|
||||||
|
}
|
||||||
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
|
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
|
||||||
}
|
}
|
||||||
function triggerSly(id) {
|
function triggerSly(id) {
|
||||||
const c = cards[id];
|
const c = cards[id];
|
||||||
if (!c?.sly) return;
|
if (!c) return;
|
||||||
|
if (!c.sly && !skillSlyOnPlayCards.has(id) && !turnSkillSlyCards.has(id)) return;
|
||||||
resolveCardEffects(id, c, 0, false);
|
resolveCardEffects(id, c, 0, false);
|
||||||
}
|
}
|
||||||
function discardHandCard(idx, trigger = true) {
|
function discardHandCard(idx, trigger = true) {
|
||||||
const [id] = hand.splice(idx, 1);
|
const [id] = hand.splice(idx, 1);
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
discard.push(id);
|
discard.push(id);
|
||||||
|
turnDiscardedCards++;
|
||||||
if (trigger) triggerSly(id);
|
if (trigger) triggerSly(id);
|
||||||
}
|
}
|
||||||
function applyDiscardEffects(c) {
|
function applyDiscardEffects(c) {
|
||||||
@@ -208,39 +633,111 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
while (hand.length) { discardHandCard(hand.length - 1, true); discarded++; }
|
while (hand.length) { discardHandCard(hand.length - 1, true); discarded++; }
|
||||||
} else if (c.discard) {
|
} else if (c.discard) {
|
||||||
const n = Math.min(c.discard, hand.length);
|
const n = Math.min(c.discard, hand.length);
|
||||||
for (let i = 0; i < n; i++) { discardHandCard(hand.length - 1, true); discarded++; }
|
for (let i = 0; i < n; i++) { discardHandCard(smartDiscardIndex(), true); discarded++; }
|
||||||
}
|
}
|
||||||
if (c.addShiv && (c.discard || c.discardAll === true)) addCardsToHand('Shiv', c.addShiv);
|
if (c.addShiv && (c.discard || c.discardAll === true)) addCardsToHand('Shiv', c.addShiv);
|
||||||
if (c.addShivPerDiscard === true) addCardsToHand('Shiv', discarded);
|
if (c.addShivPerDiscard === true) addCardsToHand('Shiv', discarded);
|
||||||
|
if (c.drawPerDiscarded) draw(discarded * c.drawPerDiscarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (turns < MAX_TURNS) {
|
while (turns < MAX_TURNS) {
|
||||||
turns++;
|
turns++;
|
||||||
|
turnAttackCardsPlayed = 0;
|
||||||
|
turnDiscardedCards = 0;
|
||||||
|
shivFirstDamageBonusUsed = false;
|
||||||
|
drawDamageThisTurn = 0;
|
||||||
|
drawPoisonThisTurn = 0;
|
||||||
|
shivAoeThisCombat = false;
|
||||||
|
turnSkillSlyCards.clear();
|
||||||
|
enemyStrengthLossThisTurn = 0;
|
||||||
|
blockGainMultiplier = 1;
|
||||||
|
handCostZeroThisTurn = false;
|
||||||
|
drawDisabledThisTurn = false;
|
||||||
|
skillCostReductionThisTurn = 0;
|
||||||
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
|
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
|
||||||
pBlock = 0;
|
if (nextTurnKeepBlock === true) nextTurnKeepBlock = false;
|
||||||
|
else pBlock = 0;
|
||||||
|
turnAttackMultiplier = nextTurnAttackMultiplier;
|
||||||
|
nextTurnAttackMultiplier = 1;
|
||||||
let energyBonus = 0;
|
let energyBonus = 0;
|
||||||
|
let powerTurnDraw = 0;
|
||||||
|
let powerTurnDiscard = 0;
|
||||||
for (const pid of powers) {
|
for (const pid of powers) {
|
||||||
const pc = cards[pid];
|
const pc = cards[pid];
|
||||||
if (!pc) continue;
|
if (!pc) continue;
|
||||||
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
|
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
|
||||||
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
|
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
|
||||||
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
|
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
|
||||||
|
else if (pc.powerEffect === 'poisonPerTurn') {
|
||||||
|
for (const m of mob) if (m.alive) applyPoisonToMonster(m, pc.value);
|
||||||
|
} else if (pc.powerEffect === 'damagePerTurn') {
|
||||||
|
for (const m of mob) {
|
||||||
|
if (!m.alive) continue;
|
||||||
|
const r = applyDamage(m.hp, m.block, pc.value || 0);
|
||||||
|
m.hp = r.hp; m.block = r.block;
|
||||||
|
if (m.hp <= 0) m.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
|
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
|
||||||
|
if (pc.turnStartDraw) powerTurnDraw += pc.turnStartDraw;
|
||||||
|
if (pc.turnStartDiscard) powerTurnDiscard += pc.turnStartDiscard;
|
||||||
}
|
}
|
||||||
let energy = ENERGY + energyBonus; draw(HAND_SIZE);
|
if (nextTurnBlock > 0) { addBlock(nextTurnBlock); nextTurnBlock = 0; }
|
||||||
|
if (nextTurnAddCards.length) {
|
||||||
|
for (const entry of nextTurnAddCards) addCardsToHand(entry.cardId, entry.amount);
|
||||||
|
nextTurnAddCards = [];
|
||||||
|
}
|
||||||
|
energy = ENERGY + (data.energyBonus || 0) + energyBonus;
|
||||||
|
const drawBonus = nextTurnDraw + powerTurnDraw;
|
||||||
|
nextTurnDraw = 0;
|
||||||
|
draw(HAND_SIZE + drawBonus + (turns === 1 ? (data.openingDrawBonus || 0) : 0));
|
||||||
|
if (powerTurnDiscard > 0) discardForTurnStart(powerTurnDiscard);
|
||||||
while (true) {
|
while (true) {
|
||||||
const alive = aliveList();
|
const alive = aliveList();
|
||||||
if (alive.length === 0) break;
|
if (alive.length === 0) break;
|
||||||
const idx = chooseAction(hand, cards, energy);
|
const idx = chooseAction(hand, cards, energy, {
|
||||||
|
drawPileCount: drawPile.length,
|
||||||
|
nextSkillCostZero,
|
||||||
|
skillCostReductionThisTurn,
|
||||||
|
handCostZeroThisTurn,
|
||||||
|
combatCardCostReduction,
|
||||||
|
incomingDamage: data.smartPlayer === true ? expectedIncomingDamage() : 0,
|
||||||
|
currentBlock: pBlock,
|
||||||
|
});
|
||||||
if (idx < 0) break;
|
if (idx < 0) break;
|
||||||
const id = hand[idx], c = cards[id];
|
const id = hand[idx], c = cards[id];
|
||||||
energy -= c.cost;
|
let dmg = 0;
|
||||||
resolveCardEffects(id, c, c.cost);
|
const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
|
||||||
|
const skillRepeat = c.kind === 'Skill' ? nextSkillRepeatCount : 0;
|
||||||
|
const baseCost = c.cost || 0;
|
||||||
|
const combatReduction = combatCardCostReduction[id] || 0;
|
||||||
|
const cost = handCostZeroThisTurn === true ? 0 : (c.useAllEnergy === true ? energy : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost)));
|
||||||
|
const finalCost = c.useAllEnergy === true ? cost : Math.max(0, cost - combatReduction);
|
||||||
|
energy -= finalCost;
|
||||||
|
resolveCardEffects(id, c, finalCost);
|
||||||
|
if (c.kind === 'Attack' && (data.healOnAttack || 0) > 0) {
|
||||||
|
pHp = Math.min(playerMaxHp, pHp + data.healOnAttack);
|
||||||
|
}
|
||||||
|
const playedBlock = powerFieldTotal('cardPlayedBlock');
|
||||||
|
if (playedBlock > 0) addBlock(playedBlock);
|
||||||
|
if (skillRepeat > 0) {
|
||||||
|
nextSkillRepeatCount = Math.max(0, nextSkillRepeatCount - skillRepeat);
|
||||||
|
for (let r = 0; r < skillRepeat; r++) {
|
||||||
|
resolveCardEffects(id, c, finalCost);
|
||||||
|
if (playedBlock > 0) addBlock(playedBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.kind === 'Attack') turnAttackCardsPlayed++;
|
||||||
|
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
|
||||||
hand.splice(idx, 1);
|
hand.splice(idx, 1);
|
||||||
|
queueSelectedReserve(c);
|
||||||
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
|
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
|
||||||
else if (c.kind !== 'Power') discard.push(id);
|
else if (c.kind !== 'Power') discard.push(id);
|
||||||
|
if (c.combatCostReductionOnPlay && c.combatCostReductionOnPlay > 0) {
|
||||||
|
combatCardCostReduction[id] = (combatCardCostReduction[id] || 0) + c.combatCostReductionOnPlay;
|
||||||
|
}
|
||||||
applyDiscardEffects(c);
|
applyDiscardEffects(c);
|
||||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
|
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
|
||||||
}
|
}
|
||||||
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
|
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
|
||||||
let burn = 0;
|
let burn = 0;
|
||||||
@@ -249,10 +746,18 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
const kept = [];
|
const kept = [];
|
||||||
for (const hid of hand) {
|
for (const hid of hand) {
|
||||||
const hc = cards[hid];
|
const hc = cards[hid];
|
||||||
if (hc?.retain === true) kept.push(hid);
|
if (hc?.retain === true || (hc?.class === 'shiv' && powerFieldTotal('shivRetain') > 0)) kept.push(hid);
|
||||||
else discard.push(hid);
|
else discard.push(hid);
|
||||||
}
|
}
|
||||||
hand = kept;
|
hand = kept;
|
||||||
|
for (const pid of powers) {
|
||||||
|
const pc = cards[pid];
|
||||||
|
if (pc?.endTurnDexLoss) {
|
||||||
|
pDex -= pc.endTurnDexLoss;
|
||||||
|
if (pDex < 0) pDex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pIntangible > 0) pIntangible--;
|
||||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||||
// 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전)
|
// 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전)
|
||||||
if (pWeak > 0) pWeak--;
|
if (pWeak > 0) pWeak--;
|
||||||
@@ -260,19 +765,24 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
for (const m of mob) {
|
for (const m of mob) {
|
||||||
if (!m.alive) continue;
|
if (!m.alive) continue;
|
||||||
// 독 틱 — 행동 시작 시 (Lua EnemyActStep 동기화). 사망 시 행동 생략
|
// 독 틱 — 행동 시작 시 (Lua EnemyActStep 동기화). 사망 시 행동 생략
|
||||||
if (m.poison > 0) {
|
const poisonTicks = 1 + Math.max(0, powerFieldTotal('extraPoisonTicks'));
|
||||||
|
for (let tick = 0; tick < poisonTicks; tick++) {
|
||||||
|
if (m.poison <= 0) break;
|
||||||
m.hp -= m.poison;
|
m.hp -= m.poison;
|
||||||
m.poison--;
|
m.poison--;
|
||||||
if (m.hp <= 0) { m.hp = 0; m.alive = false; continue; }
|
if (m.hp <= 0) { m.hp = 0; m.alive = false; break; }
|
||||||
}
|
}
|
||||||
|
if (!m.alive) continue;
|
||||||
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
|
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
|
||||||
// 정의된 intent 중 랜덤 선택 (Lua EnemyActStep 동기화 — 순차→랜덤)
|
// 정의된 intent 중 랜덤 선택 (Lua EnemyActStep 동기화 — 순차→랜덤)
|
||||||
const it = m.intents.length ? m.intents[Math.floor(rng() * m.intents.length)] : null;
|
const it = m.intents.length ? m.intents[Math.floor(rng() * m.intents.length)] : null;
|
||||||
if (it) {
|
if (it) {
|
||||||
if (it.kind === 'Attack') {
|
if (it.kind === 'Attack') {
|
||||||
const atk = calcAttack(it.value, m.str, m.weak, pVuln);
|
const atk = calcEnemyAttack(it.value, m.str, m.weak, pVuln, enemyStrengthLossThisTurn);
|
||||||
const beforeHp = pHp;
|
const beforeHp = pHp;
|
||||||
const r = applyDamage(pHp, pBlock, atk); pHp = r.hp; pBlock = r.block;
|
let incoming = atk;
|
||||||
|
if (pIntangible > 0 && incoming > 1) incoming = 1;
|
||||||
|
const r = applyDamage(pHp, pBlock, incoming); pHp = r.hp; pBlock = r.block;
|
||||||
if (beforeHp > pHp && pThorns > 0) {
|
if (beforeHp > pHp && pThorns > 0) {
|
||||||
m.hp -= pThorns;
|
m.hp -= pThorns;
|
||||||
if (m.hp <= 0) m.alive = false;
|
if (m.hp <= 0) m.alive = false;
|
||||||
@@ -293,9 +803,9 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||||
}
|
}
|
||||||
// 독 사망 등 적 페이즈 중 전멸 처리 (Lua FinishEnemyTurn→CheckCombatEnd 동기화)
|
// 독 사망 등 적 페이즈 중 전멸 처리 (Lua FinishEnemyTurn→CheckCombatEnd 동기화)
|
||||||
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp };
|
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
|
||||||
}
|
}
|
||||||
return { win: false, turns, playerHpRemaining: pHp, draw: true };
|
return { win: false, turns, playerHpRemaining: pHp, draw: true, bonusRewardScreens };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }
|
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, rarityForRoll,
|
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, calcEnemyAttack, rarityForRoll,
|
||||||
} from './sim-balance.mjs';
|
} from './sim-balance.mjs';
|
||||||
|
|
||||||
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
||||||
@@ -13,6 +13,85 @@ test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
|||||||
assert.equal(rarityForRoll(100), 'legend');
|
assert.equal(rarityForRoll(100), 'legend');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnBlock grants block on the following turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
GuardLater: { name: "예약 방어", cost: 0, kind: "Skill", nextTurnBlock: 4 },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["GuardLater", "Pass"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 99, intents: [{ kind: "Attack", value: 3 }, { kind: "Attack", value: 3 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, false);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 77);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnDraw draws extra cards next turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Setup: { name: "설치", cost: 0, kind: "Skill", nextTurnDraw: 2 },
|
||||||
|
Hit1: { name: "타격1", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Hit2: { name: "타격2", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass4: { name: "대기4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Hit1", "Hit2", "Pass1", "Pass2", "Pass3", "Pass4", "Setup"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnKeepBlock preserves current block", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
BlurLater: { name: "흐릿함", cost: 0, kind: "Skill", block: 5, nextTurnKeepBlock: true },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["BlurLater", "Pass"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 99, intents: [{ kind: "Attack", value: 3 }, { kind: "Attack", value: 3 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, false);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnAttackMultiplier boosts attacks next turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Prep: { name: "그림자 걸음", cost: 0, kind: "Skill", nextTurnAttackMultiplier: 2 },
|
||||||
|
Hit: { name: "타격", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Prep", "Pass", "Hit"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnSelectHandCard queues selected copies for next turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Nightmare: { name: "악몽", cost: 0, kind: "Skill", nextTurnCopies: 3, nextTurnSelectHandCard: true },
|
||||||
|
Hit: { name: "타격", cost: 0, kind: "Attack", damage: 2 },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Pass", "Nightmare", "Hit"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 4);
|
||||||
|
});
|
||||||
|
|
||||||
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
||||||
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
||||||
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
||||||
@@ -42,6 +121,14 @@ test('chooseAction: 공격 없으면 스킬 선택', () => {
|
|||||||
assert.equal(idx, 0);
|
assert.equal(idx, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('chooseAction: 예상 피해가 남으면 방어 카드를 우선 선택', () => {
|
||||||
|
const cards = {
|
||||||
|
Hit: { kind: 'Attack', cost: 1, damage: 12 },
|
||||||
|
Guard: { kind: 'Skill', cost: 1, block: 8 },
|
||||||
|
};
|
||||||
|
assert.equal(chooseAction(['Hit', 'Guard'], cards, 1, { incomingDamage: 8, currentBlock: 0 }), 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('chooseAction: 사용 가능 카드 없으면 -1', () => {
|
test('chooseAction: 사용 가능 카드 없으면 -1', () => {
|
||||||
const idx = chooseAction(['Bash'], CARDS, 1);
|
const idx = chooseAction(['Bash'], CARDS, 1);
|
||||||
assert.equal(idx, -1);
|
assert.equal(idx, -1);
|
||||||
@@ -141,6 +228,21 @@ test('simulateCombat: 복합 카드(공격+방어) 블록이 적 공격을 흡
|
|||||||
assert.equal(r.playerHpRemaining, 80);
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('simulateCombat: 캠페인 시작 체력과 유물 전투 보너스를 반영', () => {
|
||||||
|
const data = {
|
||||||
|
cards: { Guard: { name: 'Guard', cost: 1, kind: 'Skill', block: 1 } },
|
||||||
|
starterDeck: ['Guard'],
|
||||||
|
monsters: [{ name: 'Dummy', maxHp: 1, intents: [{ kind: 'Attack', value: 1 }] }],
|
||||||
|
playerHp: 37,
|
||||||
|
playerMaxHp: 70,
|
||||||
|
playerStartBlock: 6,
|
||||||
|
energyBonus: 1,
|
||||||
|
openingDrawBonus: 2,
|
||||||
|
};
|
||||||
|
const result = simulateCombat(data, mulberry32(3));
|
||||||
|
assert.ok(result.playerHpRemaining <= 37);
|
||||||
|
});
|
||||||
|
|
||||||
test('calcAttack: 힘·약화·취약 공식 (Lua CalcPlayerAttack·DealDamageToTarget 동기화)', () => {
|
test('calcAttack: 힘·약화·취약 공식 (Lua CalcPlayerAttack·DealDamageToTarget 동기화)', () => {
|
||||||
assert.equal(calcAttack(6, 0, 0, 0), 6); // 기본
|
assert.equal(calcAttack(6, 0, 0, 0), 6); // 기본
|
||||||
assert.equal(calcAttack(6, 2, 0, 0), 8); // 힘+2
|
assert.equal(calcAttack(6, 2, 0, 0), 8); // 힘+2
|
||||||
@@ -183,6 +285,19 @@ test('simulateCombat: 카드 취약 부여가 같은 카드 피해에 선적용
|
|||||||
assert.equal(r.turns, 1);
|
assert.equal(r.turns, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('simulateCombat: firstCardDamageBonus가 턴 첫 카드에 적용 (kind===Attack, Lua 동기화)', () => {
|
||||||
|
// ChargedBlow처럼 class=warrior·kind=Attack인 카드의 첫-카드 보너스.
|
||||||
|
// 게이트가 class==="Attack"이면 영구 false라 미발동(버그) → 5뎀/2턴.
|
||||||
|
// kind==="Attack"이면 5+2=7 → 1턴 처치.
|
||||||
|
const data = {
|
||||||
|
cards: { CB: { name: '차지블로우', cost: 3, kind: 'Attack', class: 'warrior', damage: 5, firstCardDamageBonus: 2 } },
|
||||||
|
starterDeck: ['CB', 'CB', 'CB', 'CB', 'CB'],
|
||||||
|
monsters: [{ name: '적', maxHp: 7, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, mulberry32(1));
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('simulateCombat: Power(매턴 힘) 누적', () => {
|
test('simulateCombat: Power(매턴 힘) 누적', () => {
|
||||||
const data = {
|
const data = {
|
||||||
cards: {
|
cards: {
|
||||||
@@ -461,3 +576,642 @@ test("simulateCombat: addShiv creates shuriken cards in hand", () => {
|
|||||||
assert.equal(r.win, true);
|
assert.equal(r.win, true);
|
||||||
assert.equal(r.turns, 1);
|
assert.equal(r.turns, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: innate cards are drawn into the opening hand first", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Backstab: { name: "배신", cost: 0, kind: "Attack", damage: 11, innate: true, exhaust: true },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass4: { name: "대기4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass5: { name: "대기5", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Pass1", "Pass2", "Pass3", "Pass4", "Pass5", "Backstab"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 11, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: GrandFinale waits until draw pile is empty", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Finale: { name: "피날레", cost: 0, kind: "Attack", damage: 60, aoe: true, playableWhenDrawPileEmpty: true },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass4: { name: "대기4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass5: { name: "대기5", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Pass1", "Pass2", "Pass3", "Pass4", "Pass5", "Finale"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 60, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, false);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: turnStartDraw and turnStartDiscard powers resolve at turn start", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Tool: { name: "작업 도구", cost: 0, kind: "Power", turnStartDraw: 1, turnStartDiscard: 1 },
|
||||||
|
Hit1: { name: "타격1", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Hit2: { name: "타격2", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Tool", "Pass1", "Pass2", "Pass3", "Hit1", "Hit2"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chooseAction: GrandFinale is blocked until draw pile is empty", () => {
|
||||||
|
const cards = {
|
||||||
|
Finale: { name: "피날레", cost: 0, kind: "Attack", damage: 60, playableWhenDrawPileEmpty: true },
|
||||||
|
Defend: { name: "방어", cost: 1, kind: "Skill", block: 5 },
|
||||||
|
};
|
||||||
|
assert.equal(chooseAction(["Finale", "Defend"], cards, 3, { drawPileCount: 1 }), 1);
|
||||||
|
assert.equal(chooseAction(["Finale"], cards, 3, { drawPileCount: 0 }), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerAttackPlayedThisTurn scales Finisher", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Hit: { name: "타격", cost: 0, kind: "Attack", damage: 6 },
|
||||||
|
Finisher: { name: "마무리", cost: 0, kind: "Attack", damage: 0, damagePerAttackPlayedThisTurn: 6 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Hit", "Finisher"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 12, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerOtherHandCard and damagePerSkillInHand are applied", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Precise: { name: "정밀", cost: 0, kind: "Attack", damage: 13, damagePerOtherHandCard: -2 },
|
||||||
|
Flechettes: { name: "프레췌", cost: 0, kind: "Attack", damage: 0, damagePerSkillInHand: 5 },
|
||||||
|
Skill1: { name: "스킬1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Skill2: { name: "스킬2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Blank: { name: "공백", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Skill1", "Skill2", "Blank", "Precise", "Flechettes"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 21, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerDiscardedThisTurn and bonusHitsWhenOtherHandAtLeast work", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Toss: { name: "버리기", cost: 0, kind: "Skill", discard: 1 },
|
||||||
|
Memento: { name: "메멘토", cost: 0, kind: "Attack", damage: 9, damagePerDiscardedThisTurn: 4 },
|
||||||
|
Follow: { name: "완수", cost: 0, kind: "Attack", damage: 7, otherHandAtLeast: 2, bonusHitsWhenOtherHandAtLeast: 1 },
|
||||||
|
Blank1: { name: "공백1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Blank2: { name: "공백2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Toss", "Memento", "Follow", "Blank1", "Blank2"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 27, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: gainEnergy, drawUntilHandSize, and drawPerDiscarded are applied", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Adrenaline: { name: "Adrenaline", cost: 0, kind: "Skill", gainEnergy: 1, draw: 2, exhaust: true },
|
||||||
|
Expertise: { name: "Expertise", cost: 1, kind: "Skill", drawUntilHandSize: 6 },
|
||||||
|
Gamble: { name: "Gamble", cost: 0, kind: "Skill", discardAll: true, drawPerDiscarded: 1, exhaust: true },
|
||||||
|
Tactician: { name: "Tactician", cost: 99, kind: "Skill", gainEnergy: 1, sly: true },
|
||||||
|
Hit1: { name: "Hit1", cost: 1, kind: "Attack", damage: 6 },
|
||||||
|
Hit2: { name: "Hit2", cost: 1, kind: "Attack", damage: 6 },
|
||||||
|
Hit3: { name: "Hit3", cost: 1, kind: "Attack", damage: 6 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Adrenaline", "Expertise", "Gamble", "Tactician", "Hit1", "Hit2", "Hit3"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 18, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: cardPlayedBlock grants block whenever a card is played", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
After: { name: "Afterimage", cost: 1, kind: "Power", cardPlayedBlock: 1 },
|
||||||
|
Hit: { name: "Hit", cost: 1, kind: "Attack", damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["After", "Hit", "Hit", "Hit", "Hit"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 1 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: blockGainMultiplier doubles block gain for the turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Shadow: { name: "Shadowmeld", cost: 1, kind: "Skill", block: 5, blockGainMultiplier: 2 },
|
||||||
|
Shield: { name: "Shield", cost: 1, kind: "Skill", block: 2 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Shadow", "Shield"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 8 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextSkillCostZero makes the next skill free", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Pounce: { name: "Pounce", cost: 2, kind: "Attack", damage: 12, nextSkillCostZero: true },
|
||||||
|
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Pounce", "Guard"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 8 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextSkillRepeatCount repeats the next skill effect", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
Burst: { name: "Burst", cost: 1, kind: "Skill", draw: 1, block: 5, nextSkillRepeatCount: 1 },
|
||||||
|
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Burst", "Guard"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 15 }] }],
|
||||||
|
};
|
||||||
|
const withBurst = simulateCombat(shared, () => 0.999999);
|
||||||
|
const withoutBurst = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
Burst: { name: "Burst", cost: 1, kind: "Skill", draw: 1, block: 5 },
|
||||||
|
Guard: shared.cards.Guard,
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
assert.equal(withBurst.draw, true);
|
||||||
|
assert.equal(withBurst.playerHpRemaining, 80);
|
||||||
|
assert.ok(withBurst.playerHpRemaining > withoutBurst.playerHpRemaining);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chooseAction: skillCostReductionThisTurn allows discounted skills", () => {
|
||||||
|
const cards = {
|
||||||
|
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
|
||||||
|
};
|
||||||
|
assert.equal(chooseAction(["Guard"], cards, 1, { skillCostReductionThisTurn: 1 }), 0);
|
||||||
|
assert.equal(chooseAction(["Guard"], cards, 1, {}), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chooseAction: handCostZeroThisTurn lets expensive cards be played", () => {
|
||||||
|
const cards = {
|
||||||
|
Burst: { name: "Burst", cost: 3, kind: "Skill", block: 8 },
|
||||||
|
};
|
||||||
|
assert.equal(chooseAction(["Burst"], cards, 0, { handCostZeroThisTurn: true }), 0);
|
||||||
|
assert.equal(chooseAction(["Burst"], cards, 0, {}), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chooseAction: useAllEnergy cards remain playable at zero energy", () => {
|
||||||
|
const cards = {
|
||||||
|
Skewer: { name: "Skewer", cost: 2, kind: "Attack", useAllEnergy: true, xDamagePerEnergy: 8 },
|
||||||
|
};
|
||||||
|
assert.equal(chooseAction(["Skewer"], cards, 0, {}), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chooseAction: combatCardCostReduction discounts the same card across combat", () => {
|
||||||
|
const cards = {
|
||||||
|
Sleeve: { name: "UpMySleeve", cost: 2, kind: "Skill" },
|
||||||
|
};
|
||||||
|
assert.equal(chooseAction(["Sleeve"], cards, 1, { combatCardCostReduction: { Sleeve: 1 } }), 0);
|
||||||
|
assert.equal(chooseAction(["Sleeve"], cards, 1, {}), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: drawSkillBlock grants block for each drawn skill", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Escape: { name: "EscapePlan", cost: 0, kind: "Skill", draw: 1, drawSkillBlock: 3, innate: true, exhaust: true },
|
||||||
|
Filler1: { name: "Filler1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler2: { name: "Filler2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler3: { name: "Filler3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler4: { name: "Filler4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler5: { name: "Filler5", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Escape", "Filler1", "Filler2", "Filler3", "Filler4", "Filler5"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const stats = {};
|
||||||
|
const r = simulateCombat(data, () => 0.999999, stats);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(stats.Escape.block, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: poisonPerTurn powers poison all enemies at turn start", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Fumes: { name: "NoxiousFumes", cost: 1, kind: "Power", powerEffect: "poisonPerTurn", value: 2 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Fumes"],
|
||||||
|
monsters: [
|
||||||
|
{ name: "DummyA", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
{ name: "DummyB", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerTurn powers damage all enemies at turn start", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Speed: { name: "Speedster", cost: 2, kind: "Power", powerEffect: "damagePerTurn", value: 2 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Speed"],
|
||||||
|
monsters: [
|
||||||
|
{ name: "DummyA", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
{ name: "DummyB", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: attackPoison power applies poison on attack damage", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Venom: { name: "Envenom", cost: 2, kind: "Power", attackPoison: 2 },
|
||||||
|
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Venom", "Strike"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: skillSlyOnPlay makes later discards of the same skill trigger sly effects", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
MasterPlanner: { name: "MasterPlanner", cost: 1, kind: "Skill", poison: 1, discardAll: true },
|
||||||
|
},
|
||||||
|
starterDeck: ["MasterPlanner", "MasterPlanner"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const withSly = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
MasterPlanner: { name: "MasterPlanner", cost: 1, kind: "Skill", poison: 1, discardAll: true, skillSlyOnPlay: true },
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
const withoutSly = simulateCombat(shared, () => 0.999999);
|
||||||
|
assert.equal(withSly.win, true);
|
||||||
|
assert.equal(withSly.turns, 1);
|
||||||
|
assert.ok(withoutSly.turns > withSly.turns);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: randomTargetEachHit can spread hits across alive enemies", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
Ricochet: { name: "Ricochet", cost: 2, kind: "Attack", damage: 3, hits: 4, randomTargetEachHit: true },
|
||||||
|
},
|
||||||
|
starterDeck: ["Ricochet"],
|
||||||
|
monsters: [
|
||||||
|
{ name: "DummyA", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
{ name: "DummyB", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const makeRng = () => {
|
||||||
|
const seq = [0, 0.999999, 0, 0.999999];
|
||||||
|
let i = 0;
|
||||||
|
return () => seq[i++ % seq.length];
|
||||||
|
};
|
||||||
|
const withRicochet = simulateCombat(shared, makeRng());
|
||||||
|
const withoutRicochet = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
Ricochet: { name: "Ricochet", cost: 2, kind: "Attack", damage: 3, hits: 4 },
|
||||||
|
},
|
||||||
|
}, makeRng());
|
||||||
|
assert.equal(withRicochet.win, true);
|
||||||
|
assert.equal(withRicochet.turns, 1);
|
||||||
|
assert.equal(withoutRicochet.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calcEnemyAttack: enemyStrengthLossThisTurn reduces enemy attack damage", () => {
|
||||||
|
assert.equal(calcEnemyAttack(10, 6, 0, 0, 6), 10);
|
||||||
|
assert.equal(calcEnemyAttack(10, 6, 0, 0, 0), 16);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calcEnemyAttack: 힘 손실이 base 아래로 공격을 낮춘다 (음수 힘, Lua 동기화)", () => {
|
||||||
|
// 적 str=0, loss=6 → 힘 -6 → 10-6=4. JS가 str을 0에서 클램프하면 10(버그). Lua는 전체에서 차감.
|
||||||
|
assert.equal(calcEnemyAttack(10, 0, 0, 0, 6), 4);
|
||||||
|
assert.equal(calcEnemyAttack(10, 3, 0, 0, 6), 7);
|
||||||
|
assert.equal(calcEnemyAttack(5, 0, 0, 0, 6), 0); // 5-6=-1 → 0 클램프
|
||||||
|
});
|
||||||
|
|
||||||
|
test('simulateCombat: firstShivDamageBonus는 턴당 첫 Shiv에만 적용 (Lua 동기화)', () => {
|
||||||
|
// PhantomBlades(firstShivDamageBonus 9) 활성. 턴당 3 Shiv 사용(에너지3·cost1).
|
||||||
|
// 정답(첫 Shiv만 +9): 턴1 = 10+1+1=12 → 13HP에 1 남김 → 2턴.
|
||||||
|
// 버그(모든 Shiv +9): 턴1 = 10*3=30 → 1턴.
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
PhantomBlades: { name: '환영검', cost: 0, kind: 'Power', firstShivDamageBonus: 9 },
|
||||||
|
Shiv: { name: '시브', cost: 1, kind: 'Attack', class: 'shiv', damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ['PhantomBlades', 'Shiv', 'Shiv', 'Shiv', 'Shiv'],
|
||||||
|
monsters: [{ name: '적', maxHp: 13, intents: [{ kind: 'Attack', value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, mulberry32(1));
|
||||||
|
assert.equal(r.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('simulateCombat: blockPerDamageDealtThisTurn이 실제 방어를 부여 (Lua 동기화)', () => {
|
||||||
|
// 매턴 Hit(5뎀) → Guard(준 피해만큼 방어 5) → 적 공격 5 상쇄.
|
||||||
|
// 수정(실제 방어): 무한 생존 → 무승부. 버그(방어 미부여): 매턴 5피해 → 사망.
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Hit: { name: '타격', cost: 2, kind: 'Attack', damage: 5 },
|
||||||
|
Guard: { name: '대비', cost: 1, kind: 'Skill', blockPerDamageDealtThisTurn: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ['Hit', 'Guard'],
|
||||||
|
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 5 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, mulberry32(1));
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: repeatOnKill repeats an attack until no kill occurs", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
EchoingSlash: { name: "EchoingSlash", cost: 1, kind: "Attack", aoe: true, damage: 10, repeatOnKill: true },
|
||||||
|
},
|
||||||
|
starterDeck: ["EchoingSlash"],
|
||||||
|
monsters: [
|
||||||
|
{ name: "DummyA", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
{ name: "DummyB", maxHp: 20, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const withRepeat = simulateCombat(shared, () => 0.999999);
|
||||||
|
const withoutRepeat = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
EchoingSlash: { name: "EchoingSlash", cost: 1, kind: "Attack", aoe: true, damage: 10 },
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
assert.equal(withRepeat.win, true);
|
||||||
|
assert.equal(withRepeat.turns, 1);
|
||||||
|
assert.equal(withoutRepeat.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: poisonIfTargetPoisoned only applies poison to already poisoned enemies", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
Bubble: { name: "BubbleBubble", cost: 1, kind: "Skill", poison: 9, poisonIfTargetPoisoned: true },
|
||||||
|
},
|
||||||
|
starterDeck: ["Bubble"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const withBubble = simulateCombat(shared, () => 0.999999);
|
||||||
|
const withoutBubble = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
Bubble: { name: "BubbleBubble", cost: 1, kind: "Skill", poison: 9 },
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
assert.equal(withBubble.draw, true);
|
||||||
|
assert.equal(withBubble.turns, 100);
|
||||||
|
assert.equal(withoutBubble.win, true);
|
||||||
|
assert.equal(withoutBubble.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: turnHandSlyCount marks a skill in hand as sly for the turn", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
HandTrick: { name: "HandTrick", cost: 0, kind: "Skill", block: 7, turnHandSlyCount: 1 },
|
||||||
|
Shield: { name: "Shield", cost: 0, kind: "Skill", unplayable: true, block: 7 },
|
||||||
|
Gamble: { name: "Gamble", cost: 0, kind: "Skill", discardAll: true },
|
||||||
|
},
|
||||||
|
starterDeck: ["Gamble", "Shield", "HandTrick"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 10 }] }],
|
||||||
|
};
|
||||||
|
const withHandTrick = simulateCombat(shared, () => 0.999999);
|
||||||
|
const withoutHandTrick = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
HandTrick: { name: "HandTrick", cost: 0, kind: "Skill", block: 7 },
|
||||||
|
Shield: shared.cards.Shield,
|
||||||
|
Gamble: shared.cards.Gamble,
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
assert.equal(withHandTrick.playerHpRemaining, 80);
|
||||||
|
assert.equal(withoutHandTrick.playerHpRemaining, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: extraPoisonTicks adds an extra poison tick at enemy turn start", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
Accelerant: { name: "Accelerant", cost: 1, kind: "Power", extraPoisonTicks: 1 },
|
||||||
|
Poison: { name: "Poison", cost: 1, kind: "Skill", poison: 2 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Accelerant", "Poison"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const withTick = simulateCombat(shared, () => 0.999999);
|
||||||
|
const withoutTick = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
Accelerant: { name: "Accelerant", cost: 1, kind: "Power" },
|
||||||
|
Poison: shared.cards.Poison,
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
assert.equal(withTick.win, true);
|
||||||
|
assert.equal(withTick.turns, 1);
|
||||||
|
assert.equal(withoutTick.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: poisonApplicationBurstEvery bursts after every third poison application", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
Outbreak: { name: "Outbreak", cost: 1, kind: "Power", poisonApplicationBurstEvery: 3, poisonApplicationBurstDamage: 11 },
|
||||||
|
Poison1: { name: "Poison1", cost: 0, kind: "Skill", poison: 1 },
|
||||||
|
Poison2: { name: "Poison2", cost: 0, kind: "Skill", poison: 1 },
|
||||||
|
Poison3: { name: "Poison3", cost: 0, kind: "Skill", poison: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Outbreak", "Poison1", "Poison2", "Poison3"],
|
||||||
|
monsters: [
|
||||||
|
{ name: "DummyA", maxHp: 11, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
{ name: "DummyB", maxHp: 11, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const withBurst = simulateCombat(shared, () => 0.999999);
|
||||||
|
const withoutBurst = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
Outbreak: { name: "Outbreak", cost: 1, kind: "Power" },
|
||||||
|
Poison1: shared.cards.Poison1,
|
||||||
|
Poison2: shared.cards.Poison2,
|
||||||
|
Poison3: shared.cards.Poison3,
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
assert.equal(withBurst.win, true);
|
||||||
|
assert.equal(withBurst.turns, 1);
|
||||||
|
assert.ok(withoutBurst.turns > withBurst.turns);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: firstCardDamageBonus applies on the first card played this turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Strangle: { name: "Strangle", cost: 1, kind: "Attack", damage: 8, firstCardDamageBonus: 2 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Strangle"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: blockPerDamageDealtThisTurn grants block from damage dealt this turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Mirage: { name: "Mirage", cost: 1, kind: "Skill", blockPerDamageDealtThisTurn: 1, block: 0 },
|
||||||
|
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 4 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Strike", "Mirage"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 4, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: cardPlayedRandomDamage hits a random enemy on card play", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
SerpentForm: { name: "SerpentForm", cost: 3, kind: "Power", cardPlayedRandomDamage: 4 },
|
||||||
|
},
|
||||||
|
starterDeck: ["SerpentForm"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 4, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: rewardOnKill grants an extra reward screen when an attack kills", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
TheHunt: { name: "TheHunt", cost: 1, kind: "Attack", damage: 10, rewardOnKill: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["TheHunt"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.bonusRewardScreens, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: intangible cards reduce incoming damage and persist across turns", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Wraith: { name: "WraithForm", cost: 3, kind: "Power", intangible: 2, endTurnDexLoss: 1, innate: true },
|
||||||
|
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Wraith", "Strike"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 1, intents: [{ kind: "Attack", value: 10 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: useAllEnergy skewer consumes all energy for damage", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Skewer: { name: "Skewer", cost: 2, kind: "Attack", useAllEnergy: true, xDamagePerEnergy: 8 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Skewer"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 24, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: useAllEnergy malaise scales weak with energy spent", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Malaise: { name: "Malaise", cost: 2, kind: "Skill", useAllEnergy: true, xWeakPerEnergy: 1 },
|
||||||
|
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Malaise", "Strike"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 1, intents: [{ kind: "Attack", value: 10 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerCardDrawnThisCombat scales murder", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Murder: { name: "Murder", cost: 3, kind: "Attack", damage: 1, damagePerCardDrawnThisCombat: 1 },
|
||||||
|
Filler1: { name: "Filler1", cost: 99, kind: "Skill" },
|
||||||
|
Filler2: { name: "Filler2", cost: 99, kind: "Skill" },
|
||||||
|
Filler3: { name: "Filler3", cost: 99, kind: "Skill" },
|
||||||
|
Filler4: { name: "Filler4", cost: 99, kind: "Skill" },
|
||||||
|
Filler5: { name: "Filler5", cost: 99, kind: "Skill" },
|
||||||
|
},
|
||||||
|
starterDeck: ["Murder", "Filler1", "Filler2", "Filler3", "Filler4", "Filler5"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const stats = {};
|
||||||
|
const r = simulateCombat(data, () => 0.999999, stats);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.ok(stats.Murder.damage > 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: shiv damage bonuses stack and first Shiv bonus applies once per turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Accuracy: { name: "Accuracy", cost: 1, kind: "Power", shivDamageBonus: 2 },
|
||||||
|
PhantomBlades: { name: "PhantomBlades", cost: 1, kind: "Power", firstShivDamageBonus: 3 },
|
||||||
|
Shiv: { name: "Shiv", cost: 0, kind: "Attack", class: "shiv", damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Accuracy", "PhantomBlades", "Shiv"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: shivAoe makes Shivs hit all enemies", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
FanOfKnives: { name: "FanOfKnives", cost: 2, kind: "Skill", addShiv: 2, shivAoe: true },
|
||||||
|
Accuracy: { name: "Accuracy", cost: 1, kind: "Power", shivDamageBonus: 2 },
|
||||||
|
Shiv: { name: "Shiv", cost: 0, kind: "Attack", class: "shiv", damage: 1 },
|
||||||
|
Pass: { name: "Pass", cost: 99, kind: "Skill" },
|
||||||
|
},
|
||||||
|
starterDeck: ["Accuracy", "FanOfKnives", "Pass"],
|
||||||
|
monsters: [
|
||||||
|
{ name: "A", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
{ name: "B", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
{ name: "C", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|||||||
629
tools/cards/cards_excel.ps1
Normal file
629
tools/cards/cards_excel.ps1
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true, Position = 0)]
|
||||||
|
[ValidateSet('export', 'import')]
|
||||||
|
[string]$Action,
|
||||||
|
[string]$JsonPath,
|
||||||
|
[string]$XlsxPath,
|
||||||
|
[string]$OutJsonPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
Add-Type -AssemblyName System.IO.Compression
|
||||||
|
|
||||||
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||||
|
if ([string]::IsNullOrWhiteSpace($JsonPath)) { $JsonPath = Join-Path $repoRoot 'data\cards.json' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($XlsxPath)) { $XlsxPath = Join-Path $repoRoot 'data\cards.xlsx' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($OutJsonPath)) { $OutJsonPath = $JsonPath }
|
||||||
|
|
||||||
|
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||||
|
|
||||||
|
function Escape-Xml([string]$Text) {
|
||||||
|
if ($null -eq $Text) { return '' }
|
||||||
|
return [System.Security.SecurityElement]::Escape($Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ColumnName([int]$Index) {
|
||||||
|
$n = $Index
|
||||||
|
$name = ''
|
||||||
|
while ($n -gt 0) {
|
||||||
|
$n--
|
||||||
|
$name = [char][int](65 + ($n % 26)) + $name
|
||||||
|
$n = [math]::Floor($n / 26)
|
||||||
|
}
|
||||||
|
return $name
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ColumnIndex([string]$Name) {
|
||||||
|
$n = 0
|
||||||
|
foreach ($ch in $Name.ToCharArray()) {
|
||||||
|
if ($ch -match '[A-Z]') {
|
||||||
|
$n = $n * 26 + ([int][char]$ch - 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $n
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CellRef([int]$Col, [int]$Row) {
|
||||||
|
return (Get-ColumnName $Col) + $Row
|
||||||
|
}
|
||||||
|
|
||||||
|
function Has-MapKey($Map, $Key) {
|
||||||
|
if ($null -eq $Map) { return $false }
|
||||||
|
if ($null -eq $Key) { return $false }
|
||||||
|
if ($Key -is [string] -and [string]::IsNullOrWhiteSpace($Key)) { return $false }
|
||||||
|
foreach ($existingKey in $Map.Keys) {
|
||||||
|
if ($existingKey -eq $Key) { return $true }
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ScalarType($Value) {
|
||||||
|
if ($null -eq $Value) { return 'null' }
|
||||||
|
if ($Value -is [bool]) { return 'boolean' }
|
||||||
|
if ($Value -is [byte] -or $Value -is [sbyte] -or
|
||||||
|
$Value -is [int16] -or $Value -is [uint16] -or
|
||||||
|
$Value -is [int32] -or $Value -is [uint32] -or
|
||||||
|
$Value -is [int64] -or $Value -is [uint64] -or
|
||||||
|
$Value -is [single] -or $Value -is [double] -or $Value -is [decimal]) { return 'number' }
|
||||||
|
if ($Value -is [string]) { return 'string' }
|
||||||
|
return 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CardSchema($Cards) {
|
||||||
|
$schema = [ordered]@{}
|
||||||
|
foreach ($cardEntry in $Cards.PSObject.Properties) {
|
||||||
|
$card = $cardEntry.Value
|
||||||
|
foreach ($prop in $card.PSObject.Properties) {
|
||||||
|
$kind = Get-ScalarType $prop.Value
|
||||||
|
if (-not (Has-MapKey $schema $prop.Name)) {
|
||||||
|
$schema[$prop.Name] = $kind
|
||||||
|
} elseif ($schema[$prop.Name] -ne $kind -and $kind -ne 'null') {
|
||||||
|
$schema[$prop.Name] = 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $schema
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ColumnWidth([string]$Header, [string]$Type) {
|
||||||
|
switch ($Header) {
|
||||||
|
'id' { return 18 }
|
||||||
|
'name' { return 24 }
|
||||||
|
'desc' { return 48 }
|
||||||
|
'image' { return 36 }
|
||||||
|
'fx' { return 36 }
|
||||||
|
'kind' { return 12 }
|
||||||
|
'class' { return 12 }
|
||||||
|
'rarity' { return 12 }
|
||||||
|
default {
|
||||||
|
if ($Type -eq 'boolean') { return 10 }
|
||||||
|
if ($Type -eq 'number') { return 12 }
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function To-InvariantNumber($Value) {
|
||||||
|
return [string]::Format([System.Globalization.CultureInfo]::InvariantCulture, '{0}', $Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-HeaderCellXml([string]$Ref, [string]$Text) {
|
||||||
|
$escaped = Escape-Xml $Text
|
||||||
|
return "<c r=""$Ref"" s=""1"" t=""inlineStr""><is><t xml:space=""preserve"">$escaped</t></is></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-TextCellXml([string]$Ref, [string]$Text) {
|
||||||
|
$escaped = Escape-Xml $Text
|
||||||
|
return "<c r=""$Ref"" t=""inlineStr""><is><t xml:space=""preserve"">$escaped</t></is></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-NumberCellXml([string]$Ref, $Value) {
|
||||||
|
if ($null -eq $Value) { return $null }
|
||||||
|
if ($Value -is [string] -and $Value -eq '') { return $null }
|
||||||
|
return "<c r=""$Ref""><v>$(To-InvariantNumber $Value)</v></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-BoolCellXml([string]$Ref, $Value) {
|
||||||
|
if ($null -eq $Value) { return $null }
|
||||||
|
if ($Value -is [string] -and $Value -eq '') { return $null }
|
||||||
|
$bool = $false
|
||||||
|
if ($Value -is [bool]) {
|
||||||
|
$bool = $Value
|
||||||
|
} else {
|
||||||
|
$text = [string]$Value
|
||||||
|
if ($text -match '^(?i:true|1|yes|y)$') { $bool = $true }
|
||||||
|
elseif ($text -match '^(?i:false|0|no|n)$') { $bool = $false }
|
||||||
|
else { return $null }
|
||||||
|
}
|
||||||
|
$n = if ($bool) { 1 } else { 0 }
|
||||||
|
return "<c r=""$Ref"" t=""b""><v>$n</v></c>"
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-CellXml([string]$Ref, $Value, [string]$Type) {
|
||||||
|
switch ($Type) {
|
||||||
|
'number' { return New-NumberCellXml $Ref $Value }
|
||||||
|
'boolean' { return New-BoolCellXml $Ref $Value }
|
||||||
|
default {
|
||||||
|
if ($null -eq $Value) {
|
||||||
|
return New-TextCellXml $Ref ''
|
||||||
|
}
|
||||||
|
return New-TextCellXml $Ref ([string]$Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WorksheetXml([string]$SheetName, [string[]]$Headers, [object[]]$Rows, [hashtable]$TypeMap) {
|
||||||
|
$maxCol = $Headers.Count
|
||||||
|
$lastCol = Get-ColumnName $maxCol
|
||||||
|
$rowCount = $Rows.Count + 1
|
||||||
|
$colsXml = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 0; $i -lt $Headers.Count; $i++) {
|
||||||
|
$header = $Headers[$i]
|
||||||
|
$type = if (Has-MapKey $TypeMap $header) { [string]$TypeMap[$header] } else { 'string' }
|
||||||
|
$width = Get-ColumnWidth $header $type
|
||||||
|
$colsXml.Add("<col min=""$($i + 1)"" max=""$($i + 1)"" width=""$width"" customWidth=""1"" />")
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowsXml = New-Object System.Collections.Generic.List[string]
|
||||||
|
$headerCells = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 0; $i -lt $Headers.Count; $i++) {
|
||||||
|
$headerCells.Add((New-HeaderCellXml (Get-CellRef ($i + 1) 1) $Headers[$i]))
|
||||||
|
}
|
||||||
|
$rowsXml.Add("<row r=""1"" spans=""1:$maxCol"" ht=""20"" customHeight=""1"">$($headerCells -join '')</row>")
|
||||||
|
|
||||||
|
for ($r = 0; $r -lt $Rows.Count; $r++) {
|
||||||
|
$row = $Rows[$r]
|
||||||
|
$cells = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($c = 0; $c -lt $Headers.Count; $c++) {
|
||||||
|
$header = $Headers[$c]
|
||||||
|
$type = if (Has-MapKey $TypeMap $header) { [string]$TypeMap[$header] } else { 'string' }
|
||||||
|
$value = $null
|
||||||
|
if (Has-MapKey $row $header) { $value = $row[$header] }
|
||||||
|
$cellXml = New-CellXml (Get-CellRef ($c + 1) ($r + 2)) $value $type
|
||||||
|
if ($null -ne $cellXml) { $cells.Add($cellXml) }
|
||||||
|
}
|
||||||
|
$rowsXml.Add("<row r=""$($r + 2)"" spans=""1:$maxCol"">$($cells -join '')</row>")
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheetView = '<sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/><selection pane="bottomLeft" activeCell="A2" sqref="A2"/></sheetView></sheetViews>'
|
||||||
|
$cols = '<cols>' + ($colsXml -join '') + '</cols>'
|
||||||
|
$sheetData = '<sheetData>' + ($rowsXml -join '') + '</sheetData>'
|
||||||
|
$autoFilter = "<autoFilter ref=""A1:$lastCol$rowCount""/>"
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||||
|
$sheetView
|
||||||
|
<sheetFormatPr defaultRowHeight="18"/>
|
||||||
|
$cols
|
||||||
|
$sheetData
|
||||||
|
$autoFilter
|
||||||
|
<pageMargins left="0.25" right="0.25" top="0.5" bottom="0.5" header="0.3" footer="0.3"/>
|
||||||
|
</worksheet>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-StylesXml {
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||||
|
<fonts count="2">
|
||||||
|
<font>
|
||||||
|
<sz val="11"/>
|
||||||
|
<color rgb="FF000000"/>
|
||||||
|
<name val="Calibri"/>
|
||||||
|
<family val="2"/>
|
||||||
|
<scheme val="minor"/>
|
||||||
|
</font>
|
||||||
|
<font>
|
||||||
|
<b/>
|
||||||
|
<sz val="11"/>
|
||||||
|
<color rgb="FFFFFFFF"/>
|
||||||
|
<name val="Calibri"/>
|
||||||
|
<family val="2"/>
|
||||||
|
<scheme val="minor"/>
|
||||||
|
</font>
|
||||||
|
</fonts>
|
||||||
|
<fills count="2">
|
||||||
|
<fill><patternFill patternType="none"/></fill>
|
||||||
|
<fill><patternFill patternType="solid"><fgColor rgb="FF2D3748"/><bgColor indexed="64"/></patternFill></fill>
|
||||||
|
</fills>
|
||||||
|
<borders count="1">
|
||||||
|
<border>
|
||||||
|
<left/><right/><top/><bottom/><diagonal/>
|
||||||
|
</border>
|
||||||
|
</borders>
|
||||||
|
<cellStyleXfs count="1">
|
||||||
|
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
|
||||||
|
</cellStyleXfs>
|
||||||
|
<cellXfs count="2">
|
||||||
|
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
|
||||||
|
<xf numFmtId="0" fontId="1" fillId="1" borderId="0" xfId="0" applyFont="1" applyFill="1"/>
|
||||||
|
</cellXfs>
|
||||||
|
<cellStyles count="1">
|
||||||
|
<cellStyle name="Normal" xfId="0" builtinId="0"/>
|
||||||
|
</cellStyles>
|
||||||
|
</styleSheet>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WorkbookXml([string[]]$SheetNames) {
|
||||||
|
$sheetsXml = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 0; $i -lt $SheetNames.Count; $i++) {
|
||||||
|
$sheetsXml.Add("<sheet name=""$(Escape-Xml $SheetNames[$i])"" sheetId=""$($i + 1)"" r:id=""rId$($i + 1)""/>")
|
||||||
|
}
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||||
|
<sheets>
|
||||||
|
$($sheetsXml -join '')
|
||||||
|
</sheets>
|
||||||
|
</workbook>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WorkbookRelsXml([int]$SheetCount) {
|
||||||
|
$rels = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 1; $i -le $SheetCount; $i++) {
|
||||||
|
$rels.Add("<Relationship Id=""rId$i"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"" Target=""worksheets/sheet$i.xml""/>")
|
||||||
|
}
|
||||||
|
$rels.Add("<Relationship Id=""rId$($SheetCount + 1)"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"" Target=""styles.xml""/>")
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
$($rels -join '')
|
||||||
|
</Relationships>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RootRelsXml {
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||||
|
</Relationships>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ContentTypesXml([int]$SheetCount) {
|
||||||
|
$overrides = New-Object System.Collections.Generic.List[string]
|
||||||
|
for ($i = 1; $i -le $SheetCount; $i++) {
|
||||||
|
$overrides.Add("<Override PartName=""/xl/worksheets/sheet$i.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml""/>")
|
||||||
|
}
|
||||||
|
$overrides.Add('<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>')
|
||||||
|
$overrides.Add('<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>')
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="xml" ContentType="application/xml"/>
|
||||||
|
$($overrides -join '')
|
||||||
|
</Types>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Xlsx([string]$Path, [hashtable]$Parts) {
|
||||||
|
$dir = Split-Path -Parent $Path
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($dir) -and -not (Test-Path $dir)) {
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
}
|
||||||
|
if (Test-Path $Path) {
|
||||||
|
Remove-Item -LiteralPath $Path -Force
|
||||||
|
}
|
||||||
|
$file = [System.IO.File]::Open($Path, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite)
|
||||||
|
try {
|
||||||
|
$zip = New-Object System.IO.Compression.ZipArchive($file, [System.IO.Compression.ZipArchiveMode]::Create, $false)
|
||||||
|
try {
|
||||||
|
foreach ($entryName in $Parts.Keys) {
|
||||||
|
$entry = $zip.CreateEntry($entryName)
|
||||||
|
$stream = $entry.Open()
|
||||||
|
$writer = New-Object System.IO.StreamWriter($stream, $utf8NoBom)
|
||||||
|
try {
|
||||||
|
$writer.Write([string]$Parts[$entryName])
|
||||||
|
} finally {
|
||||||
|
$writer.Dispose()
|
||||||
|
$stream.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$zip.Dispose()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$file.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-XlsxXml([string]$Path, [string]$EntryName) {
|
||||||
|
$zip = [System.IO.Compression.ZipFile]::OpenRead($Path)
|
||||||
|
try {
|
||||||
|
$entry = $zip.GetEntry($EntryName)
|
||||||
|
if ($null -eq $entry) { throw "Missing XLSX entry: $EntryName" }
|
||||||
|
$stream = $entry.Open()
|
||||||
|
try {
|
||||||
|
$reader = New-Object System.IO.StreamReader($stream, $utf8NoBom)
|
||||||
|
try { return $reader.ReadToEnd() } finally { $reader.Dispose() }
|
||||||
|
} finally {
|
||||||
|
$stream.Dispose()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$zip.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-SharedStrings([string]$Path) {
|
||||||
|
try {
|
||||||
|
$xmlText = Read-XlsxXml $Path 'xl/sharedStrings.xml'
|
||||||
|
} catch {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
[xml]$xml = $xmlText
|
||||||
|
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
|
||||||
|
$ns.AddNamespace('x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main')
|
||||||
|
$items = $xml.SelectNodes('/x:sst/x:si', $ns)
|
||||||
|
$values = New-Object System.Collections.Generic.List[string]
|
||||||
|
foreach ($item in $items) {
|
||||||
|
$values.Add([string]$item.InnerText)
|
||||||
|
}
|
||||||
|
return $values.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-WorksheetRows([string]$XmlText, [string[]]$SharedStrings) {
|
||||||
|
[xml]$xml = $XmlText
|
||||||
|
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
|
||||||
|
$ns.AddNamespace('x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main')
|
||||||
|
$rows = $xml.SelectNodes('/x:worksheet/x:sheetData/x:row', $ns)
|
||||||
|
$parsed = @()
|
||||||
|
foreach ($row in $rows) {
|
||||||
|
$cells = @{}
|
||||||
|
foreach ($cell in @($row.ChildNodes)) {
|
||||||
|
if ($cell.Name -ne 'c') { continue }
|
||||||
|
$ref = [string]$cell.Attributes['r'].Value
|
||||||
|
$col = Get-ColumnIndex (($ref -replace '\d+$', ''))
|
||||||
|
$type = [string]$cell.Attributes['t'].Value
|
||||||
|
$text = [string]$cell.InnerText
|
||||||
|
if ($type -eq 's' -and $text -match '^\d+$') {
|
||||||
|
$index = [int]$text
|
||||||
|
if ($index -ge 0 -and $index -lt $SharedStrings.Count) {
|
||||||
|
$text = [string]$SharedStrings[$index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$cells[$col] = $text
|
||||||
|
}
|
||||||
|
$parsed += ,$cells
|
||||||
|
}
|
||||||
|
return $parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-CellValue([string]$Text, [string]$Type) {
|
||||||
|
if ($null -eq $Text -or $Text -eq '') { return $null }
|
||||||
|
switch ($Type) {
|
||||||
|
'number' {
|
||||||
|
$num = 0
|
||||||
|
if ([double]::TryParse($Text, [System.Globalization.NumberStyles]::Any, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$num)) {
|
||||||
|
if ([math]::Abs($num - [math]::Round($num)) -lt 0.0000001) { return [int64][math]::Round($num) }
|
||||||
|
return $num
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
'boolean' {
|
||||||
|
if ($Text -match '^(?i:true|1|yes|y)$') { return $true }
|
||||||
|
if ($Text -match '^(?i:false|0|no|n)$') { return $false }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Export-Cards {
|
||||||
|
$source = Get-Content -LiteralPath $JsonPath -Raw -Encoding utf8 | ConvertFrom-Json
|
||||||
|
$schema = Get-CardSchema $source.cards
|
||||||
|
$cardCore = @('id', 'name', 'cost', 'kind', 'rarity', 'class', 'desc', 'image', 'fx')
|
||||||
|
$cardExtras = @($schema.Keys | Where-Object { $_ -notin $cardCore } | Sort-Object)
|
||||||
|
$cardHeaders = @($cardCore + $cardExtras)
|
||||||
|
|
||||||
|
$maxDeckSize = 0
|
||||||
|
foreach ($deckEntry in $source.starterDecks.PSObject.Properties) {
|
||||||
|
$deckSize = @($deckEntry.Value).Count
|
||||||
|
if ($deckSize -gt $maxDeckSize) {
|
||||||
|
$maxDeckSize = $deckSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($maxDeckSize -lt 1) { $maxDeckSize = 1 }
|
||||||
|
|
||||||
|
$starterDeckHeaders = New-Object System.Collections.Generic.List[string]
|
||||||
|
$starterDeckHeaders.Add('class')
|
||||||
|
for ($i = 1; $i -le $maxDeckSize; $i++) {
|
||||||
|
$starterDeckHeaders.Add("slot$i")
|
||||||
|
}
|
||||||
|
|
||||||
|
$cardRows = New-Object System.Collections.Generic.List[object]
|
||||||
|
foreach ($cardEntry in $source.cards.PSObject.Properties) {
|
||||||
|
$cardId = $cardEntry.Name
|
||||||
|
$card = $cardEntry.Value
|
||||||
|
$row = [ordered]@{ id = $cardId }
|
||||||
|
foreach ($header in $cardHeaders) {
|
||||||
|
if ($header -eq 'id') { continue }
|
||||||
|
if ($card.PSObject.Properties.Name -contains $header) {
|
||||||
|
$row[$header] = $card.$header
|
||||||
|
} else {
|
||||||
|
$row[$header] = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$cardRows.Add($row)
|
||||||
|
}
|
||||||
|
|
||||||
|
$deckRows = New-Object System.Collections.Generic.List[object]
|
||||||
|
foreach ($deckEntry in $source.starterDecks.PSObject.Properties) {
|
||||||
|
$cls = $deckEntry.Name
|
||||||
|
$deck = @($deckEntry.Value)
|
||||||
|
$row = [ordered]@{ class = $cls }
|
||||||
|
for ($i = 1; $i -le $maxDeckSize; $i++) {
|
||||||
|
$key = "slot$i"
|
||||||
|
$row[$key] = if ($i -le $deck.Count) { $deck[$i - 1] } else { $null }
|
||||||
|
}
|
||||||
|
$deckRows.Add($row)
|
||||||
|
}
|
||||||
|
|
||||||
|
$cardSheet = Get-WorksheetXml 'Cards' $cardHeaders $cardRows $schema
|
||||||
|
$deckTypeMap = [ordered]@{ class = 'string' }
|
||||||
|
for ($i = 1; $i -le $maxDeckSize; $i++) { $deckTypeMap["slot$i"] = 'string' }
|
||||||
|
$deckSheet = Get-WorksheetXml 'StarterDecks' $starterDeckHeaders $deckRows $deckTypeMap
|
||||||
|
|
||||||
|
$parts = [ordered]@{
|
||||||
|
'[Content_Types].xml' = (Get-ContentTypesXml 2)
|
||||||
|
'_rels/.rels' = (Get-RootRelsXml)
|
||||||
|
'xl/workbook.xml' = (Get-WorkbookXml @('Cards', 'StarterDecks'))
|
||||||
|
'xl/_rels/workbook.xml.rels' = (Get-WorkbookRelsXml 2)
|
||||||
|
'xl/styles.xml' = (Get-StylesXml)
|
||||||
|
'xl/worksheets/sheet1.xml' = $cardSheet
|
||||||
|
'xl/worksheets/sheet2.xml' = $deckSheet
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Source JSON: $JsonPath"
|
||||||
|
Write-Host "Target XLSX: $XlsxPath"
|
||||||
|
Write-Xlsx $XlsxPath $parts
|
||||||
|
Write-Host "Excel export complete: $XlsxPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-Cards {
|
||||||
|
$source = Get-Content -LiteralPath $JsonPath -Raw -Encoding utf8 | ConvertFrom-Json
|
||||||
|
$schema = Get-CardSchema $source.cards
|
||||||
|
$origCardOrders = @{}
|
||||||
|
foreach ($cardEntry in $source.cards.PSObject.Properties) {
|
||||||
|
$origCardOrders[$cardEntry.Name] = @($cardEntry.Value.PSObject.Properties.Name)
|
||||||
|
}
|
||||||
|
$origDeckOrder = @($source.starterDecks.PSObject.Properties.Name)
|
||||||
|
|
||||||
|
$sharedStrings = Read-SharedStrings $XlsxPath
|
||||||
|
$cardsXml = Read-XlsxXml $XlsxPath 'xl/worksheets/sheet1.xml'
|
||||||
|
$deckXml = Read-XlsxXml $XlsxPath 'xl/worksheets/sheet2.xml'
|
||||||
|
$cardRowsRaw = Read-WorksheetRows $cardsXml $sharedStrings
|
||||||
|
$deckRowsRaw = Read-WorksheetRows $deckXml $sharedStrings
|
||||||
|
|
||||||
|
if ($cardRowsRaw.Count -lt 2) { throw 'Cards sheet has no data rows.' }
|
||||||
|
if ($deckRowsRaw.Count -lt 2) { throw 'StarterDecks sheet has no data rows.' }
|
||||||
|
|
||||||
|
$cardHeaderMap = $cardRowsRaw[0]
|
||||||
|
$cardHeaders = @($cardHeaderMap.Keys | Sort-Object)
|
||||||
|
$orderedCardHeaders = New-Object System.Collections.Generic.List[string]
|
||||||
|
foreach ($col in $cardHeaders) {
|
||||||
|
$header = $cardHeaderMap[$col]
|
||||||
|
if ([string]::IsNullOrWhiteSpace($header)) { continue }
|
||||||
|
$orderedCardHeaders.Add($header)
|
||||||
|
}
|
||||||
|
|
||||||
|
$newCards = [ordered]@{}
|
||||||
|
for ($r = 1; $r -lt $cardRowsRaw.Count; $r++) {
|
||||||
|
$row = $cardRowsRaw[$r]
|
||||||
|
$cardId = $null
|
||||||
|
$rowValues = @{}
|
||||||
|
for ($c = 0; $c -lt $orderedCardHeaders.Count; $c++) {
|
||||||
|
$header = $orderedCardHeaders[$c]
|
||||||
|
$text = $null
|
||||||
|
if (Has-MapKey $row ($c + 1)) { $text = $row[$c + 1] }
|
||||||
|
if ($header -eq 'id') {
|
||||||
|
$cardId = [string]$text
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$rowValues[$header] = $text
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($cardId)) {
|
||||||
|
$cardObj = [ordered]@{}
|
||||||
|
$fieldOrder = New-Object System.Collections.Generic.List[string]
|
||||||
|
if ($origCardOrders.ContainsKey($cardId)) {
|
||||||
|
foreach ($name in @($origCardOrders[$cardId])) {
|
||||||
|
if ($name -ne 'id' -and -not $fieldOrder.Contains($name)) {
|
||||||
|
$fieldOrder.Add($name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($name in $orderedCardHeaders) {
|
||||||
|
if ($name -ne 'id' -and -not $fieldOrder.Contains($name)) {
|
||||||
|
$fieldOrder.Add($name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($header in $fieldOrder) {
|
||||||
|
$text = $null
|
||||||
|
if (Has-MapKey $rowValues $header) { $text = $rowValues[$header] }
|
||||||
|
$type = if (Has-MapKey $schema $header) { [string]$schema[$header] } else { 'string' }
|
||||||
|
$value = Convert-CellValue $text $type
|
||||||
|
if ($null -eq $value) { continue }
|
||||||
|
$cardObj[$header] = $value
|
||||||
|
}
|
||||||
|
$newCards[$cardId] = $cardObj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$deckHeaderMap = $deckRowsRaw[0]
|
||||||
|
$deckHeaderCols = @($deckHeaderMap.Keys | Sort-Object)
|
||||||
|
$orderedDeckHeaders = New-Object System.Collections.Generic.List[string]
|
||||||
|
foreach ($col in $deckHeaderCols) {
|
||||||
|
$header = $deckHeaderMap[$col]
|
||||||
|
if ([string]::IsNullOrWhiteSpace($header)) { continue }
|
||||||
|
$orderedDeckHeaders.Add($header)
|
||||||
|
}
|
||||||
|
|
||||||
|
$newDecks = [ordered]@{}
|
||||||
|
for ($r = 1; $r -lt $deckRowsRaw.Count; $r++) {
|
||||||
|
$row = $deckRowsRaw[$r]
|
||||||
|
$cls = $null
|
||||||
|
$deckValues = @{}
|
||||||
|
for ($c = 0; $c -lt $orderedDeckHeaders.Count; $c++) {
|
||||||
|
$header = $orderedDeckHeaders[$c]
|
||||||
|
$text = $null
|
||||||
|
if (Has-MapKey $row ($c + 1)) { $text = $row[$c + 1] }
|
||||||
|
if ($header -eq 'class') {
|
||||||
|
$cls = [string]$text
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$deckValues[$header] = $text
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($cls)) {
|
||||||
|
$deck = New-Object System.Collections.Generic.List[string]
|
||||||
|
foreach ($header in $orderedDeckHeaders) {
|
||||||
|
if ($header -eq 'class') { continue }
|
||||||
|
$text = $null
|
||||||
|
if (Has-MapKey $deckValues $header) { $text = $deckValues[$header] }
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$text)) {
|
||||||
|
$deck.Add([string]$text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newDecks[$cls] = $deck.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($origDeckOrder.Count -gt 0) {
|
||||||
|
$orderedDecks = [ordered]@{}
|
||||||
|
foreach ($cls in $origDeckOrder) {
|
||||||
|
if (Has-MapKey $newDecks $cls) {
|
||||||
|
$orderedDecks[$cls] = $newDecks[$cls]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($entry in $newDecks.GetEnumerator()) {
|
||||||
|
if (-not (Has-MapKey $orderedDecks $entry.Key)) {
|
||||||
|
$orderedDecks[$entry.Key] = $entry.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newDecks = $orderedDecks
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [ordered]@{
|
||||||
|
cards = $newCards
|
||||||
|
starterDecks = $newDecks
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = $out | ConvertTo-Json -Depth 64
|
||||||
|
Write-Host "Source XLSX: $XlsxPath"
|
||||||
|
Write-Host "Target JSON: $OutJsonPath"
|
||||||
|
[System.IO.File]::WriteAllText($OutJsonPath, $json, $utf8NoBom)
|
||||||
|
Write-Host "JSON import complete: $OutJsonPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Action) {
|
||||||
|
'export' { Export-Cards }
|
||||||
|
'import' { Import-Cards }
|
||||||
|
}
|
||||||
@@ -1,15 +1,29 @@
|
|||||||
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 { 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 { 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';
|
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 = [
|
export const bootMethods = [
|
||||||
method('OnBeginPlay', `${luaCardsTable(CARDS.cards)}
|
method('OnBeginPlay', `${luaCardsTable(CARDS.cards)}
|
||||||
${luaFramesTable()}
|
${luaFramesTable()}
|
||||||
${luaNodeIconsTable()}
|
${luaNodeIconsTable()}
|
||||||
|
${luaCharsTable()}
|
||||||
${luaSoulShopTable(SOUL_UNLOCKS)}
|
${luaSoulShopTable(SOUL_UNLOCKS)}
|
||||||
self.SoulUnlocks = {}
|
self.SoulUnlocks = {}
|
||||||
self.SoulPoints = self.SoulPoints or 0
|
self.SoulPoints = self.SoulPoints or 0
|
||||||
self:ShowLobby()
|
local uiTries = 0
|
||||||
|
local uiInit = 0
|
||||||
|
uiInit = _TimerService:SetTimerRepeat(function()
|
||||||
|
uiTries = uiTries + 1
|
||||||
|
if _EntityService:GetEntityByPath("/ui/DeckUIGroup") ~= nil then
|
||||||
|
self:ActivateUIGroups()
|
||||||
|
-- MainMenu는 한동안 비활성화: 시작 시 바로 로비로 진입.
|
||||||
|
-- 추후 싱글/멀티/종료 선택 메뉴가 필요하면 self:ShowMainMenu()로 되돌린다(메서드·UI 유지됨).
|
||||||
|
self:ShowLobby()
|
||||||
|
_TimerService:ClearTimer(uiInit)
|
||||||
|
elseif uiTries > 80 then
|
||||||
|
_TimerService:ClearTimer(uiInit)
|
||||||
|
end
|
||||||
|
end, 0.1)
|
||||||
local lp = _UserService.LocalPlayer
|
local lp = _UserService.LocalPlayer
|
||||||
if lp ~= nil then
|
if lp ~= nil then
|
||||||
self:ReqLoadAscension(lp.PlayerComponent.UserId)
|
self:ReqLoadAscension(lp.PlayerComponent.UserId)
|
||||||
@@ -17,12 +31,38 @@ if lp ~= nil then
|
|||||||
end
|
end
|
||||||
_InputService:ConnectEvent(KeyDownEvent, function(e)
|
_InputService:ConnectEvent(KeyDownEvent, function(e)
|
||||||
if e.key == KeyboardKey.LeftControl then
|
if e.key == KeyboardKey.LeftControl then
|
||||||
|
self.DebugCtrlDown = true
|
||||||
local lp2 = _UserService.LocalPlayer
|
local lp2 = _UserService.LocalPlayer
|
||||||
if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then
|
if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then
|
||||||
self:PlayerAttackMotion()
|
self:PlayerAttackMotion()
|
||||||
end
|
end
|
||||||
|
elseif e.key == KeyboardKey.LeftShift or e.key == KeyboardKey.RightShift then
|
||||||
|
self.DebugShiftDown = true
|
||||||
|
elseif e.key == KeyboardKey.C then
|
||||||
|
if self.DebugCtrlDown == true and self.DebugShiftDown == true then
|
||||||
|
self:OpenDebugCardPicker()
|
||||||
|
end
|
||||||
|
elseif e.key == KeyboardKey.E then
|
||||||
|
if self.DebugCtrlDown == true and self.DebugShiftDown == true then
|
||||||
|
self:CheatFillEnergy()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
_InputService:ConnectEvent(KeyUpEvent, function(e)
|
||||||
|
if e.key == KeyboardKey.LeftControl then
|
||||||
|
self.DebugCtrlDown = false
|
||||||
|
elseif e.key == KeyboardKey.LeftShift or e.key == KeyboardKey.RightShift then
|
||||||
|
self.DebugShiftDown = false
|
||||||
end
|
end
|
||||||
end)`),
|
end)`),
|
||||||
|
method('CheatFillEnergy', `if self.RunActive ~= true or self.CombatOver == true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.PlayerHp = self.PlayerMaxHp
|
||||||
|
self.Energy = self.MaxEnergy
|
||||||
|
self:RenderCombat()
|
||||||
|
self:RenderPiles()
|
||||||
|
self:Toast("치트: 체력·에너지 회복")`),
|
||||||
method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||||
local errCode, value = ds:GetAndWait("ascensionUnlocked")
|
local errCode, value = ds:GetAndWait("ascensionUnlocked")
|
||||||
local n = 0
|
local n = 0
|
||||||
@@ -49,7 +89,7 @@ if v > self.AscensionUnlocked then v = self.AscensionUnlocked end
|
|||||||
self.AscensionLevel = v
|
self.AscensionLevel = v
|
||||||
self:RenderAscension()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delta' }]),
|
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))
|
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))`),
|
self:SetText("/ui/LobbyUIGroup/LobbyHud/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`),
|
||||||
method('AscHpMult', `local m = 1
|
method('AscHpMult', `local m = 1
|
||||||
if self.AscensionLevel >= 1 then m = m + 0.1 end
|
if self.AscensionLevel >= 1 then m = m + 0.1 end
|
||||||
if self.AscensionLevel >= 6 then m = m + 0.1 end
|
if self.AscensionLevel >= 6 then m = m + 0.1 end
|
||||||
|
|||||||
@@ -10,41 +10,63 @@ self:RenderCharacterSelect()`),
|
|||||||
self:RenderCharacterSelect()`, [
|
self:RenderCharacterSelect()`, [
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' },
|
||||||
]),
|
]),
|
||||||
method('RenderCharacterSelect', `local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
|
method('RenderCharacterSelect', `local base = "/ui/SelectUIGroup/CharacterSelectHud"
|
||||||
if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then
|
local arts = { { p = "/WarriorButton/Art", c = "warrior" }, { p = "/MageButton/Art", c = "magician" }, { p = "/BanditButton/Art", c = "rogue" } }
|
||||||
if self.SelectedClass == "warrior" then
|
for i = 1, #arts do
|
||||||
warrior.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
local e = _EntityService:GetEntityByPath(base .. arts[i].p)
|
||||||
else
|
if e ~= nil and e.SpriteGUIRendererComponent ~= nil and self.ClassPortraits ~= nil and self.ClassPortraits[arts[i].c] ~= nil then
|
||||||
warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
e.SpriteGUIRendererComponent.ImageRUID = self.ClassPortraits[arts[i].c]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
local btns = { { p = "/WarriorButton", c = "warrior" }, { p = "/MageButton", c = "magician" }, { p = "/BanditButton", c = "rogue" } }
|
||||||
if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then
|
for i = 1, #btns do
|
||||||
if self.SelectedClass == "magician" then
|
local e = _EntityService:GetEntityByPath(base .. btns[i].p)
|
||||||
mage.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
if e ~= nil then
|
||||||
else
|
if e.MaskComponent == nil then
|
||||||
mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
e:AddComponent("MaskComponent")
|
||||||
end
|
end
|
||||||
end
|
if e.SpriteGUIRendererComponent ~= nil then
|
||||||
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
|
if self.SelectedClass == btns[i].c then
|
||||||
if thief ~= nil and thief.SpriteGUIRendererComponent ~= nil then
|
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||||
if self.SelectedClass == "bandit" then
|
else
|
||||||
thief.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
e.SpriteGUIRendererComponent.Color = Color(0.45, 0.5, 0.58, 1)
|
||||||
else
|
end
|
||||||
thief.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
local nl = string.char(10)
|
||||||
|
local name = ""
|
||||||
|
local eng = ""
|
||||||
|
local desc = "직업을 선택하고 시작하세요"
|
||||||
|
local btnName = ""
|
||||||
if self.SelectedClass == "warrior" then
|
if self.SelectedClass == "warrior" then
|
||||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨")
|
name = "전사"
|
||||||
elseif self.SelectedClass == "bandit" then
|
eng = "Warrior"
|
||||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "도적 선택됨")
|
btnName = "/WarriorButton"
|
||||||
|
desc = "직업군 · 모험가" .. nl .. "방어를 쌓고 버티다 강하게 역공하는 단단한 탱커."
|
||||||
|
elseif self.SelectedClass == "rogue" then
|
||||||
|
name = "도적"
|
||||||
|
eng = "Rogue"
|
||||||
|
btnName = "/BanditButton"
|
||||||
|
desc = "직업군 · 모험가" .. nl .. "표창 난사와 독으로 빠르게 몰아치는 민첩한 직업."
|
||||||
elseif self.SelectedClass == "magician" then
|
elseif self.SelectedClass == "magician" then
|
||||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "마법사 선택됨")
|
name = "법사"
|
||||||
else
|
eng = "Magician"
|
||||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요")
|
btnName = "/MageButton"
|
||||||
end`),
|
desc = "직업군 · 모험가" .. nl .. "약하지만 게이지 운용으로 화력을 집중하는 원소 마법사."
|
||||||
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "bandit" and self.SelectedClass ~= "magician" then
|
end
|
||||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요")
|
if btnName ~= "" then
|
||||||
|
local art = _EntityService:GetEntityByPath(base .. btnName .. "/Art")
|
||||||
|
local target = _EntityService:GetEntityByPath(base .. "/SelectedCharacterArt")
|
||||||
|
if art ~= nil and art.SpriteGUIRendererComponent ~= nil and target ~= nil and target.SpriteGUIRendererComponent ~= nil then
|
||||||
|
target.SpriteGUIRendererComponent.ImageRUID = art.SpriteGUIRendererComponent.ImageRUID
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:SetText(base .. "/SelectedClass", name)
|
||||||
|
self:SetText(base .. "/SelectedClass/SelectedClassEng", eng)
|
||||||
|
self:SetText(base .. "/SelectedClassStatus", desc)`),
|
||||||
|
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "rogue" and self.SelectedClass ~= "magician" then
|
||||||
|
self:SetText("/ui/SelectUIGroup/CharacterSelectHud/SelectedClassStatus", "직업을 먼저 선택하세요")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:StartRun()`),
|
self:StartRun()`),
|
||||||
@@ -54,5 +76,5 @@ if e ~= nil then
|
|||||||
end`, [
|
end`, [
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
|
||||||
]),
|
], 2),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,10 +3,26 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
|
|||||||
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 { 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 = [
|
export const combatMethods = [
|
||||||
|
method('CanPlayCardNow', `if c == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if c.playableWhenDrawPileEmpty == true and self.DrawPile ~= nil and #self.DrawPile > 0 then
|
||||||
|
self:Toast("뽑을 카드 더미가 비어 있을 때만 사용할 수 있습니다.")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||||
method('PlayCard', `if self:IsDiscardSelecting() == true then
|
method('PlayCard', `if self:IsDiscardSelecting() == true then
|
||||||
self:SelectDiscardSlot(slot)
|
self:SelectDiscardSlot(slot)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:IsRetainSelecting() == true then
|
||||||
|
self:SelectRetainSlot(slot)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self:IsReserveSelecting() == true then
|
||||||
|
self:SelectReserveSlot(slot)
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -25,12 +41,77 @@ if c.unplayable == true then
|
|||||||
self:Toast("사용할 수 없는 카드입니다")
|
self:Toast("사용할 수 없는 카드입니다")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self.Energy < c.cost then
|
if self:CanPlayCardNow(c) ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cost = c.cost or 0
|
||||||
|
local skillFree = false
|
||||||
|
local skillRepeat = 0
|
||||||
|
if self.HandCostZeroThisTurn == true then
|
||||||
|
cost = 0
|
||||||
|
elseif c.useAllEnergy == true then
|
||||||
|
cost = self.Energy
|
||||||
|
end
|
||||||
|
if c.kind == "Skill" and c.useAllEnergy ~= true and self.NextSkillCostZero == true then
|
||||||
|
cost = 0
|
||||||
|
skillFree = true
|
||||||
|
end
|
||||||
|
if c.kind == "Skill" and c.useAllEnergy ~= true and self.SkillCostReductionThisTurn ~= nil and self.SkillCostReductionThisTurn > 0 then
|
||||||
|
cost = math.max(0, cost - self.SkillCostReductionThisTurn)
|
||||||
|
end
|
||||||
|
if c.useAllEnergy ~= true and self.CombatCardCostReduction ~= nil and self.CombatCardCostReduction[cardId] ~= nil then
|
||||||
|
cost = math.max(0, cost - self.CombatCardCostReduction[cardId])
|
||||||
|
end
|
||||||
|
if c.kind == "Skill" and self.NextSkillRepeatCount ~= nil and self.NextSkillRepeatCount > 0 then
|
||||||
|
skillRepeat = self.NextSkillRepeatCount
|
||||||
|
end
|
||||||
|
if self.Energy < cost then
|
||||||
self:Toast("에너지가 부족합니다")
|
self:Toast("에너지가 부족합니다")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.Energy = self.Energy - c.cost
|
self.Energy = self.Energy - cost
|
||||||
self:ResolveCardEffects(cardId, c, false)
|
self.ActiveKillReward = c.rewardOnKill or 0
|
||||||
|
self:ResolveCardEffects(cardId, slot, c, false, cost)
|
||||||
|
local function applyCardPlayHooks()
|
||||||
|
if self:HasPowerField("cardPlayedBlock") == true then
|
||||||
|
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
|
||||||
|
end
|
||||||
|
if c.cardPlayedDamage ~= nil and c.cardPlayedDamage > 0 then
|
||||||
|
self:DealDirectDamageToTarget(c.cardPlayedDamage)
|
||||||
|
end
|
||||||
|
if c.cardPlayedRandomDamage ~= nil and c.cardPlayedRandomDamage > 0 then
|
||||||
|
self:DealDirectDamageToRandomMonster(c.cardPlayedRandomDamage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
applyCardPlayHooks()
|
||||||
|
if skillRepeat > 0 then
|
||||||
|
local remaining = (self.NextSkillRepeatCount or 0) - skillRepeat
|
||||||
|
if remaining < 0 then
|
||||||
|
remaining = 0
|
||||||
|
end
|
||||||
|
self.NextSkillRepeatCount = remaining
|
||||||
|
for i = 1, skillRepeat do
|
||||||
|
self:ResolveCardEffects(cardId, slot, c, false, cost)
|
||||||
|
applyCardPlayHooks()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if c.kind == "Attack" then
|
||||||
|
self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1
|
||||||
|
end
|
||||||
|
if skillFree == true then
|
||||||
|
if c.nextSkillCostZero ~= true then
|
||||||
|
self.NextSkillCostZero = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then
|
||||||
|
self.ActiveKillReward = 0
|
||||||
|
end
|
||||||
|
if c.combatCostReductionOnPlay ~= nil and c.combatCostReductionOnPlay > 0 then
|
||||||
|
if self.CombatCardCostReduction == nil then
|
||||||
|
self.CombatCardCostReduction = {}
|
||||||
|
end
|
||||||
|
self.CombatCardCostReduction[cardId] = (self.CombatCardCostReduction[cardId] or 0) + c.combatCostReductionOnPlay
|
||||||
|
end
|
||||||
table.remove(self.Hand, slot)
|
table.remove(self.Hand, slot)
|
||||||
if c.exhaust == true then
|
if c.exhaust == true then
|
||||||
if self.ExhaustPile == nil then self.ExhaustPile = {} end
|
if self.ExhaustPile == nil then self.ExhaustPile = {} end
|
||||||
@@ -44,12 +125,19 @@ self:RenderCombat()
|
|||||||
if self:BeginDiscardSelection(c) == true then
|
if self:BeginDiscardSelection(c) == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:BeginReserveSelection(c) == true then
|
||||||
|
return
|
||||||
|
end
|
||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||||
method('OnCardButton', `if self:IsDiscardSelecting() == true then
|
method('OnCardButton', `if self:IsDiscardSelecting() == true then
|
||||||
self:SelectDiscardSlot(slot)
|
self:SelectDiscardSlot(slot)
|
||||||
|
elseif self:IsRetainSelecting() == true then
|
||||||
|
self:SelectRetainSlot(slot)
|
||||||
|
elseif self:IsReserveSelecting() == true then
|
||||||
|
self:SelectReserveSlot(slot)
|
||||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||||
method('FindMonsterAtTouch', `local best = 0
|
method('FindMonsterAtTouch', `local best = 0
|
||||||
local bestDist = 200
|
local bestDist = 200
|
||||||
@@ -75,9 +163,8 @@ for i = 1, #self.Monsters do
|
|||||||
local m = self.Monsters[i]
|
local m = self.Monsters[i]
|
||||||
local active = false
|
local active = false
|
||||||
if m ~= nil and m.alive == true and i == shownTarget then active = true end
|
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/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) .. "/TargetMarker", active and dragActive)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
|
|
||||||
end`),
|
end`),
|
||||||
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||||
return
|
return
|
||||||
@@ -90,7 +177,7 @@ if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
|
|||||||
self.CardHoverTweenId = 0
|
self.CardHoverTweenId = 0
|
||||||
end
|
end
|
||||||
for i = 1, 10 do
|
for i = 1, 10 do
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i))
|
||||||
if e ~= nil and e.UITransformComponent ~= nil then
|
if e ~= nil and e.UITransformComponent ~= nil then
|
||||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||||
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
|
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
|
||||||
@@ -102,7 +189,7 @@ self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection
|
|||||||
method('OnCardDrag', `if self.DragSlot ~= slot then
|
method('OnCardDrag', `if self.DragSlot ~= slot then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
if e ~= nil and e.UITransformComponent ~= nil then
|
if e ~= nil and e.UITransformComponent ~= nil then
|
||||||
local ui = _UILogic:ScreenToUIPosition(touchPoint)
|
local ui = _UILogic:ScreenToUIPosition(touchPoint)
|
||||||
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
|
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
|
||||||
@@ -124,7 +211,7 @@ end`, [
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.DragSlot = 0
|
self.DragSlot = 0
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
if e ~= nil and e.UITransformComponent ~= nil then
|
if e ~= nil and e.UITransformComponent ~= nil then
|
||||||
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
|
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
|
||||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||||
@@ -137,6 +224,14 @@ self:ResolveCardDrop(slot, touchPoint)`, [
|
|||||||
self:SelectDiscardSlot(slot)
|
self:SelectDiscardSlot(slot)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:IsRetainSelecting() == true then
|
||||||
|
self:SelectRetainSlot(slot)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self:IsReserveSelecting() == true then
|
||||||
|
self:SelectReserveSlot(slot)
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -171,7 +266,7 @@ end`, [
|
|||||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||||
]),
|
]),
|
||||||
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
||||||
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
||||||
if m == nil or m.alive ~= true then
|
if m == nil or m.alive ~= true then
|
||||||
m = nil
|
m = nil
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
@@ -179,35 +274,159 @@ if m == nil or m.alive ~= true then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if m == nil then
|
if m == nil then
|
||||||
return
|
return false
|
||||||
end
|
end
|
||||||
local dmg = amount
|
local dmg = amount
|
||||||
if m.vuln > 0 then
|
if m.vuln > 0 then
|
||||||
dmg = math.floor(dmg * 1.5)
|
dmg = math.floor(dmg * 1.5)
|
||||||
end
|
end
|
||||||
|
if m.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
|
||||||
|
dmg = math.floor(dmg * self.ActiveAttackDamageVsWeakMultiplier)
|
||||||
|
end
|
||||||
if m.block > 0 and pierce ~= true then
|
if m.block > 0 and pierce ~= true then
|
||||||
local absorbed = math.min(m.block, dmg)
|
local absorbed = math.min(m.block, dmg)
|
||||||
m.block = m.block - absorbed
|
m.block = m.block - absorbed
|
||||||
dmg = dmg - absorbed
|
dmg = dmg - absorbed
|
||||||
end
|
end
|
||||||
m.hp = m.hp - dmg
|
m.hp = m.hp - dmg
|
||||||
|
if dmg > 0 then
|
||||||
|
local poison = self:AddPowerFieldTotal("attackPoison")
|
||||||
|
if poison ~= nil and poison > 0 then
|
||||||
|
self:ApplyPoisonToMonster(m, poison)
|
||||||
|
end
|
||||||
|
end
|
||||||
self:MonsterHitMotion(m.slot)
|
self:MonsterHitMotion(m.slot)
|
||||||
|
local killed = false
|
||||||
if m.hp <= 0 then
|
if m.hp <= 0 then
|
||||||
m.hp = 0
|
m.hp = 0
|
||||||
self:KillMonster(m.slot)
|
self:KillMonster(m.slot)
|
||||||
end`, [
|
killed = true
|
||||||
|
end
|
||||||
|
return killed`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||||||
|
], 0, 'boolean'),
|
||||||
|
method('DealDirectDamageToTarget', `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 false
|
||||||
|
end
|
||||||
|
m.hp = m.hp - amount
|
||||||
|
self:ShowDmgPop(m.slot, amount)
|
||||||
|
self:MonsterHitMotion(m.slot)
|
||||||
|
local killed = false
|
||||||
|
if m.hp <= 0 then
|
||||||
|
m.hp = 0
|
||||||
|
self:KillMonster(m.slot)
|
||||||
|
killed = true
|
||||||
|
end
|
||||||
|
return killed`, [
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
|
], 0, 'boolean'),
|
||||||
|
method('DealDirectDamageToRandomMonster', `local alive = {}
|
||||||
|
for i = 1, #self.Monsters do
|
||||||
|
local m = self.Monsters[i]
|
||||||
|
if m ~= nil and m.alive == true then
|
||||||
|
table.insert(alive, m)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #alive <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local m = alive[math.random(1, #alive)]
|
||||||
|
if m == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
m.hp = m.hp - amount
|
||||||
|
self:ShowDmgPop(m.slot, amount)
|
||||||
|
self:MonsterHitMotion(m.slot)
|
||||||
|
local killed = false
|
||||||
|
if m.hp <= 0 then
|
||||||
|
m.hp = 0
|
||||||
|
self:KillMonster(m.slot)
|
||||||
|
killed = true
|
||||||
|
end
|
||||||
|
return killed`, [
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
|
], 0, 'boolean'),
|
||||||
|
method('ApplyPoisonToMonster', `if target == nil or target.alive ~= true or amount == nil or amount <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if target.artifact ~= nil and target.artifact > 0 then
|
||||||
|
target.artifact = target.artifact - 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
target.poison = (target.poison or 0) + amount
|
||||||
|
self.PoisonApplicationsThisCombat = (self.PoisonApplicationsThisCombat or 0) + 1
|
||||||
|
local burstEvery = self:AddPowerFieldTotal("poisonApplicationBurstEvery")
|
||||||
|
local burstDamage = self:AddPowerFieldTotal("poisonApplicationBurstDamage")
|
||||||
|
if burstEvery ~= nil and burstEvery > 0 and burstDamage ~= nil and burstDamage > 0 then
|
||||||
|
if (self.PoisonApplicationsThisCombat % burstEvery) == 0 then
|
||||||
|
self:DealDamageToAllMonsters(burstDamage)
|
||||||
|
end
|
||||||
|
end`, [
|
||||||
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'target' },
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
]),
|
]),
|
||||||
|
method('DealDamageToAllMonsters', `if self.Monsters == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local killCount = 0
|
||||||
|
for i = 1, #self.Monsters do
|
||||||
|
local m = self.Monsters[i]
|
||||||
|
if m ~= nil and m.alive == true then
|
||||||
|
local dmg = amount
|
||||||
|
if isAttack == true and 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
|
||||||
|
if dmg > 0 then
|
||||||
|
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + dmg
|
||||||
|
if isAttack == true then
|
||||||
|
local poison = self:AddPowerFieldTotal("attackPoison")
|
||||||
|
if poison ~= nil and poison > 0 then
|
||||||
|
self:ApplyPoisonToMonster(m, poison)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:ShowDmgPop(i, dmg)
|
||||||
|
self:MonsterHitMotion(i)
|
||||||
|
if m.hp <= 0 then
|
||||||
|
m.hp = 0
|
||||||
|
self:KillMonster(m.slot)
|
||||||
|
killCount = killCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
|
||||||
|
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
|
||||||
|
end
|
||||||
|
self:RenderCombat()
|
||||||
|
self:CheckCombatEnd()
|
||||||
|
return killCount > 0`, [
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'isAttack' },
|
||||||
|
], 0, 'boolean'),
|
||||||
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
|
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
|
||||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||||
self:DealDamageToTarget(damage, pierce)
|
self:DealDamageToTarget(damage, pierce)
|
||||||
|
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()
|
self:CheckCombatEnd()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.FxBusy = true
|
self.FxBusy = true
|
||||||
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
|
local fx = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/SkillFx")
|
||||||
if fx ~= nil then
|
if fx ~= nil then
|
||||||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||||||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||||||
@@ -227,7 +446,15 @@ _TimerService:SetTimerOnce(function()
|
|||||||
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
||||||
shown = math.floor(damage * 1.5)
|
shown = math.floor(damage * 1.5)
|
||||||
end
|
end
|
||||||
self:DealDamageToTarget(damage, pierce)
|
if mt ~= nil and mt.alive == true and mt.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
|
||||||
|
shown = math.floor(shown * self.ActiveAttackDamageVsWeakMultiplier)
|
||||||
|
end
|
||||||
|
local killed = self:DealDamageToTarget(damage, pierce)
|
||||||
|
if killed == true and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
|
||||||
|
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + self.ActiveKillReward
|
||||||
|
end
|
||||||
|
self.ActiveKillReward = 0
|
||||||
|
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||||
self:ShowDmgPop(targetIndex, shown)
|
self:ShowDmgPop(targetIndex, shown)
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()
|
self:CheckCombatEnd()
|
||||||
@@ -238,7 +465,7 @@ end, 0.35)`, [
|
|||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||||||
]),
|
]),
|
||||||
method('PlayAoeFx', `self.FxBusy = true
|
method('PlayAoeFx', `self.FxBusy = true
|
||||||
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
|
local fx = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/SkillFx")
|
||||||
if fx ~= nil then
|
if fx ~= nil then
|
||||||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||||||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||||||
@@ -251,6 +478,7 @@ end
|
|||||||
_TimerService:SetTimerOnce(function()
|
_TimerService:SetTimerOnce(function()
|
||||||
if fx ~= nil then fx.Enable = false end
|
if fx ~= nil then fx.Enable = false end
|
||||||
self.FxBusy = false
|
self.FxBusy = false
|
||||||
|
local killCount = 0
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
local m = self.Monsters[i]
|
local m = self.Monsters[i]
|
||||||
if m ~= nil and m.alive == true then
|
if m ~= nil and m.alive == true then
|
||||||
@@ -258,20 +486,35 @@ _TimerService:SetTimerOnce(function()
|
|||||||
if m.vuln > 0 then
|
if m.vuln > 0 then
|
||||||
dmg = math.floor(dmg * 1.5)
|
dmg = math.floor(dmg * 1.5)
|
||||||
end
|
end
|
||||||
|
if m.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
|
||||||
|
dmg = math.floor(dmg * self.ActiveAttackDamageVsWeakMultiplier)
|
||||||
|
end
|
||||||
if m.block > 0 then
|
if m.block > 0 then
|
||||||
local absorbed = math.min(m.block, dmg)
|
local absorbed = math.min(m.block, dmg)
|
||||||
m.block = m.block - absorbed
|
m.block = m.block - absorbed
|
||||||
dmg = dmg - absorbed
|
dmg = dmg - absorbed
|
||||||
end
|
end
|
||||||
m.hp = m.hp - dmg
|
m.hp = m.hp - dmg
|
||||||
|
if dmg > 0 then
|
||||||
|
local poison = self:AddPowerFieldTotal("attackPoison")
|
||||||
|
if poison ~= nil and poison > 0 then
|
||||||
|
self:ApplyPoisonToMonster(m, poison)
|
||||||
|
end
|
||||||
|
end
|
||||||
self:ShowDmgPop(i, dmg)
|
self:ShowDmgPop(i, dmg)
|
||||||
self:MonsterHitMotion(i)
|
self:MonsterHitMotion(i)
|
||||||
if m.hp <= 0 then
|
if m.hp <= 0 then
|
||||||
m.hp = 0
|
m.hp = 0
|
||||||
self:KillMonster(m.slot)
|
self:KillMonster(m.slot)
|
||||||
|
killCount = killCount + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
|
||||||
|
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
|
||||||
|
end
|
||||||
|
self.ActiveKillReward = 0
|
||||||
|
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()
|
self:CheckCombatEnd()
|
||||||
end, 0.35)`, [
|
end, 0.35)`, [
|
||||||
@@ -287,7 +530,7 @@ if m.entity ~= nil and isvalid(m.entity) then
|
|||||||
local ent = m.entity
|
local ent = m.entity
|
||||||
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
|
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
|
||||||
end
|
end
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(slot), false)
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
|
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
|
||||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||||
@@ -297,6 +540,9 @@ if self.PlayerBlock > 0 then
|
|||||||
self.PlayerBlock = self.PlayerBlock - absorbed
|
self.PlayerBlock = self.PlayerBlock - absorbed
|
||||||
dmg = dmg - absorbed
|
dmg = dmg - absorbed
|
||||||
end
|
end
|
||||||
|
if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then
|
||||||
|
dmg = 1
|
||||||
|
end
|
||||||
if dmg > 0 then
|
if dmg > 0 then
|
||||||
self.PlayerHp = self.PlayerHp - dmg
|
self.PlayerHp = self.PlayerHp - dmg
|
||||||
local reflect = self.PlayerThorns or 0
|
local reflect = self.PlayerThorns or 0
|
||||||
@@ -341,21 +587,28 @@ if idx == 0 or self.PlayerHp <= 0 then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local m = self.Monsters[idx]
|
local m = self.Monsters[idx]
|
||||||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx)
|
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(idx)
|
||||||
self:SetEntityEnabled(base .. "/ActFrame", true)
|
self:SetEntityEnabled(base .. "/ActFrame", true)
|
||||||
_TimerService:SetTimerOnce(function()
|
_TimerService:SetTimerOnce(function()
|
||||||
if m.poison ~= nil and m.poison > 0 then
|
local poisonTicks = 1
|
||||||
m.hp = m.hp - m.poison
|
local bonusTicks = self:AddPowerFieldTotal("extraPoisonTicks")
|
||||||
self:ShowDmgPop(idx, m.poison)
|
if bonusTicks ~= nil and bonusTicks > 0 then
|
||||||
self:MonsterHitMotion(idx)
|
poisonTicks = poisonTicks + bonusTicks
|
||||||
m.poison = m.poison - 1
|
end
|
||||||
if m.hp <= 0 then
|
for pt = 1, poisonTicks do
|
||||||
m.hp = 0
|
if m.poison ~= nil and m.poison > 0 then
|
||||||
self:KillMonster(m.slot)
|
m.hp = m.hp - m.poison
|
||||||
self:RenderCombat()
|
self:ShowDmgPop(idx, m.poison)
|
||||||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
self:MonsterHitMotion(idx)
|
||||||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
m.poison = m.poison - 1
|
||||||
return
|
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
|
end
|
||||||
end
|
end
|
||||||
m.block = 0
|
m.block = 0
|
||||||
@@ -364,6 +617,10 @@ _TimerService:SetTimerOnce(function()
|
|||||||
if intent.kind == "Attack" then
|
if intent.kind == "Attack" then
|
||||||
self:MonsterLunge(idx)
|
self:MonsterLunge(idx)
|
||||||
local atk = intent.value + m.str
|
local atk = intent.value + m.str
|
||||||
|
if self.EnemyStrengthLossThisTurn ~= nil and self.EnemyStrengthLossThisTurn > 0 then
|
||||||
|
atk = atk - self.EnemyStrengthLossThisTurn
|
||||||
|
if atk < 0 then atk = 0 end
|
||||||
|
end
|
||||||
if m.weak > 0 then
|
if m.weak > 0 then
|
||||||
atk = math.floor(atk * 0.75)
|
atk = math.floor(atk * 0.75)
|
||||||
end
|
end
|
||||||
@@ -417,10 +674,25 @@ self.DiscardSelectRemaining = 0
|
|||||||
self.DiscardSelectTotal = 0
|
self.DiscardSelectTotal = 0
|
||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
|
self.RetainSelectActive = false
|
||||||
|
self.TurnAttackCardsPlayed = 0
|
||||||
|
self.TurnDiscardedCards = 0
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnBlock = 0
|
||||||
|
self.NextTurnDraw = 0
|
||||||
|
self.NextTurnKeepBlock = false
|
||||||
|
self.NextTurnAttackMultiplier = 1
|
||||||
|
self.TurnAttackMultiplier = 1
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()`),
|
self:RenderPiles()`),
|
||||||
method('CheckCombatEnd', `local anyAlive = false
|
method('CheckCombatEnd', `if self.CombatOver == true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local anyAlive = false
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
if self.Monsters[i].alive == true then anyAlive = true; break end
|
if self.Monsters[i].alive == true then anyAlive = true; break end
|
||||||
end
|
end
|
||||||
@@ -445,7 +717,7 @@ if anyAlive == false then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if node ~= nil and node.type == "boss" then
|
if node ~= nil and node.type == "boss" then
|
||||||
if self.PlayerJob == "" and self.Floor < self.RunLength then
|
if self:CanAdvanceJob() == true and self.Floor < self.RunLength then
|
||||||
self:ShowJobChoice()
|
self:ShowJobChoice()
|
||||||
else
|
else
|
||||||
if self.PlayerJob ~= "" then self:AwardSouls(1) end
|
if self.PlayerJob ~= "" then self:AwardSouls(1) end
|
||||||
|
|||||||
@@ -10,56 +10,60 @@ for i = #list, 2, -1 do
|
|||||||
\tlocal j = math.random(1, i)
|
\tlocal j = math.random(1, i)
|
||||||
\tlist[i], list[j] = list[j], list[i]
|
\tlist[i], list[j] = list[j], list[i]
|
||||||
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]),
|
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]),
|
||||||
method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton")
|
method('BindButtons', `if self.ButtonsBound == true then
|
||||||
if endTurn ~= nil and endTurn.ButtonComponent ~= nil then
|
return
|
||||||
|
end
|
||||||
|
self.ButtonsBound = true
|
||||||
|
local endTurn = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/EndTurnButton")
|
||||||
|
if endTurn ~= nil and (endTurn.ButtonComponent ~= nil or endTurn:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.EndTurnHandler ~= nil then
|
if self.EndTurnHandler ~= nil then
|
||||||
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
|
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
|
||||||
self.EndTurnHandler = nil
|
self.EndTurnHandler = nil
|
||||||
end
|
end
|
||||||
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
|
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
|
||||||
end
|
end
|
||||||
local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile")
|
local drawPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/DrawPile")
|
||||||
if drawPile ~= nil and drawPile.ButtonComponent ~= nil then
|
if drawPile ~= nil and (drawPile.ButtonComponent ~= nil or drawPile:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.DrawPileHandler ~= nil then
|
if self.DrawPileHandler ~= nil then
|
||||||
drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)
|
drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)
|
||||||
self.DrawPileHandler = nil
|
self.DrawPileHandler = nil
|
||||||
end
|
end
|
||||||
self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end)
|
self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end)
|
||||||
end
|
end
|
||||||
local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile")
|
local discardPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/DiscardPile")
|
||||||
if discardPile ~= nil and discardPile.ButtonComponent ~= nil then
|
if discardPile ~= nil and (discardPile.ButtonComponent ~= nil or discardPile:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.DiscardPileHandler ~= nil then
|
if self.DiscardPileHandler ~= nil then
|
||||||
discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)
|
discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)
|
||||||
self.DiscardPileHandler = nil
|
self.DiscardPileHandler = nil
|
||||||
end
|
end
|
||||||
self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end)
|
self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end)
|
||||||
end
|
end
|
||||||
local exhaustPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/ExhaustPile")
|
local exhaustPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/ExhaustPile")
|
||||||
if exhaustPile ~= nil and exhaustPile.ButtonComponent ~= nil then
|
if exhaustPile ~= nil and (exhaustPile.ButtonComponent ~= nil or exhaustPile:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.ExhaustPileHandler ~= nil then
|
if self.ExhaustPileHandler ~= nil then
|
||||||
exhaustPile:DisconnectEvent(ButtonClickEvent, self.ExhaustPileHandler)
|
exhaustPile:DisconnectEvent(ButtonClickEvent, self.ExhaustPileHandler)
|
||||||
self.ExhaustPileHandler = nil
|
self.ExhaustPileHandler = nil
|
||||||
end
|
end
|
||||||
self.ExhaustPileHandler = exhaustPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("exhaust") end)
|
self.ExhaustPileHandler = exhaustPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("exhaust") end)
|
||||||
end
|
end
|
||||||
local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close")
|
local inspectClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud/Close")
|
||||||
if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then
|
if inspectClose ~= nil and (inspectClose.ButtonComponent ~= nil or inspectClose:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.DeckInspectCloseHandler ~= nil then
|
if self.DeckInspectCloseHandler ~= nil then
|
||||||
inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)
|
inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)
|
||||||
self.DeckInspectCloseHandler = nil
|
self.DeckInspectCloseHandler = nil
|
||||||
end
|
end
|
||||||
self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)
|
self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)
|
||||||
end
|
end
|
||||||
local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton")
|
local allDeckButton = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/AllDeckButton")
|
||||||
if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then
|
if allDeckButton ~= nil and (allDeckButton.ButtonComponent ~= nil or allDeckButton:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.AllDeckHandler ~= nil then
|
if self.AllDeckHandler ~= nil then
|
||||||
allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)
|
allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)
|
||||||
self.AllDeckHandler = nil
|
self.AllDeckHandler = nil
|
||||||
end
|
end
|
||||||
self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)
|
self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)
|
||||||
end
|
end
|
||||||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
local allDeckClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
|
||||||
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.AllDeckCloseHandler ~= nil then
|
if self.AllDeckCloseHandler ~= nil then
|
||||||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||||
self.AllDeckCloseHandler = nil
|
self.AllDeckCloseHandler = nil
|
||||||
@@ -67,10 +71,20 @@ if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
|||||||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||||
end
|
end
|
||||||
self:BindClassDeckTabs()
|
self:BindClassDeckTabs()
|
||||||
|
for i = 1, 120 do
|
||||||
|
local allCard = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(i))
|
||||||
|
if allCard ~= nil and (allCard.ButtonComponent ~= nil or allCard:AddComponent("ButtonComponent") ~= nil) then
|
||||||
|
if allCard.SpriteGUIRendererComponent ~= nil then
|
||||||
|
allCard.SpriteGUIRendererComponent.RaycastTarget = true
|
||||||
|
end
|
||||||
|
local slot = i
|
||||||
|
allCard:ConnectEvent(ButtonClickEvent, function() self:OnAllDeckCardButton(slot) end)
|
||||||
|
end
|
||||||
|
end
|
||||||
for i = 1, 10 do
|
for i = 1, 10 do
|
||||||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i))
|
||||||
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
|
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
|
||||||
local cardPath = "/ui/DefaultGroup/CardHand/Card" .. tostring(i)
|
local cardPath = "/ui/RunUIGroup/CardHand/Card" .. tostring(i)
|
||||||
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||||
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
|
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
|
||||||
@@ -78,24 +92,24 @@ for i = 1, 10 do
|
|||||||
cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(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(UITouchEnterEvent, function() self:HoverCard(i) end)
|
||||||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end)
|
cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end)
|
||||||
if cardEntity.ButtonComponent ~= nil then
|
if (cardEntity.ButtonComponent ~= nil or cardEntity:AddComponent("ButtonComponent") ~= nil) then
|
||||||
cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end)
|
cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for i = 1, 3 do
|
for i = 1, 3 do
|
||||||
local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i))
|
local rc = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Reward" .. tostring(i))
|
||||||
if rc ~= nil and rc.ButtonComponent ~= nil then
|
if rc ~= nil and (rc.ButtonComponent ~= nil or rc:AddComponent("ButtonComponent") ~= nil) then
|
||||||
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
|
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
|
||||||
if rc.UITouchReceiveComponent ~= nil then
|
if rc.UITouchReceiveComponent ~= nil then
|
||||||
local cardPath = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(i)
|
local cardPath = "/ui/RunUIGroup/RewardHud/Reward" .. tostring(i)
|
||||||
rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||||
rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip")
|
local skip = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Skip")
|
||||||
if skip ~= nil and skip.ButtonComponent ~= nil then
|
if skip ~= nil and (skip.ButtonComponent ~= nil or skip:AddComponent("ButtonComponent") ~= nil) then
|
||||||
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
|
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
|
||||||
end
|
end
|
||||||
local mapNodeIds = {}
|
local mapNodeIds = {}
|
||||||
@@ -107,42 +121,42 @@ end
|
|||||||
table.insert(mapNodeIds, "boss")
|
table.insert(mapNodeIds, "boss")
|
||||||
for i = 1, #mapNodeIds do
|
for i = 1, #mapNodeIds do
|
||||||
local nid = mapNodeIds[i]
|
local nid = mapNodeIds[i]
|
||||||
local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid)
|
local mn = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud/Node_" .. nid)
|
||||||
if mn ~= nil and mn.ButtonComponent ~= nil then
|
if mn ~= nil and (mn.ButtonComponent ~= nil or mn:AddComponent("ButtonComponent") ~= nil) then
|
||||||
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
|
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for i = 1, 3 do
|
for i = 1, 3 do
|
||||||
local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i))
|
local sc = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Card" .. tostring(i))
|
||||||
if sc ~= nil and sc.ButtonComponent ~= nil then
|
if sc ~= nil and (sc.ButtonComponent ~= nil or sc:AddComponent("ButtonComponent") ~= nil) then
|
||||||
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
|
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
|
||||||
if sc.UITouchReceiveComponent ~= nil then
|
if sc.UITouchReceiveComponent ~= nil then
|
||||||
local cardPath = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
local cardPath = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i)
|
||||||
sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||||
sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave")
|
local shopLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Leave")
|
||||||
if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then
|
if shopLeave ~= nil and (shopLeave.ButtonComponent ~= nil or shopLeave:AddComponent("ButtonComponent") ~= nil) then
|
||||||
shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||||
end
|
end
|
||||||
local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
local shopRelic = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Relic")
|
||||||
if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then
|
if shopRelic ~= nil and (shopRelic.ButtonComponent ~= nil or shopRelic:AddComponent("ButtonComponent") ~= nil) then
|
||||||
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)
|
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)
|
||||||
end
|
end
|
||||||
local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave")
|
local restLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/RestHud/Leave")
|
||||||
if restLeave ~= nil and restLeave.ButtonComponent ~= nil then
|
if restLeave ~= nil and (restLeave.ButtonComponent ~= nil or restLeave:AddComponent("ButtonComponent") ~= nil) then
|
||||||
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||||
end
|
end
|
||||||
for i = 1, ${MAX_MONSTERS} do
|
for i = 1, ${MAX_MONSTERS} do
|
||||||
local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i))
|
local ms = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i))
|
||||||
if ms ~= nil and ms.ButtonComponent ~= nil then
|
if ms ~= nil and (ms.ButtonComponent ~= nil or ms:AddComponent("ButtonComponent") ~= nil) then
|
||||||
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
|
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for i = 1, 10 do
|
for i = 1, 10 do
|
||||||
local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
|
local rs = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
|
||||||
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
|
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
|
||||||
local idx = i
|
local idx = i
|
||||||
rs:ConnectEvent(UITouchEnterEvent, function()
|
rs:ConnectEvent(UITouchEnterEvent, function()
|
||||||
@@ -156,7 +170,7 @@ for i = 1, 10 do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
for i = 1, 5 do
|
for i = 1, 5 do
|
||||||
local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
|
local ps = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
|
||||||
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
|
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
|
||||||
local idx = i
|
local idx = i
|
||||||
ps:ConnectEvent(UITouchEnterEvent, function()
|
ps:ConnectEvent(UITouchEnterEvent, function()
|
||||||
@@ -170,42 +184,42 @@ for i = 1, 5 do
|
|||||||
ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end)
|
ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use")
|
local pmUse = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Use")
|
||||||
if pmUse ~= nil and pmUse.ButtonComponent ~= nil then
|
if pmUse ~= nil and (pmUse.ButtonComponent ~= nil or pmUse:AddComponent("ButtonComponent") ~= nil) then
|
||||||
pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end)
|
pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end)
|
||||||
end
|
end
|
||||||
local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss")
|
local pmToss = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Toss")
|
||||||
if pmToss ~= nil and pmToss.ButtonComponent ~= nil then
|
if pmToss ~= nil and (pmToss.ButtonComponent ~= nil or pmToss:AddComponent("ButtonComponent") ~= nil) then
|
||||||
pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end)
|
pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end)
|
||||||
end
|
end
|
||||||
local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close")
|
local pmClose = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Close")
|
||||||
if pmClose ~= nil and pmClose.ButtonComponent ~= nil then
|
if pmClose ~= nil and (pmClose.ButtonComponent ~= nil or pmClose:AddComponent("ButtonComponent") ~= nil) then
|
||||||
pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end)
|
pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end)
|
||||||
end
|
end
|
||||||
local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
local shopPotion = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Potion")
|
||||||
if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then
|
if shopPotion ~= nil and (shopPotion.ButtonComponent ~= nil or shopPotion:AddComponent("ButtonComponent") ~= nil) then
|
||||||
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
|
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
|
||||||
end
|
end
|
||||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
|
||||||
if chest ~= nil and chest.ButtonComponent ~= nil then
|
if chest ~= nil and (chest.ButtonComponent ~= nil or chest:AddComponent("ButtonComponent") ~= nil) then
|
||||||
chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end)
|
chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end)
|
||||||
end
|
end
|
||||||
local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave")
|
local treasureLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Leave")
|
||||||
if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then
|
if treasureLeave ~= nil and (treasureLeave.ButtonComponent ~= nil or treasureLeave:AddComponent("ButtonComponent") ~= nil) then
|
||||||
treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||||
end
|
end
|
||||||
local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton")
|
local jcRelic = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobChoiceHud/RelicButton")
|
||||||
if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then
|
if jcRelic ~= nil and (jcRelic.ButtonComponent ~= nil or jcRelic:AddComponent("ButtonComponent") ~= nil) then
|
||||||
jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end)
|
jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end)
|
||||||
end
|
end
|
||||||
local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton")
|
local jcJob = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobChoiceHud/JobButton")
|
||||||
if jcJob ~= nil and jcJob.ButtonComponent ~= nil then
|
if jcJob ~= nil and (jcJob.ButtonComponent ~= nil or jcJob:AddComponent("ButtonComponent") ~= nil) then
|
||||||
jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end)
|
jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end)
|
||||||
end
|
end
|
||||||
for i = 1, 3 do
|
for i = 1, 3 do
|
||||||
local slotIdx = i
|
local slotIdx = i
|
||||||
local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i))
|
local jb = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i))
|
||||||
if jb ~= nil and jb.ButtonComponent ~= nil then
|
if jb ~= nil and (jb.ButtonComponent ~= nil or jb:AddComponent("ButtonComponent") ~= nil) then
|
||||||
jb:ConnectEvent(ButtonClickEvent, function()
|
jb:ConnectEvent(ButtonClickEvent, function()
|
||||||
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
|
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
|
||||||
self:SetJob(self.JobOpts[slotIdx].id)
|
self:SetJob(self.JobOpts[slotIdx].id)
|
||||||
@@ -214,13 +228,43 @@ for i = 1, 3 do
|
|||||||
end
|
end
|
||||||
end`),
|
end`),
|
||||||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||||||
|
self.RetainSelectActive = false
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.TurnAttackCardsPlayed = 0
|
||||||
|
self.TurnDiscardedCards = 0
|
||||||
|
self.TurnCardsPlayedThisTurn = 0
|
||||||
|
self.DamageDealtThisTurn = 0
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self.SkillCostReductionThisTurn = 0
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
self.Energy = self.MaxEnergy
|
self.Energy = self.MaxEnergy
|
||||||
|
self.BlockGainMultiplier = 1
|
||||||
self:ApplyRelics("turnStart")
|
self:ApplyRelics("turnStart")
|
||||||
self.PlayerBlock = 0
|
if self.NextTurnKeepBlock == true then
|
||||||
|
self.NextTurnKeepBlock = false
|
||||||
|
else
|
||||||
|
self.PlayerBlock = 0
|
||||||
|
end
|
||||||
if self.ClayBlockNext > 0 then
|
if self.ClayBlockNext > 0 then
|
||||||
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
|
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
|
||||||
self.ClayBlockNext = 0
|
self.ClayBlockNext = 0
|
||||||
end
|
end
|
||||||
|
self.TurnAttackMultiplier = self.NextTurnAttackMultiplier or 1
|
||||||
|
self.NextTurnAttackMultiplier = 1
|
||||||
|
self.CardsDrawnThisCombat = self.CardsDrawnThisCombat or 0
|
||||||
|
self.ShivFirstDamageBonusUsed = false
|
||||||
|
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||||
|
self.DrawDamageThisTurn = 0
|
||||||
|
self.DrawPoisonThisTurn = 0
|
||||||
|
self.ShivAoeThisCombat = false
|
||||||
|
self.SkillSlyOnPlayCards = self.SkillSlyOnPlayCards or {}
|
||||||
|
self.TurnSkillSlyCards = {}
|
||||||
|
self.EnemyStrengthLossThisTurn = 0
|
||||||
|
self.HandCostZeroThisTurn = false
|
||||||
|
self.DrawDisabledThisTurn = false
|
||||||
|
local powerTurnDraw = 0
|
||||||
|
local powerTurnDiscard = 0
|
||||||
if self.PlayerPowers ~= nil then
|
if self.PlayerPowers ~= nil then
|
||||||
for i = 1, #self.PlayerPowers do
|
for i = 1, #self.PlayerPowers do
|
||||||
local pc = self.Cards[self.PlayerPowers[i]]
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
@@ -231,16 +275,124 @@ if self.PlayerPowers ~= nil then
|
|||||||
self.Energy = self.Energy + pc.value
|
self.Energy = self.Energy + pc.value
|
||||||
elseif pc.powerEffect == "blockPerTurn" then
|
elseif pc.powerEffect == "blockPerTurn" then
|
||||||
self.PlayerBlock = self.PlayerBlock + pc.value
|
self.PlayerBlock = self.PlayerBlock + pc.value
|
||||||
|
elseif pc.powerEffect == "poisonPerTurn" then
|
||||||
|
if self.Monsters ~= nil then
|
||||||
|
for j = 1, #self.Monsters do
|
||||||
|
local tm = self.Monsters[j]
|
||||||
|
if tm ~= nil and tm.alive == true then
|
||||||
|
self:ApplyPoisonToMonster(tm, pc.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif pc.powerEffect == "damagePerTurn" then
|
||||||
|
if self.Monsters ~= nil then
|
||||||
|
self:PlayAoeFx(pc.fx or pc.image, pc.value or 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if pc.turnStartShiv ~= nil then
|
if pc.turnStartShiv ~= nil then
|
||||||
self:AddCardsToHand("Shiv", pc.turnStartShiv)
|
self:AddCardsToHand("Shiv", pc.turnStartShiv)
|
||||||
end
|
end
|
||||||
|
if pc.turnStartDraw ~= nil then
|
||||||
|
powerTurnDraw = powerTurnDraw + pc.turnStartDraw
|
||||||
|
end
|
||||||
|
if pc.turnStartDiscard ~= nil then
|
||||||
|
powerTurnDiscard = powerTurnDiscard + pc.turnStartDiscard
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:DrawCards(5)
|
if self.NextTurnBlock ~= nil and self.NextTurnBlock > 0 then
|
||||||
|
self:AddCardBlock(self.NextTurnBlock)
|
||||||
|
self.NextTurnBlock = 0
|
||||||
|
end
|
||||||
|
if self.NextTurnAddCards ~= nil then
|
||||||
|
for i = 1, #self.NextTurnAddCards do
|
||||||
|
local entry = self.NextTurnAddCards[i]
|
||||||
|
if entry ~= nil and entry.cardId ~= nil and entry.amount ~= nil and entry.amount > 0 then
|
||||||
|
self:AddCardsToHand(entry.cardId, entry.amount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
|
end
|
||||||
|
local drawN = 5 + (self.NextTurnDraw or 0) + powerTurnDraw
|
||||||
|
self.NextTurnDraw = 0
|
||||||
|
self:DrawCards(drawN)
|
||||||
self:RenderHand(true)
|
self:RenderHand(true)
|
||||||
|
self:RenderCombat()
|
||||||
|
if powerTurnDiscard > 0 then
|
||||||
|
self:BeginDiscardSelection({ discard = math.min(powerTurnDiscard, #self.Hand) })
|
||||||
|
return
|
||||||
|
end
|
||||||
self:RenderCombat()`),
|
self:RenderCombat()`),
|
||||||
|
method('PrepareCombatDrawPile', `if self.DrawPile == nil or self.Cards == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local rest = {}
|
||||||
|
local innate = {}
|
||||||
|
for i = 1, #self.DrawPile do
|
||||||
|
local cardId = self.DrawPile[i]
|
||||||
|
local c = self.Cards[cardId]
|
||||||
|
if c ~= nil and c.innate == true then
|
||||||
|
table.insert(innate, cardId)
|
||||||
|
else
|
||||||
|
table.insert(rest, cardId)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.DrawPile = {}
|
||||||
|
for i = 1, #rest do
|
||||||
|
table.insert(self.DrawPile, rest[i])
|
||||||
|
end
|
||||||
|
for i = 1, #innate do
|
||||||
|
table.insert(self.DrawPile, innate[i])
|
||||||
|
end`, []),
|
||||||
|
method('HasPowerEffect', `if self.PlayerPowers == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil and pc.powerEffect == effect then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'effect' }], 0, 'boolean'),
|
||||||
|
method('HasPowerField', `if self.PlayerPowers == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil and pc[field] ~= nil and pc[field] ~= 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'field' }], 0, 'boolean'),
|
||||||
|
method('AddPowerFieldTotal', `local total = 0
|
||||||
|
if self.PlayerPowers == nil then
|
||||||
|
return total
|
||||||
|
end
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil and pc[field] ~= nil then
|
||||||
|
total = total + pc[field]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return total`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'field' }], 0, 'number'),
|
||||||
|
method('ShouldOfferRetain', `if self:HasPowerEffect("retainOne") ~= true then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.Hand == nil or #self.Hand <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for i = 1, #self.Hand do
|
||||||
|
local c = self.Cards[self.Hand[i]]
|
||||||
|
if c ~= nil and c.retain ~= true then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false`, [], 0, 'boolean'),
|
||||||
|
method('BeginRetainSelection', `self.RetainSelectActive = true
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
|
self:Toast("보존할 카드를 선택하세요")
|
||||||
|
self:RenderHand(false)`, []),
|
||||||
method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -248,6 +400,24 @@ if self:IsDiscardSelecting() == true then
|
|||||||
self:Toast("버릴 카드를 먼저 선택하세요")
|
self:Toast("버릴 카드를 먼저 선택하세요")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:IsRetainSelecting() == true then
|
||||||
|
self:FinishPlayerTurn(0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self:IsReserveSelecting() == true then
|
||||||
|
self:Toast("예약할 카드를 먼저 선택하세요")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self:ShouldOfferRetain() == true then
|
||||||
|
self:BeginRetainSelection()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:FinishPlayerTurn(0)`),
|
||||||
|
method('FinishPlayerTurn', `self.RetainSelectActive = false
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
local burn = 0
|
local burn = 0
|
||||||
for bi = 1, #self.Hand do
|
for bi = 1, #self.Hand do
|
||||||
\tlocal hc = self.Cards[self.Hand[bi]]
|
\tlocal hc = self.Cards[self.Hand[bi]]
|
||||||
@@ -263,19 +433,38 @@ local kept = {}
|
|||||||
for i = 1, #self.Hand do
|
for i = 1, #self.Hand do
|
||||||
\tlocal cardId = self.Hand[i]
|
\tlocal cardId = self.Hand[i]
|
||||||
\tlocal c = self.Cards[cardId]
|
\tlocal c = self.Cards[cardId]
|
||||||
\tif c ~= nil and c.retain == true then
|
\tif c ~= nil and (c.retain == true or (c.class == "shiv" and self:HasPowerField("shivRetain") == true) or i == retainSlot) then
|
||||||
\t\ttable.insert(kept, cardId)
|
\t\ttable.insert(kept, cardId)
|
||||||
\telse
|
\telse
|
||||||
\t\ttable.insert(self.DiscardPile, cardId)
|
\t\ttable.insert(self.DiscardPile, cardId)
|
||||||
\tend
|
\tend
|
||||||
end
|
end
|
||||||
self.Hand = kept
|
self.Hand = kept
|
||||||
|
if self.PlayerPowers ~= nil then
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil and pc.endTurnDexLoss ~= nil and pc.endTurnDexLoss > 0 then
|
||||||
|
self.PlayerDex = self.PlayerDex - pc.endTurnDexLoss
|
||||||
|
if self.PlayerDex < 0 then self.PlayerDex = 0 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then
|
||||||
|
self.PlayerIntangible = self.PlayerIntangible - 1
|
||||||
|
if self.PlayerIntangible < 0 then self.PlayerIntangible = 0 end
|
||||||
|
end
|
||||||
if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end
|
if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end
|
||||||
if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
|
if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
|
||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
self:EnemyTurn()`),
|
self.TurnSkillSlyCards = {}
|
||||||
method('DrawCards', `local drawnSlots = {}
|
self:EnemyTurn()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'retainSlot' }]),
|
||||||
|
method('DrawCards', `local drawnSlots = {}
|
||||||
|
local drawnCards = {}
|
||||||
|
local drewAny = false
|
||||||
|
if self.DrawDisabledThisTurn == true then
|
||||||
|
\treturn drawnCards
|
||||||
|
end
|
||||||
for i = 1, amount do
|
for i = 1, amount do
|
||||||
\tif #self.DrawPile <= 0 then
|
\tif #self.DrawPile <= 0 then
|
||||||
\t\tself:RecycleDiscardIntoDraw()
|
\t\tself:RecycleDiscardIntoDraw()
|
||||||
@@ -284,28 +473,33 @@ for i = 1, amount do
|
|||||||
\t\tbreak
|
\t\tbreak
|
||||||
\tend
|
\tend
|
||||||
\tlocal cardId = table.remove(self.DrawPile)
|
\tlocal cardId = table.remove(self.DrawPile)
|
||||||
|
\ttable.insert(drawnCards, cardId)
|
||||||
|
\tself.CardsDrawnThisCombat = (self.CardsDrawnThisCombat or 0) + 1
|
||||||
|
\tself:ApplyDrawTrigger()
|
||||||
\tif #self.Hand >= 10 then
|
\tif #self.Hand >= 10 then
|
||||||
\t\ttable.insert(self.DiscardPile, cardId)
|
\t\ttable.insert(self.DiscardPile, cardId)
|
||||||
\t\tself:TriggerSly(cardId)
|
\t\tself:TriggerSly(cardId)
|
||||||
\telse
|
\telse
|
||||||
\t\ttable.insert(self.Hand, cardId)
|
\t\ttable.insert(self.Hand, cardId)
|
||||||
\t\tif #self.Hand <= 5 then
|
\t\tdrewAny = true
|
||||||
\t\t\ttable.insert(drawnSlots, #self.Hand)
|
\t\ttable.insert(drawnSlots, #self.Hand)
|
||||||
\t\tend
|
|
||||||
\tend
|
\tend
|
||||||
end
|
end
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
if animate == true and #drawnSlots > 0 then
|
if drewAny == true then
|
||||||
\tself:RenderHand(false)
|
\tself:RenderHand(false)
|
||||||
|
end
|
||||||
|
if animate == true and #drawnSlots > 0 then
|
||||||
\tlocal drawStart = Vector2(-590, 8)
|
\tlocal drawStart = Vector2(-590, 8)
|
||||||
\tfor i = 1, #drawnSlots do
|
\tfor i = 1, #drawnSlots do
|
||||||
\t\tlocal slot = drawnSlots[i]
|
\t\tlocal slot = drawnSlots[i]
|
||||||
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
|
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
|
||||||
\tend
|
\tend
|
||||||
|
return drawnCards
|
||||||
end`, [
|
end`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
|
||||||
]),
|
], 0, 'any'),
|
||||||
method('AddCardsToHand', `if self.Hand == nil then
|
method('AddCardsToHand', `if self.Hand == nil then
|
||||||
self.Hand = {}
|
self.Hand = {}
|
||||||
end
|
end
|
||||||
@@ -333,11 +527,11 @@ for i = 1, #self.DiscardPile do
|
|||||||
end
|
end
|
||||||
self.DiscardPile = {}
|
self.DiscardPile = {}
|
||||||
self:Shuffle(self.DrawPile)`),
|
self:Shuffle(self.DrawPile)`),
|
||||||
method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", self:FormatNumber(#self.DrawPile))
|
method('RenderPiles', `self:SetText("/ui/RunUIGroup/DeckHud/DrawPile/Count", self:FormatNumber(#self.DrawPile))
|
||||||
self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", self:FormatNumber(#self.DiscardPile))
|
self:SetText("/ui/RunUIGroup/DeckHud/DiscardPile/Count", self:FormatNumber(#self.DiscardPile))
|
||||||
self:SetText("/ui/DefaultGroup/DeckHud/ExhaustPile/Count", self:FormatNumber(#(self.ExhaustPile or {})))
|
self:SetText("/ui/RunUIGroup/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))
|
self:SetText("/ui/RunUIGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
|
||||||
local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
local inspect = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||||
if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then
|
if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then
|
||||||
self:OpenDeckInspect(self.DeckInspectKind)
|
self:OpenDeckInspect(self.DeckInspectKind)
|
||||||
end`),
|
end`),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const deckViewMethods = [
|
|||||||
method('OpenDeckInspect', `self.DeckInspectKind = kind
|
method('OpenDeckInspect', `self.DeckInspectKind = kind
|
||||||
if self.DeckAllOpen == true then
|
if self.DeckAllOpen == true then
|
||||||
self.DeckAllOpen = false
|
self.DeckAllOpen = false
|
||||||
local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
local allHud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||||
if allHud ~= nil then
|
if allHud ~= nil then
|
||||||
allHud.Enable = false
|
allHud.Enable = false
|
||||||
end
|
end
|
||||||
@@ -24,12 +24,12 @@ else
|
|||||||
title = "뽑을 덱"
|
title = "뽑을 덱"
|
||||||
end
|
end
|
||||||
self:RenderDeckInspect(pile, title)
|
self:RenderDeckInspect(pile, title)
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = true
|
hud.Enable = true
|
||||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||||||
method('CloseDeckInspect', `self.DeckInspectKind = ""
|
method('CloseDeckInspect', `self.DeckInspectKind = ""
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = false
|
hud.Enable = false
|
||||||
end`),
|
end`),
|
||||||
@@ -41,51 +41,46 @@ local suffix = " (" .. tostring(count) .. ")"
|
|||||||
if count > 60 then
|
if count > 60 then
|
||||||
suffix = suffix .. " - 60장까지 표시"
|
suffix = suffix .. " - 60장까지 표시"
|
||||||
end
|
end
|
||||||
self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix)
|
self:SetText("/ui/DeckUIGroup/DeckInspectHud/Title", title .. suffix)
|
||||||
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty")
|
self:SetEntityEnabled("/ui/DeckUIGroup/DeckInspectHud/Empty", count <= 0)
|
||||||
if empty ~= nil then
|
|
||||||
empty.Enable = count <= 0
|
|
||||||
end
|
|
||||||
for i = 1, 60 do
|
for i = 1, 60 do
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i))
|
local path = "/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. tostring(i)
|
||||||
if e ~= nil then
|
local cardId = nil
|
||||||
local cardId = nil
|
if pile ~= nil then
|
||||||
if pile ~= nil then
|
cardId = pile[i]
|
||||||
cardId = pile[i]
|
end
|
||||||
end
|
if cardId == nil then
|
||||||
if cardId == nil then
|
self:SetEntityEnabled(path, false)
|
||||||
e.Enable = false
|
else
|
||||||
else
|
self:SetEntityEnabled(path, true)
|
||||||
e.Enable = true
|
self:ApplyInspectCardVisual(i, cardId)
|
||||||
self:ApplyInspectCardVisual(i, cardId)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end`, [
|
end`, [
|
||||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
|
||||||
]),
|
]),
|
||||||
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
|
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/WarriorTab")
|
method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/WarriorTab")
|
||||||
if warriorTab ~= nil and warriorTab.ButtonComponent ~= nil then
|
if warriorTab ~= nil and (warriorTab.ButtonComponent ~= nil or warriorTab:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.WarriorDeckTabHandler ~= nil then
|
if self.WarriorDeckTabHandler ~= nil then
|
||||||
warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler)
|
warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler)
|
||||||
self.WarriorDeckTabHandler = nil
|
self.WarriorDeckTabHandler = nil
|
||||||
end
|
end
|
||||||
self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end)
|
self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end)
|
||||||
end
|
end
|
||||||
local thiefTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/ThiefTab")
|
local thiefTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/ThiefTab")
|
||||||
if thiefTab ~= nil and thiefTab.ButtonComponent ~= nil then
|
if thiefTab ~= nil and (thiefTab.ButtonComponent ~= nil or thiefTab:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.ThiefDeckTabHandler ~= nil then
|
if self.ThiefDeckTabHandler ~= nil then
|
||||||
thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler)
|
thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler)
|
||||||
self.ThiefDeckTabHandler = nil
|
self.ThiefDeckTabHandler = nil
|
||||||
end
|
end
|
||||||
self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("bandit") end)
|
self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("rogue") end)
|
||||||
end
|
end
|
||||||
local mageTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/MageTab")
|
local mageTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/MageTab")
|
||||||
if mageTab ~= nil and mageTab.ButtonComponent ~= nil then
|
if mageTab ~= nil and (mageTab.ButtonComponent ~= nil or mageTab:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.MageDeckTabHandler ~= nil then
|
if self.MageDeckTabHandler ~= nil then
|
||||||
mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler)
|
mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler)
|
||||||
self.MageDeckTabHandler = nil
|
self.MageDeckTabHandler = nil
|
||||||
@@ -94,43 +89,60 @@ if mageTab ~= nil and mageTab.ButtonComponent ~= nil then
|
|||||||
end`),
|
end`),
|
||||||
method('OpenClassDeck', `self.CodexMode = false
|
method('OpenClassDeck', `self.CodexMode = false
|
||||||
self.ClassDeckMode = true
|
self.ClassDeckMode = true
|
||||||
|
self.DebugCardPickerMode = false
|
||||||
self.DeckAllOpen = true
|
self.DeckAllOpen = true
|
||||||
self:SetClassDeckTab(className)
|
self:SetClassDeckTab(className)
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = true
|
hud.Enable = true
|
||||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
||||||
|
method('OpenDebugCardPicker', `if self.RunActive ~= true or self.CombatOver == true or self.Hand == nil then
|
||||||
|
self:Toast("전투 중에만 테스트 카드를 추가할 수 있습니다")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local className = self.SelectedClass
|
||||||
|
if className ~= "warrior" and className ~= "magician" and className ~= "rogue" then
|
||||||
|
className = "rogue"
|
||||||
|
end
|
||||||
|
self.CodexMode = false
|
||||||
|
self.ClassDeckMode = true
|
||||||
|
self.DebugCardPickerMode = true
|
||||||
|
self.DeckAllOpen = true
|
||||||
|
self:SetClassDeckTab(className)
|
||||||
|
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||||
|
if hud ~= nil then
|
||||||
|
hud.Enable = true
|
||||||
|
end
|
||||||
|
self:Toast("테스트 카드 추가 모드")`),
|
||||||
method('SetClassDeckTab', `if self.ClassDeckMode ~= true then
|
method('SetClassDeckTab', `if self.ClassDeckMode ~= true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.ClassDeckCards = {}
|
self.ClassDeckCards = {}
|
||||||
self.ClassDeckTitle = "직업 덱"
|
self.ClassDeckTitle = "직업 덱"
|
||||||
if className ~= "warrior" and className ~= "magician" and className ~= "bandit" then
|
if className ~= "warrior" and className ~= "magician" and className ~= "rogue" then
|
||||||
className = "bandit"
|
className = "rogue"
|
||||||
end
|
end
|
||||||
self.ClassDeckClass = className
|
self.ClassDeckClass = className
|
||||||
local allowed = {}
|
local allowed = {}
|
||||||
|
local group = nil
|
||||||
|
if self.ClassGroups ~= nil then
|
||||||
|
group = self.ClassGroups[className]
|
||||||
|
end
|
||||||
|
if group == nil then
|
||||||
|
group = { className }
|
||||||
|
end
|
||||||
|
for i = 1, #group do
|
||||||
|
allowed[group[i]] = true
|
||||||
|
end
|
||||||
if className == "warrior" then
|
if className == "warrior" then
|
||||||
allowed["warrior"] = true
|
|
||||||
allowed["fighter"] = true
|
|
||||||
allowed["page"] = true
|
|
||||||
allowed["spearman"] = true
|
|
||||||
self.ClassDeckTitle = "전사 전체 덱"
|
self.ClassDeckTitle = "전사 전체 덱"
|
||||||
elseif className == "magician" then
|
elseif className == "magician" then
|
||||||
allowed["magician"] = true
|
|
||||||
allowed["firepoison"] = true
|
|
||||||
allowed["icelightning"] = true
|
|
||||||
allowed["cleric"] = true
|
|
||||||
self.ClassDeckTitle = "마법사 전체 덱"
|
self.ClassDeckTitle = "마법사 전체 덱"
|
||||||
else
|
else
|
||||||
allowed["bandit"] = true
|
|
||||||
allowed["shiv"] = true
|
|
||||||
allowed["poisoner"] = true
|
|
||||||
allowed["trickster"] = true
|
|
||||||
self.ClassDeckTitle = "도적 전체 덱"
|
self.ClassDeckTitle = "도적 전체 덱"
|
||||||
end
|
end
|
||||||
for id, c in pairs(self.Cards) do
|
for id, c in pairs(self.Cards) do
|
||||||
if c ~= nil and c.curse ~= true and allowed[c.class] == true then
|
if c ~= nil and c.curse ~= true and c.token ~= true and allowed[c.class] == true then
|
||||||
table.insert(self.ClassDeckCards, id)
|
table.insert(self.ClassDeckCards, id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -147,39 +159,38 @@ end)
|
|||||||
self:RenderAllDeck()
|
self:RenderAllDeck()
|
||||||
self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
||||||
method('RenderClassDeckTabs', `local tabs = {
|
method('RenderClassDeckTabs', `local tabs = {
|
||||||
{ path = "/ui/DefaultGroup/DeckAllHud/WarriorTab", cls = "warrior" },
|
{ path = "/ui/DeckUIGroup/DeckAllHud/WarriorTab", cls = "warrior" },
|
||||||
{ path = "/ui/DefaultGroup/DeckAllHud/ThiefTab", cls = "bandit" },
|
{ path = "/ui/DeckUIGroup/DeckAllHud/ThiefTab", cls = "rogue" },
|
||||||
{ path = "/ui/DefaultGroup/DeckAllHud/MageTab", cls = "magician" },
|
{ path = "/ui/DeckUIGroup/DeckAllHud/MageTab", cls = "magician" },
|
||||||
}
|
}
|
||||||
for i = 1, #tabs do
|
for i = 1, #tabs do
|
||||||
|
self:SetEntityEnabled(tabs[i].path, self.ClassDeckMode == true)
|
||||||
local e = _EntityService:GetEntityByPath(tabs[i].path)
|
local e = _EntityService:GetEntityByPath(tabs[i].path)
|
||||||
if e ~= nil then
|
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||||
e.Enable = self.ClassDeckMode == true
|
if self.ClassDeckClass == tabs[i].cls then
|
||||||
if e.SpriteGUIRendererComponent ~= nil then
|
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1)
|
||||||
if self.ClassDeckClass == tabs[i].cls then
|
else
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1)
|
e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1)
|
||||||
else
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end`),
|
end`),
|
||||||
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||||
if inspectHud ~= nil then
|
if inspectHud ~= nil then
|
||||||
inspectHud.Enable = false
|
inspectHud.Enable = false
|
||||||
end
|
end
|
||||||
self.DeckInspectKind = ""
|
self.DeckInspectKind = ""
|
||||||
self.ClassDeckMode = false
|
self.ClassDeckMode = false
|
||||||
self.ClassDeckClass = ""
|
self.ClassDeckClass = ""
|
||||||
|
self.DebugCardPickerMode = false
|
||||||
self:RenderClassDeckTabs()
|
self:RenderClassDeckTabs()
|
||||||
self.DeckAllOpen = true
|
self.DeckAllOpen = true
|
||||||
self:RenderAllDeck()
|
self:RenderAllDeck()
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = true
|
hud.Enable = true
|
||||||
end`),
|
end`),
|
||||||
method('CloseAllDeck', `self.DeckAllOpen = false
|
method('CloseAllDeck', `self.DeckAllOpen = false
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = false
|
hud.Enable = false
|
||||||
end
|
end
|
||||||
@@ -189,6 +200,7 @@ if self.ClassDeckMode == true then
|
|||||||
self.ClassDeckTitle = ""
|
self.ClassDeckTitle = ""
|
||||||
self.ClassDeckClass = ""
|
self.ClassDeckClass = ""
|
||||||
end
|
end
|
||||||
|
self.DebugCardPickerMode = false
|
||||||
self:RenderClassDeckTabs()
|
self:RenderClassDeckTabs()
|
||||||
if self.CodexMode == true then
|
if self.CodexMode == true then
|
||||||
self.CodexMode = false
|
self.CodexMode = false
|
||||||
@@ -199,31 +211,46 @@ local title = "모든 덱"
|
|||||||
if self.ClassDeckMode == true then
|
if self.ClassDeckMode == true then
|
||||||
pile = self.ClassDeckCards or {}
|
pile = self.ClassDeckCards or {}
|
||||||
title = self.ClassDeckTitle
|
title = self.ClassDeckTitle
|
||||||
|
if self.DebugCardPickerMode == true then
|
||||||
|
title = title .. " - 테스트 카드 추가"
|
||||||
|
end
|
||||||
elseif self.CodexMode == true then
|
elseif self.CodexMode == true then
|
||||||
pile = self.CodexCards or {}
|
pile = self.CodexCards or {}
|
||||||
title = "카드 도감"
|
title = "카드 도감"
|
||||||
end
|
end
|
||||||
local count = #pile
|
local count = #pile
|
||||||
self:SetText("/ui/DefaultGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")")
|
self:SetText("/ui/DeckUIGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")")
|
||||||
self:RenderClassDeckTabs()
|
self:RenderClassDeckTabs()
|
||||||
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Empty")
|
self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud/Empty", count <= 0)
|
||||||
if empty ~= nil then
|
|
||||||
empty.Enable = count <= 0
|
|
||||||
end
|
|
||||||
for i = 1, 120 do
|
for i = 1, 120 do
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(i))
|
local path = "/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(i)
|
||||||
if e ~= nil then
|
local cardId = pile[i]
|
||||||
local cardId = pile[i]
|
if cardId == nil then
|
||||||
if cardId == nil then
|
self:SetEntityEnabled(path, false)
|
||||||
e.Enable = false
|
else
|
||||||
else
|
self:SetEntityEnabled(path, true)
|
||||||
e.Enable = true
|
self:ApplyAllDeckCardVisual(i, cardId)
|
||||||
self:ApplyAllDeckCardVisual(i, cardId)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end`),
|
end`),
|
||||||
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
|
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
|
method('OnAllDeckCardButton', `if self.DebugCardPickerMode ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self.ClassDeckCards == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cardId = self.ClassDeckCards[slot]
|
||||||
|
if cardId == nil or self.Cards == nil or self.Cards[cardId] == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:AddCardsToHand(cardId, 1)
|
||||||
|
local c = self.Cards[cardId]
|
||||||
|
local name = cardId
|
||||||
|
if c.name ~= nil then name = c.name end
|
||||||
|
self:Toast("테스트 카드 추가: " .. name)`, [
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,6 +3,42 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
|
|||||||
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 { 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 = [
|
export const handMethods = [
|
||||||
|
method('ApplyDrawTrigger', `if self.Monsters == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local drawDamage = self:AddPowerFieldTotal("drawDamage") + (self.DrawDamageThisTurn or 0)
|
||||||
|
local drawPoison = self:AddPowerFieldTotal("drawPoison") + (self.DrawPoisonThisTurn or 0)
|
||||||
|
if (drawDamage ~= nil and drawDamage > 0) or (drawPoison ~= nil and drawPoison > 0) then
|
||||||
|
for mi = 1, #self.Monsters do
|
||||||
|
local m2 = self.Monsters[mi]
|
||||||
|
if m2 ~= nil and m2.alive == true then
|
||||||
|
local dmg = drawDamage or 0
|
||||||
|
if m2.vuln > 0 then
|
||||||
|
dmg = math.floor(dmg * 1.5)
|
||||||
|
end
|
||||||
|
if m2.block > 0 then
|
||||||
|
local absorbed = math.min(m2.block, dmg)
|
||||||
|
m2.block = m2.block - absorbed
|
||||||
|
dmg = dmg - absorbed
|
||||||
|
end
|
||||||
|
if drawPoison ~= nil and drawPoison > 0 then
|
||||||
|
self:ApplyPoisonToMonster(m2, drawPoison)
|
||||||
|
end
|
||||||
|
if dmg > 0 then
|
||||||
|
m2.hp = m2.hp - dmg
|
||||||
|
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + dmg
|
||||||
|
end
|
||||||
|
self:ShowDmgPop(mi, dmg)
|
||||||
|
self:MonsterHitMotion(mi)
|
||||||
|
if m2.hp <= 0 then
|
||||||
|
m2.hp = 0
|
||||||
|
self:KillMonster(m2.slot)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:RenderCombat()
|
||||||
|
self:CheckCombatEnd()
|
||||||
|
end`),
|
||||||
method('GetHandSlotX', `local n = 0
|
method('GetHandSlotX', `local n = 0
|
||||||
if self.Hand ~= nil then
|
if self.Hand ~= nil then
|
||||||
n = #self.Hand
|
n = #self.Hand
|
||||||
@@ -20,7 +56,7 @@ if n > 8 then spacing = math.floor(1400 / n) end
|
|||||||
local startX = -((n - 1) * spacing) / 2
|
local startX = -((n - 1) * spacing) / 2
|
||||||
local drawStart = Vector2(-590, 8)
|
local drawStart = Vector2(-590, 8)
|
||||||
for i = 1, 10 do
|
for i = 1, 10 do
|
||||||
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i))
|
||||||
\tif cardEntity ~= nil then
|
\tif cardEntity ~= nil then
|
||||||
\t\tlocal cardId = self.Hand[i]
|
\t\tlocal cardId = self.Hand[i]
|
||||||
\t\tif cardId == nil then
|
\t\tif cardId == nil then
|
||||||
@@ -60,7 +96,7 @@ if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
|||||||
end
|
end
|
||||||
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||||||
self:SetText(base .. "/Name", c.name)
|
self:SetText(base .. "/Name", c.name)
|
||||||
self:SetText(base .. "/Desc", c.desc)
|
self:SetText(base .. "/Desc", self:FormatCardDescription(c.desc))
|
||||||
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
||||||
if art ~= nil then
|
if art ~= nil then
|
||||||
if c.image ~= nil and c.image ~= "" then
|
if c.image ~= nil and c.image ~= "" then
|
||||||
@@ -81,11 +117,11 @@ local xs = {}
|
|||||||
local baseY = 0
|
local baseY = 0
|
||||||
local hoverIndex = 0
|
local hoverIndex = 0
|
||||||
local push = 110
|
local push = 110
|
||||||
if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then
|
if string.find(path, "/ui/RunUIGroup/CardHand/Card") == 1 then
|
||||||
if self.DragSlot ~= nil and self.DragSlot > 0 then
|
if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
prefix = "/ui/DefaultGroup/CardHand/Card"
|
prefix = "/ui/RunUIGroup/CardHand/Card"
|
||||||
count = 0
|
count = 0
|
||||||
if self.Hand ~= nil then count = #self.Hand end
|
if self.Hand ~= nil then count = #self.Hand end
|
||||||
for i = 1, count do
|
for i = 1, count do
|
||||||
@@ -93,14 +129,14 @@ if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then
|
|||||||
end
|
end
|
||||||
baseY = 0
|
baseY = 0
|
||||||
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
||||||
elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then
|
elseif string.find(path, "/ui/RunUIGroup/RewardHud/Reward") == 1 then
|
||||||
prefix = "/ui/DefaultGroup/RewardHud/Reward"
|
prefix = "/ui/RunUIGroup/RewardHud/Reward"
|
||||||
count = 3
|
count = 3
|
||||||
xs = { -300, 0, 300 }
|
xs = { -300, 0, 300 }
|
||||||
baseY = 0
|
baseY = 0
|
||||||
hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0
|
hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0
|
||||||
elseif string.find(path, "/ui/DefaultGroup/ShopHud/Card") == 1 then
|
elseif string.find(path, "/ui/RunUIGroup/ShopHud/Card") == 1 then
|
||||||
prefix = "/ui/DefaultGroup/ShopHud/Card"
|
prefix = "/ui/RunUIGroup/ShopHud/Card"
|
||||||
count = 3
|
count = 3
|
||||||
xs = { -300, 0, 300 }
|
xs = { -300, 0, 300 }
|
||||||
baseY = 20
|
baseY = 20
|
||||||
@@ -159,7 +195,7 @@ self.CardHoverTweenId = eventId`, [
|
|||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' },
|
||||||
]),
|
]),
|
||||||
method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [
|
method('ApplyCardVisual', `self:ApplyCardFace("/ui/RunUIGroup/CardHand/Card" .. tostring(slot), cardId)`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
@@ -181,7 +217,7 @@ if math.abs(n - math.floor(n)) < 0.00001 then
|
|||||||
return string.format("%d", math.floor(n))
|
return string.format("%d", math.floor(n))
|
||||||
end
|
end
|
||||||
return tostring(n)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }], 0, 'string'),
|
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))
|
method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
if cardEntity == nil or cardEntity.UITransformComponent == nil then
|
if cardEntity == nil or cardEntity.UITransformComponent == nil then
|
||||||
\treturn
|
\treturn
|
||||||
end
|
end
|
||||||
@@ -203,15 +239,132 @@ end, 1 / 60)`, [
|
|||||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
|
||||||
]),
|
]),
|
||||||
|
method('AnimateDiscardCards', `if cardIds == nil or slots == nil then
|
||||||
|
\treturn
|
||||||
|
end
|
||||||
|
local target = Vector2(590, 8)
|
||||||
|
local duration = 0.18
|
||||||
|
for i = 1, #cardIds do
|
||||||
|
\tlocal slot = slots[i] or i
|
||||||
|
\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
|
\tif e ~= nil then
|
||||||
|
\t\te.Enable = true
|
||||||
|
\t\tself:ApplyCardFace("/ui/RunUIGroup/CardHand/Card" .. tostring(slot), cardIds[i])
|
||||||
|
\t\tif e.UITransformComponent ~= nil then
|
||||||
|
\t\t\tlocal sx = 0
|
||||||
|
\t\t\tif startXs ~= nil and startXs[i] ~= nil then sx = startXs[i] else sx = self:GetHandSlotX(slot) end
|
||||||
|
\t\t\te.UITransformComponent.anchoredPosition = Vector2(sx, 0)
|
||||||
|
\t\t\te.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||||
|
\t\tend
|
||||||
|
\tend
|
||||||
|
end
|
||||||
|
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.SineEaseIn, t)
|
||||||
|
\tfor i = 1, #cardIds do
|
||||||
|
\t\tlocal slot = slots[i] or i
|
||||||
|
\t\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
|
\t\tif e ~= nil and e.UITransformComponent ~= nil then
|
||||||
|
\t\t\tlocal sx = 0
|
||||||
|
\t\t\tif startXs ~= nil and startXs[i] ~= nil then sx = startXs[i] else sx = self:GetHandSlotX(slot) end
|
||||||
|
\t\t\tlocal x = sx + (target.x - sx) * eased
|
||||||
|
\t\t\tlocal y = 0 + (target.y - 0) * eased
|
||||||
|
\t\t\tlocal s = 1 - 0.25 * eased
|
||||||
|
\t\t\te.UITransformComponent.anchoredPosition = Vector2(x, y)
|
||||||
|
\t\t\te.UITransformComponent.UIScale = Vector3(s, s, 1)
|
||||||
|
\t\tend
|
||||||
|
\tend
|
||||||
|
\tif t >= 1 then
|
||||||
|
\t\t_TimerService:ClearTimer(eventId)
|
||||||
|
\t\tfor i = 1, #cardIds do
|
||||||
|
\t\t\tlocal slot = slots[i] or i
|
||||||
|
\t\t\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
|
\t\t\tif e ~= nil then
|
||||||
|
\t\t\t\tif self.Hand ~= nil and self.Hand[slot] ~= nil then
|
||||||
|
\t\t\t\t\te.Enable = true
|
||||||
|
\t\t\t\t\tself:ApplyCardVisual(slot, self.Hand[slot])
|
||||||
|
\t\t\t\t\tif e.UITransformComponent ~= nil then
|
||||||
|
\t\t\t\t\t\te.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
|
||||||
|
\t\t\t\t\t\te.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||||
|
\t\t\t\t\tend
|
||||||
|
\t\t\t\telse
|
||||||
|
\t\t\t\t\te.Enable = false
|
||||||
|
\t\t\t\tend
|
||||||
|
\t\t\tend
|
||||||
|
\t\tend
|
||||||
|
\tend
|
||||||
|
end, 1 / 60)`, [
|
||||||
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardIds' },
|
||||||
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'startXs' },
|
||||||
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slots' },
|
||||||
|
]),
|
||||||
method('AddCardBlock', `local amount = base or 0
|
method('AddCardBlock', `local amount = base or 0
|
||||||
if amount > 0 and self.PlayerDex ~= nil then
|
if amount > 0 and self.PlayerDex ~= nil then
|
||||||
amount = amount + self.PlayerDex
|
amount = amount + self.PlayerDex
|
||||||
end
|
end
|
||||||
|
if self.BlockGainMultiplier ~= nil and self.BlockGainMultiplier > 1 then
|
||||||
|
amount = amount * self.BlockGainMultiplier
|
||||||
|
end
|
||||||
if amount < 0 then
|
if amount < 0 then
|
||||||
amount = 0
|
amount = 0
|
||||||
end
|
end
|
||||||
self.PlayerBlock = self.PlayerBlock + amount
|
self.PlayerBlock = self.PlayerBlock + amount
|
||||||
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||||
|
method('CountOtherHandSkills', `if self.Hand == nil then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
local n = 0
|
||||||
|
for i = 1, #self.Hand do
|
||||||
|
if i ~= slot then
|
||||||
|
local hc = self.Cards[self.Hand[i]]
|
||||||
|
if hc ~= nil and hc.kind == "Skill" then
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return n`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'),
|
||||||
|
method('AttackBaseForCard', `local base2 = c.damage or 0
|
||||||
|
local otherHand = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
otherHand = #self.Hand - 1
|
||||||
|
if otherHand < 0 then otherHand = 0 end
|
||||||
|
end
|
||||||
|
if c.damagePerOtherHandCard ~= nil then
|
||||||
|
base2 = base2 + otherHand * c.damagePerOtherHandCard
|
||||||
|
end
|
||||||
|
if c.damagePerAttackPlayedThisTurn ~= nil then
|
||||||
|
base2 = base2 + (self.TurnAttackCardsPlayed or 0) * c.damagePerAttackPlayedThisTurn
|
||||||
|
end
|
||||||
|
if c.damagePerDiscardedThisTurn ~= nil then
|
||||||
|
base2 = base2 + (self.TurnDiscardedCards or 0) * c.damagePerDiscardedThisTurn
|
||||||
|
end
|
||||||
|
if c.damagePerSkillInHand ~= nil then
|
||||||
|
base2 = base2 + self:CountOtherHandSkills(slot) * c.damagePerSkillInHand
|
||||||
|
end
|
||||||
|
if c.damagePerCardDrawnThisCombat ~= nil then
|
||||||
|
base2 = base2 + (self.CardsDrawnThisCombat or 0) * c.damagePerCardDrawnThisCombat
|
||||||
|
end
|
||||||
|
if c.kind == "Attack" and (self.TurnCardsPlayedThisTurn or 0) == 0 and c.firstCardDamageBonus ~= nil then
|
||||||
|
base2 = base2 + c.firstCardDamageBonus
|
||||||
|
end
|
||||||
|
if c.class == "shiv" then
|
||||||
|
if self:HasPowerField("shivDamageBonus") == true then
|
||||||
|
base2 = base2 + self:AddPowerFieldTotal("shivDamageBonus")
|
||||||
|
end
|
||||||
|
if self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
|
||||||
|
base2 = base2 + self:AddPowerFieldTotal("firstShivDamageBonus")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if base2 < 0 then
|
||||||
|
base2 = 0
|
||||||
|
end
|
||||||
|
return base2`, [
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
||||||
|
], 0, 'number'),
|
||||||
method('CalcPlayerAttack', `local base2 = base
|
method('CalcPlayerAttack', `local base2 = base
|
||||||
self.FightAttackCount = self.FightAttackCount + 1
|
self.FightAttackCount = self.FightAttackCount + 1
|
||||||
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
|
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
|
||||||
@@ -224,6 +377,9 @@ end
|
|||||||
if self.PlayerWeak > 0 then
|
if self.PlayerWeak > 0 then
|
||||||
dmg = math.floor(dmg * 0.75)
|
dmg = math.floor(dmg * 0.75)
|
||||||
end
|
end
|
||||||
|
if self.TurnAttackMultiplier ~= nil and self.TurnAttackMultiplier > 1 then
|
||||||
|
dmg = dmg * self.TurnAttackMultiplier
|
||||||
|
end
|
||||||
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
|
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
|
||||||
dmg = 5
|
dmg = 5
|
||||||
end
|
end
|
||||||
@@ -231,22 +387,187 @@ if dmg < 0 then
|
|||||||
dmg = 0
|
dmg = 0
|
||||||
end
|
end
|
||||||
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||||
method('ResolveCardEffects', `if c == nil then
|
method('QueueNextTurnAddCard', `if cardId == nil or cardId == "" or amount == nil or amount <= 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self.NextTurnAddCards == nil then
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
|
end
|
||||||
|
for i = 1, #self.NextTurnAddCards do
|
||||||
|
local entry = self.NextTurnAddCards[i]
|
||||||
|
if entry ~= nil and entry.cardId == cardId then
|
||||||
|
entry.amount = (entry.amount or 0) + amount
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(self.NextTurnAddCards, { cardId = cardId, amount = amount })`, [
|
||||||
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
|
]),
|
||||||
|
method('QueueNextTurnEffects', `if c == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if c.nextTurnBlock ~= nil then
|
||||||
|
self.NextTurnBlock = (self.NextTurnBlock or 0) + c.nextTurnBlock
|
||||||
|
end
|
||||||
|
if c.nextTurnDraw ~= nil then
|
||||||
|
self.NextTurnDraw = (self.NextTurnDraw or 0) + c.nextTurnDraw
|
||||||
|
end
|
||||||
|
if c.nextTurnKeepBlock == true then
|
||||||
|
self.NextTurnKeepBlock = true
|
||||||
|
end
|
||||||
|
if c.nextTurnAttackMultiplier ~= nil and c.nextTurnAttackMultiplier > 0 then
|
||||||
|
local cur = self.NextTurnAttackMultiplier or 1
|
||||||
|
self.NextTurnAttackMultiplier = cur * c.nextTurnAttackMultiplier
|
||||||
|
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }]),
|
||||||
|
method('ResolveCardEffects', `if c == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if c.blockGainMultiplier ~= nil and c.blockGainMultiplier > 0 then
|
||||||
|
self.BlockGainMultiplier = (self.BlockGainMultiplier or 1) * c.blockGainMultiplier
|
||||||
|
end
|
||||||
|
if c.nextSkillCostZero == true then
|
||||||
|
self.NextSkillCostZero = true
|
||||||
|
end
|
||||||
|
if c.nextSkillRepeatCount ~= nil and c.nextSkillRepeatCount > 0 then
|
||||||
|
self.NextSkillRepeatCount = (self.NextSkillRepeatCount or 0) + c.nextSkillRepeatCount
|
||||||
|
end
|
||||||
|
if c.skillCostReductionThisTurn ~= nil and c.skillCostReductionThisTurn > 0 then
|
||||||
|
self.SkillCostReductionThisTurn = (self.SkillCostReductionThisTurn or 0) + c.skillCostReductionThisTurn
|
||||||
|
end
|
||||||
|
if c.handCostZeroThisTurn == true then
|
||||||
|
self.HandCostZeroThisTurn = true
|
||||||
|
end
|
||||||
|
if c.drawDisabledThisTurn == true then
|
||||||
|
self.DrawDisabledThisTurn = true
|
||||||
|
end
|
||||||
|
if c.drawDamage ~= nil and c.drawDamage > 0 and c.kind ~= "Power" then
|
||||||
|
self.DrawDamageThisTurn = (self.DrawDamageThisTurn or 0) + c.drawDamage
|
||||||
|
end
|
||||||
|
if c.drawPoison ~= nil and c.drawPoison > 0 and c.kind ~= "Power" then
|
||||||
|
self.DrawPoisonThisTurn = (self.DrawPoisonThisTurn or 0) + c.drawPoison
|
||||||
|
end
|
||||||
|
if c.shivAoe == true and c.kind ~= "Power" then
|
||||||
|
self.ShivAoeThisCombat = true
|
||||||
|
end
|
||||||
|
if c.skillSlyOnPlay == true and c.kind == "Skill" then
|
||||||
|
if self.SkillSlyOnPlayCards == nil then
|
||||||
|
self.SkillSlyOnPlayCards = {}
|
||||||
|
end
|
||||||
|
self.SkillSlyOnPlayCards[cardId] = true
|
||||||
|
end
|
||||||
|
if c.turnHandSlyCount ~= nil and c.turnHandSlyCount > 0 then
|
||||||
|
if self.TurnSkillSlyCards == nil then
|
||||||
|
self.TurnSkillSlyCards = {}
|
||||||
|
end
|
||||||
|
local picked = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
for i = 1, #self.Hand do
|
||||||
|
local hid = self.Hand[i]
|
||||||
|
if hid ~= nil and hid ~= cardId then
|
||||||
|
local hc = self.Cards[hid]
|
||||||
|
if hc ~= nil and hc.kind == "Skill" and self.TurnSkillSlyCards[hid] ~= true and self.SkillSlyOnPlayCards[hid] ~= true and hc.sly ~= true then
|
||||||
|
self.TurnSkillSlyCards[hid] = true
|
||||||
|
picked = picked + 1
|
||||||
|
if picked >= c.turnHandSlyCount then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local xEnergy = energySpent or 0
|
||||||
|
local weakAmount = c.weak or 0
|
||||||
|
local vulnAmount = c.vuln or 0
|
||||||
|
local poisonAmount = c.poison or 0
|
||||||
|
if c.xWeakPerEnergy ~= nil and c.xWeakPerEnergy > 0 then
|
||||||
|
weakAmount = weakAmount + xEnergy * c.xWeakPerEnergy
|
||||||
|
end
|
||||||
if c.kind == "Attack" then
|
if c.kind == "Attack" then
|
||||||
if c.damage ~= nil then
|
if c.damage ~= nil or c.xDamagePerEnergy ~= nil then
|
||||||
self:PlayerAttackMotion()
|
self:PlayerAttackMotion()
|
||||||
|
local baseDmg = self:AttackBaseForCard(slot, c)
|
||||||
|
self.ActiveAttackDamageVsWeakMultiplier = c.attackDamageVsWeakMultiplier or 1
|
||||||
|
if c.xDamagePerEnergy ~= nil and c.xDamagePerEnergy > 0 then
|
||||||
|
baseDmg = xEnergy * c.xDamagePerEnergy
|
||||||
|
end
|
||||||
local total = 0
|
local total = 0
|
||||||
local hitN = c.hits or 1
|
local hitN = c.hits or 1
|
||||||
|
if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then
|
||||||
|
local otherHand = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
otherHand = #self.Hand - 1
|
||||||
|
if otherHand < 0 then otherHand = 0 end
|
||||||
|
end
|
||||||
|
if otherHand >= c.otherHandAtLeast then
|
||||||
|
hitN = hitN + c.bonusHitsWhenOtherHandAtLeast
|
||||||
|
end
|
||||||
|
end
|
||||||
for h = 1, hitN do
|
for h = 1, hitN do
|
||||||
total = total + self:CalcPlayerAttack(c.damage)
|
total = total + self:CalcPlayerAttack(baseDmg)
|
||||||
end
|
end
|
||||||
if c.aoe == true then
|
local useAoe = c.aoe == true
|
||||||
self:PlayAoeFx(c.fx or c.image, total)
|
if c.class == "shiv" and (self.ShivAoeThisCombat == true or self:HasPowerField("shivAoe") == true) then
|
||||||
else
|
useAoe = true
|
||||||
self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true)
|
|
||||||
end
|
end
|
||||||
|
if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
|
||||||
|
self.ShivFirstDamageBonusUsed = true
|
||||||
|
end
|
||||||
|
local function countAliveMonsters()
|
||||||
|
local n = 0
|
||||||
|
if self.Monsters ~= nil then
|
||||||
|
for mi = 1, #self.Monsters do
|
||||||
|
local om = self.Monsters[mi]
|
||||||
|
if om ~= nil and om.alive == true then n = n + 1 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
local function randomAliveMonsterIndex()
|
||||||
|
local alive = {}
|
||||||
|
if self.Monsters ~= nil then
|
||||||
|
for mi = 1, #self.Monsters do
|
||||||
|
local om = self.Monsters[mi]
|
||||||
|
if om ~= nil and om.alive == true then
|
||||||
|
table.insert(alive, mi)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #alive <= 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
return alive[math.random(1, #alive)]
|
||||||
|
end
|
||||||
|
local function resolveAttackRound()
|
||||||
|
local roundKilled = false
|
||||||
|
if useAoe == true then
|
||||||
|
local killed = self:DealDamageToAllMonsters(total, true)
|
||||||
|
if killed == true then roundKilled = true end
|
||||||
|
elseif c.randomTargetEachHit == true then
|
||||||
|
for h = 1, hitN do
|
||||||
|
local targetIdx = randomAliveMonsterIndex()
|
||||||
|
if targetIdx ~= nil and targetIdx > 0 then
|
||||||
|
local prev = self.TargetIndex
|
||||||
|
self.TargetIndex = targetIdx
|
||||||
|
local killed = self:DealDamageToTarget(total / hitN, c.pierce == true)
|
||||||
|
self.TargetIndex = prev
|
||||||
|
if killed == true then roundKilled = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local killed = self:DealDamageToTarget(total, c.pierce == true)
|
||||||
|
if killed == true then roundKilled = true end
|
||||||
|
end
|
||||||
|
return roundKilled
|
||||||
|
end
|
||||||
|
local totalDamage = 0
|
||||||
|
local roundKilled = false
|
||||||
|
repeat
|
||||||
|
roundKilled = resolveAttackRound()
|
||||||
|
totalDamage = totalDamage + total
|
||||||
|
until c.repeatOnKill ~= true or roundKilled ~= true or countAliveMonsters() <= 0
|
||||||
|
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + totalDamage
|
||||||
end
|
end
|
||||||
if c.block ~= nil then
|
if c.block ~= nil then
|
||||||
self:AddCardBlock(c.block)
|
self:AddCardBlock(c.block)
|
||||||
@@ -278,40 +599,146 @@ end
|
|||||||
if c.heal ~= nil then
|
if c.heal ~= nil then
|
||||||
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
||||||
end
|
end
|
||||||
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
|
if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then
|
||||||
|
self.Energy = self.Energy + c.gainEnergy
|
||||||
|
end
|
||||||
|
if c.intangible ~= nil and c.intangible > 0 then
|
||||||
|
self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible
|
||||||
|
end
|
||||||
|
self.TurnCardsPlayedThisTurn = (self.TurnCardsPlayedThisTurn or 0) + 1
|
||||||
|
if c.blockPerDamageDealtThisTurn ~= nil and c.blockPerDamageDealtThisTurn > 0 then
|
||||||
|
self:AddCardBlock((self.DamageDealtThisTurn or 0) * c.blockPerDamageDealtThisTurn)
|
||||||
|
end
|
||||||
|
self:QueueNextTurnEffects(c)
|
||||||
|
if c.combatCostReductionOnPlay ~= nil and c.combatCostReductionOnPlay > 0 then
|
||||||
|
if self.CombatCardCostReduction == nil then
|
||||||
|
self.CombatCardCostReduction = {}
|
||||||
|
end
|
||||||
|
self.CombatCardCostReduction[cardId] = (self.CombatCardCostReduction[cardId] or 0) + c.combatCostReductionOnPlay
|
||||||
|
end
|
||||||
|
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil or c.xWeakPerEnergy ~= nil or c.affectsAllEnemies == true or c.removeEnemyBlock == true or c.removeEnemyArtifact == true or (c.enemyStrengthLossThisTurn ~= nil and c.enemyStrengthLossThisTurn > 0) then
|
||||||
local tm = self.Monsters[self.TargetIndex]
|
local tm = self.Monsters[self.TargetIndex]
|
||||||
if tm == nil or tm.alive ~= true then
|
if tm == nil or tm.alive ~= true then
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
|
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if tm ~= nil and tm.alive == true then
|
local targets = {}
|
||||||
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
|
if c.affectsAllEnemies == true and self.Monsters ~= nil then
|
||||||
if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end
|
for mi = 1, #self.Monsters do
|
||||||
if c.vuln ~= nil then
|
local om = self.Monsters[mi]
|
||||||
tm.vuln = tm.vuln + c.vuln
|
if om ~= nil and om.alive == true then
|
||||||
if self:HasRelic("championBelt") then
|
table.insert(targets, om)
|
||||||
tm.weak = tm.weak + 1
|
end
|
||||||
|
end
|
||||||
|
elseif tm ~= nil and tm.alive == true then
|
||||||
|
table.insert(targets, tm)
|
||||||
|
end
|
||||||
|
if c.enemyStrengthLossThisTurn ~= nil and c.enemyStrengthLossThisTurn > 0 then
|
||||||
|
self.EnemyStrengthLossThisTurn = (self.EnemyStrengthLossThisTurn or 0) + c.enemyStrengthLossThisTurn
|
||||||
|
end
|
||||||
|
for ti = 1, #targets do
|
||||||
|
local target = targets[ti]
|
||||||
|
if target ~= nil and target.alive == true then
|
||||||
|
if c.removeEnemyBlock == true then
|
||||||
|
target.block = 0
|
||||||
|
end
|
||||||
|
if c.removeEnemyArtifact == true then
|
||||||
|
target.artifact = 0
|
||||||
|
end
|
||||||
|
if weakAmount ~= nil and weakAmount > 0 then
|
||||||
|
if target.artifact ~= nil and target.artifact > 0 then
|
||||||
|
target.artifact = target.artifact - 1
|
||||||
|
else
|
||||||
|
target.weak = target.weak + weakAmount
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if poisonAmount ~= nil and poisonAmount > 0 then
|
||||||
|
if c.poisonIfTargetPoisoned ~= true or (target.poison ~= nil and target.poison > 0) then
|
||||||
|
local poisonHits = c.poisonHits or 1
|
||||||
|
for pi = 1, poisonHits do
|
||||||
|
local target2 = target
|
||||||
|
if c.poisonRandomTargets == true and self.Monsters ~= nil then
|
||||||
|
local alive = {}
|
||||||
|
for mi = 1, #self.Monsters do
|
||||||
|
local om = self.Monsters[mi]
|
||||||
|
if om ~= nil and om.alive == true then
|
||||||
|
table.insert(alive, om)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #alive > 0 then
|
||||||
|
target2 = alive[math.random(#alive)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if target2 ~= nil and target2.alive == true then
|
||||||
|
self:ApplyPoisonToMonster(target2, poisonAmount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if vulnAmount ~= nil and vulnAmount > 0 then
|
||||||
|
if target.artifact ~= nil and target.artifact > 0 then
|
||||||
|
target.artifact = target.artifact - 1
|
||||||
|
else
|
||||||
|
target.vuln = target.vuln + vulnAmount
|
||||||
|
if self:HasRelic("championBelt") then
|
||||||
|
target.weak = target.weak + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
local drawnCards = {}
|
||||||
if c.draw ~= nil then
|
if c.draw ~= nil then
|
||||||
self:DrawCards(c.draw, true)
|
drawnCards = self:DrawCards(c.draw, true) or {}
|
||||||
|
end
|
||||||
|
if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then
|
||||||
|
local currentHand = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
currentHand = #self.Hand
|
||||||
|
if slot ~= nil and slot > 0 and self.Hand[slot] == cardId then
|
||||||
|
currentHand = currentHand - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local need = c.drawUntilHandSize - currentHand
|
||||||
|
if need > 0 then
|
||||||
|
local moreDrawnCards = self:DrawCards(need, true) or {}
|
||||||
|
for i = 1, #moreDrawnCards do
|
||||||
|
table.insert(drawnCards, moreDrawnCards[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if c.drawSkillBlock ~= nil and c.drawSkillBlock > 0 then
|
||||||
|
for i = 1, #drawnCards do
|
||||||
|
local drawnCard = self.Cards[drawnCards[i]]
|
||||||
|
if drawnCard ~= nil and drawnCard.kind == "Skill" then
|
||||||
|
self:AddCardBlock(c.drawSkillBlock)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
||||||
self:AddCardsToHand("Shiv", c.addShiv)
|
self:AddCardsToHand("Shiv", c.addShiv)
|
||||||
end`, [
|
end`, [
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' },
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'energySpent' },
|
||||||
]),
|
]),
|
||||||
method('TriggerSly', `local c = self.Cards[cardId]
|
method('TriggerSly', `local c = self.Cards[cardId]
|
||||||
if c == nil or c.sly ~= true then
|
if c == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if c.sly ~= true then
|
||||||
|
local onPlay = self.SkillSlyOnPlayCards ~= nil and self.SkillSlyOnPlayCards[cardId] == true
|
||||||
|
local tempSly = self.TurnSkillSlyCards ~= nil and self.TurnSkillSlyCards[cardId] == true
|
||||||
|
if onPlay ~= true and tempSly ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
self:Toast("교활 발동: " .. c.name)
|
self:Toast("교활 발동: " .. c.name)
|
||||||
self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
|
self:ResolveCardEffects(cardId, 0, c, true, 0)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
|
||||||
method('DiscardHandCard', `if self.Hand == nil then
|
method('DiscardHandCard', `if self.Hand == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -319,22 +746,40 @@ local cardId = self.Hand[slot]
|
|||||||
if cardId == nil then
|
if cardId == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local startX = self:GetHandSlotX(slot)
|
||||||
table.remove(self.Hand, slot)
|
table.remove(self.Hand, slot)
|
||||||
table.insert(self.DiscardPile, cardId)
|
table.insert(self.DiscardPile, cardId)
|
||||||
|
self.TurnDiscardedCards = (self.TurnDiscardedCards or 0) + 1
|
||||||
if triggerSly == true then
|
if triggerSly == true then
|
||||||
self:TriggerSly(cardId)
|
self:TriggerSly(cardId)
|
||||||
|
end
|
||||||
|
if animate == true then
|
||||||
|
self:AnimateDiscardCards({ cardId }, { startX }, { slot })
|
||||||
end`, [
|
end`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'triggerSly' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'triggerSly' },
|
||||||
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
|
||||||
]),
|
]),
|
||||||
method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'),
|
method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'),
|
||||||
method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/DiscardPrompt")
|
method('IsRetainSelecting', `return self.RetainSelectActive == true`, [], 0, 'boolean'),
|
||||||
|
method('IsReserveSelecting', `return self.ReserveSelectActive == true`, [], 0, 'boolean'),
|
||||||
|
method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/DiscardPrompt")
|
||||||
if e == nil then
|
if e == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self:IsDiscardSelecting() == true then
|
if self:IsDiscardSelecting() == true then
|
||||||
local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining
|
local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. self:FormatNumber(picked + 1) .. "/" .. self:FormatNumber(self.DiscardSelectTotal))
|
self:SetText("/ui/RunUIGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. self:FormatNumber(picked + 1) .. "/" .. self:FormatNumber(self.DiscardSelectTotal))
|
||||||
|
e.Enable = true
|
||||||
|
elseif self:IsRetainSelecting() == true then
|
||||||
|
self:SetText("/ui/RunUIGroup/CombatHud/DiscardPrompt", "보존할 카드 선택 (턴 종료: 건너뛰기)")
|
||||||
|
e.Enable = true
|
||||||
|
elseif self:IsReserveSelecting() == true then
|
||||||
|
local msg = self.NextTurnSelectPrompt or ""
|
||||||
|
if msg == "" then
|
||||||
|
msg = "다음 턴에 예약할 카드를 선택하세요"
|
||||||
|
end
|
||||||
|
self:SetText("/ui/RunUIGroup/CombatHud/DiscardPrompt", msg)
|
||||||
e.Enable = true
|
e.Enable = true
|
||||||
else
|
else
|
||||||
e.Enable = false
|
e.Enable = false
|
||||||
@@ -342,10 +787,11 @@ end`),
|
|||||||
method('BeginDiscardSelection', `if c == nil or self.Hand == nil then
|
method('BeginDiscardSelection', `if c == nil or self.Hand == nil then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local n = 0
|
|
||||||
if c.discardAll == true then
|
if c.discardAll == true then
|
||||||
n = #self.Hand
|
return self:AutoDiscardHand(c)
|
||||||
elseif c.discard ~= nil then
|
end
|
||||||
|
local n = 0
|
||||||
|
if c.discard ~= nil then
|
||||||
n = math.min(c.discard, #self.Hand)
|
n = math.min(c.discard, #self.Hand)
|
||||||
end
|
end
|
||||||
if n <= 0 then
|
if n <= 0 then
|
||||||
@@ -355,28 +801,137 @@ self.DiscardSelectRemaining = n
|
|||||||
self.DiscardSelectTotal = n
|
self.DiscardSelectTotal = n
|
||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
|
self.DiscardPostDraw = 0
|
||||||
|
self.DiscardDrawPerPick = 0
|
||||||
if c.addShiv ~= nil then
|
if c.addShiv ~= nil then
|
||||||
self.DiscardPostShiv = c.addShiv
|
self.DiscardPostShiv = c.addShiv
|
||||||
end
|
end
|
||||||
if c.addShivPerDiscard == true then
|
if c.addShivPerDiscard == true then
|
||||||
self.DiscardShivPerPick = 1
|
self.DiscardShivPerPick = 1
|
||||||
end
|
end
|
||||||
|
if c.drawPerDiscarded ~= nil and c.drawPerDiscarded > 0 then
|
||||||
|
self.DiscardDrawPerPick = c.drawPerDiscarded
|
||||||
|
end
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
self:Toast("버릴 카드를 선택하세요")
|
self:Toast("버릴 카드를 선택하세요")
|
||||||
|
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||||
|
method('BeginReserveSelection', `if c == nil or c.nextTurnSelectHandCard ~= true or c.nextTurnCopies == nil or c.nextTurnCopies <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.Hand == nil or #self.Hand <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.ReserveSelectActive = true
|
||||||
|
self.NextTurnSelectCopies = c.nextTurnCopies
|
||||||
|
self.NextTurnSelectPrompt = c.nextTurnSelectPrompt or ""
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
|
self:Toast("예약할 카드를 선택하세요")
|
||||||
|
self:RenderHand(false)
|
||||||
|
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||||
|
method('SelectReserveSlot', `if self:IsReserveSelecting() ~= true then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.Hand == nil or self.Hand[slot] == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local cardId = self.Hand[slot]
|
||||||
|
local amount = self.NextTurnSelectCopies or 0
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
|
if amount > 0 and cardId ~= nil then
|
||||||
|
self:QueueNextTurnAddCard(cardId, amount)
|
||||||
|
local label = cardId
|
||||||
|
if self.Cards[cardId] ~= nil and self.Cards[cardId].name ~= nil then
|
||||||
|
label = self.Cards[cardId].name
|
||||||
|
end
|
||||||
|
self:Toast("다음 턴 예약: " .. label .. " " .. self:FormatNumber(amount) .. "장")
|
||||||
|
end
|
||||||
|
self:RenderPiles()
|
||||||
|
self:RenderCombat()
|
||||||
|
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
|
||||||
|
method('SelectRetainSlot', `if self:IsRetainSelecting() ~= true then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.Hand == nil or self.Hand[slot] == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
self:FinishPlayerTurn(slot)
|
||||||
|
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
|
||||||
|
method('AutoDiscardHand', `if c == nil or self.Hand == nil or #self.Hand <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local cardIds = {}
|
||||||
|
local startXs = {}
|
||||||
|
local slots = {}
|
||||||
|
local n = #self.Hand
|
||||||
|
for i = 1, n do
|
||||||
|
local cardId = self.Hand[i]
|
||||||
|
table.insert(cardIds, cardId)
|
||||||
|
table.insert(startXs, self:GetHandSlotX(i))
|
||||||
|
table.insert(slots, i)
|
||||||
|
table.insert(self.DiscardPile, cardId)
|
||||||
|
self.TurnDiscardedCards = (self.TurnDiscardedCards or 0) + 1
|
||||||
|
end
|
||||||
|
self.Hand = {}
|
||||||
|
local shivCount = 0
|
||||||
|
if c.addShiv ~= nil then shivCount = shivCount + c.addShiv end
|
||||||
|
if c.addShivPerDiscard == true then shivCount = shivCount + n end
|
||||||
|
self.DiscardSelectRemaining = 0
|
||||||
|
self.DiscardSelectTotal = 0
|
||||||
|
self.DiscardPostShiv = 0
|
||||||
|
self.DiscardShivPerPick = 0
|
||||||
|
self.DiscardPostDraw = 0
|
||||||
|
self.DiscardDrawPerPick = 0
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
|
self:AnimateDiscardCards(cardIds, startXs, slots)
|
||||||
|
for i = 1, #cardIds do
|
||||||
|
self:TriggerSly(cardIds[i])
|
||||||
|
end
|
||||||
|
self:RenderPiles()
|
||||||
|
self:RenderCombat()
|
||||||
|
_TimerService:SetTimerOnce(function()
|
||||||
|
if shivCount > 0 then
|
||||||
|
self:AddCardsToHand("Shiv", shivCount)
|
||||||
|
else
|
||||||
|
self:RenderHand(false)
|
||||||
|
self:RenderPiles()
|
||||||
|
end
|
||||||
|
if c.drawPerDiscarded ~= nil and c.drawPerDiscarded > 0 then
|
||||||
|
self:DrawCards(n * c.drawPerDiscarded, true)
|
||||||
|
end
|
||||||
|
self:RenderCombat()
|
||||||
|
self:CheckCombatEnd()
|
||||||
|
end, 0.22)
|
||||||
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||||
method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0
|
method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0
|
||||||
self.DiscardSelectTotal = 0
|
self.DiscardSelectTotal = 0
|
||||||
local shivCount = self.DiscardPostShiv or 0
|
local shivCount = self.DiscardPostShiv or 0
|
||||||
|
local drawCount = self.DiscardPostDraw or 0
|
||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
|
self.DiscardPostDraw = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
|
self.DiscardDrawPerPick = 0
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
if shivCount > 0 then
|
local finish = function()
|
||||||
self:AddCardsToHand("Shiv", shivCount)
|
if shivCount > 0 then
|
||||||
|
self:AddCardsToHand("Shiv", shivCount)
|
||||||
|
else
|
||||||
|
self:RenderHand(false)
|
||||||
|
self:RenderPiles()
|
||||||
|
end
|
||||||
|
if drawCount > 0 then
|
||||||
|
self:DrawCards(drawCount, true)
|
||||||
|
end
|
||||||
|
self:RenderCombat()
|
||||||
|
self:CheckCombatEnd()
|
||||||
end
|
end
|
||||||
self:RenderHand(false)
|
if delayRender == true then
|
||||||
self:RenderPiles()
|
_TimerService:SetTimerOnce(finish, 0.22)
|
||||||
self:RenderCombat()
|
else
|
||||||
self:CheckCombatEnd()`),
|
finish()
|
||||||
|
end`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delayRender' }]),
|
||||||
method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then
|
method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -384,18 +939,21 @@ if self.Hand == nil or self.Hand[slot] == nil then
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local discarded = self.Hand[slot]
|
local discarded = self.Hand[slot]
|
||||||
self:DiscardHandCard(slot, true)
|
self:DiscardHandCard(slot, true, true)
|
||||||
if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then
|
if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then
|
||||||
self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick
|
self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick
|
||||||
end
|
end
|
||||||
|
if discarded ~= nil and self.DiscardDrawPerPick ~= nil and self.DiscardDrawPerPick > 0 then
|
||||||
|
self.DiscardPostDraw = (self.DiscardPostDraw or 0) + self.DiscardDrawPerPick
|
||||||
|
end
|
||||||
self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1
|
self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1
|
||||||
if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then
|
if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then
|
||||||
self:FinishDiscardSelection()
|
self:FinishDiscardSelection(true)
|
||||||
else
|
else
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
self:RenderHand(false)
|
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
|
_TimerService:SetTimerOnce(function() self:RenderHand(false) end, 0.22)
|
||||||
end
|
end
|
||||||
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
|
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ if self:AddPotion(pid) == true then
|
|||||||
self:Toast("물약 획득: " .. p.name)
|
self:Toast("물약 획득: " .. p.name)
|
||||||
end`),
|
end`),
|
||||||
method('RenderPotions', `for i = 1, 5 do
|
method('RenderPotions', `for i = 1, 5 do
|
||||||
local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
|
local base = "/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
|
||||||
local e = _EntityService:GetEntityByPath(base)
|
local e = _EntityService:GetEntityByPath(base)
|
||||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||||
local pid = nil
|
local pid = nil
|
||||||
@@ -121,11 +121,11 @@ self.PotionMenuSlot = slot
|
|||||||
local pid = self.RunPotions[slot]
|
local pid = self.RunPotions[slot]
|
||||||
local p = self.Potions[pid]
|
local p = self.Potions[pid]
|
||||||
if p ~= nil then
|
if p ~= nil then
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc)
|
self:SetText("/ui/RunUIGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc)
|
||||||
end
|
end
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||||
method('ClosePotionMenu', `self.PotionMenuSlot = 0
|
method('ClosePotionMenu', `self.PotionMenuSlot = 0
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`),
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", false)`),
|
||||||
method('UsePotion', `if self.PotionMenuSlot <= 0 then
|
method('UsePotion', `if self.PotionMenuSlot <= 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -133,8 +133,8 @@ if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then
|
|||||||
self:Toast("지금은 사용할 수 없습니다")
|
self:Toast("지금은 사용할 수 없습니다")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud")
|
local combat = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud")
|
||||||
local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand")
|
local hand = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand")
|
||||||
if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then
|
if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then
|
||||||
self:Toast("전투 중에만 사용할 수 있습니다")
|
self:Toast("전투 중에만 사용할 수 있습니다")
|
||||||
return
|
return
|
||||||
@@ -189,7 +189,7 @@ if self.RunRelics ~= nil then
|
|||||||
count = #self.RunRelics
|
count = #self.RunRelics
|
||||||
end
|
end
|
||||||
for i = 1, 10 do
|
for i = 1, 10 do
|
||||||
local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
|
local base = "/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
|
||||||
local e = _EntityService:GetEntityByPath(base)
|
local e = _EntityService:GetEntityByPath(base)
|
||||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||||
local rid = nil
|
local rid = nil
|
||||||
@@ -209,5 +209,5 @@ local of = ""
|
|||||||
if count > 10 then
|
if count > 10 then
|
||||||
of = "+" .. tostring(count - 9)
|
of = "+" .. tostring(count - 9)
|
||||||
end
|
end
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`),
|
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/RelicOverflow", of)`),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,12 +1,53 @@
|
|||||||
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 { method } 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 = [
|
export const jobMethods = [
|
||||||
method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
method('BaseClassLabel', `if classId == "warrior" then
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
return "전사"
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`),
|
elseif classId == "rogue" then
|
||||||
method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
|
return "Rogue"
|
||||||
|
elseif classId == "magician" then
|
||||||
|
return "마법사"
|
||||||
|
end
|
||||||
|
return "플레이어"`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'classId' }], 0, 'string'),
|
||||||
|
method('CurrentClassId', `if self.PlayerJob ~= nil and self.PlayerJob ~= "" then
|
||||||
|
return self.PlayerJob
|
||||||
|
end
|
||||||
|
return self.SelectedClass or ""`, [], 0, 'string'),
|
||||||
|
method('GetPlayableClasses', `local current = self:CurrentClassId()
|
||||||
|
if current == nil or current == "" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
if self.ClassLineages ~= nil and self.ClassLineages[current] ~= nil then
|
||||||
|
return self.ClassLineages[current]
|
||||||
|
end
|
||||||
|
return { current }`, [], 0, 'any'),
|
||||||
|
method('CanUseClassCard', `if cardClass == nil or cardClass == "" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if cardClass == "curse" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local playable = self:GetPlayableClasses()
|
||||||
|
for i = 1, #playable do
|
||||||
|
if playable[i] == cardClass then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardClass' }], 0, 'boolean'),
|
||||||
|
method('CanAdvanceJob', `local current = self:CurrentClassId()
|
||||||
|
if current == nil or current == "" or self.Jobs == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local opts = self.Jobs[current]
|
||||||
|
return opts ~= nil and #opts > 0`, [], 0, 'boolean'),
|
||||||
|
method('ShowJobChoice', `if self:CanAdvanceJob() ~= true then
|
||||||
|
self:ContinueAfterBoss()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
|
||||||
|
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", false)
|
||||||
|
self:SetEntityEnabled("/ui/SelectUIGroup/JobChoiceHud", true)`),
|
||||||
|
method('PickJobReward', `self:SetEntityEnabled("/ui/SelectUIGroup/JobChoiceHud", false)
|
||||||
if kind == "relic" then
|
if kind == "relic" then
|
||||||
local bid = self:PickNewRelic()
|
local bid = self:PickNewRelic()
|
||||||
if bid ~= "" then
|
if bid ~= "" then
|
||||||
@@ -20,13 +61,17 @@ if kind == "relic" then
|
|||||||
else
|
else
|
||||||
self:ShowJobSelect()
|
self:ShowJobSelect()
|
||||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||||||
method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass]
|
method('ShowJobSelect', `local current = self:CurrentClassId()
|
||||||
|
local opts = nil
|
||||||
|
if self.Jobs ~= nil then
|
||||||
|
opts = self.Jobs[current]
|
||||||
|
end
|
||||||
if opts == nil then
|
if opts == nil then
|
||||||
opts = self.Jobs["warrior"]
|
opts = {}
|
||||||
end
|
end
|
||||||
self.JobOpts = opts
|
self.JobOpts = opts
|
||||||
for i = 1, 3 do
|
for i = 1, 3 do
|
||||||
local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)
|
local base = "/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i)
|
||||||
local o = opts[i]
|
local o = opts[i]
|
||||||
if o ~= nil then
|
if o ~= nil then
|
||||||
self:SetEntityEnabled(base, true)
|
self:SetEntityEnabled(base, true)
|
||||||
@@ -40,40 +85,34 @@ for i = 1, 3 do
|
|||||||
self:SetEntityEnabled(base, false)
|
self:SetEntityEnabled(base, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`),
|
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", true)`),
|
||||||
method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then
|
method('JobLabel', `if self.PlayerJob ~= "" and self.JobMeta ~= nil and self.JobMeta[self.PlayerJob] ~= nil then
|
||||||
for cls, list in pairs(self.Jobs) do
|
return self.JobMeta[self.PlayerJob].name
|
||||||
for i = 1, #list do
|
|
||||||
if list[i].id == self.PlayerJob then
|
|
||||||
return list[i].name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
if self.SelectedClass == "warrior" then
|
return self:BaseClassLabel(self.SelectedClass)`, [], 0, 'string'),
|
||||||
return "전사"
|
method('SetJob', `local current = self:CurrentClassId()
|
||||||
elseif self.SelectedClass == "bandit" then
|
|
||||||
return "도적"
|
|
||||||
elseif self.SelectedClass == "magician" then
|
|
||||||
return "마법사"
|
|
||||||
end
|
|
||||||
return "플레이어"`, [], 0, 'string'),
|
|
||||||
method('SetJob', `self.PlayerJob = jobId
|
|
||||||
local starter = ""
|
local starter = ""
|
||||||
local opts = self.Jobs[self.SelectedClass] or {}
|
local tier = 2
|
||||||
|
local opts = {}
|
||||||
|
if self.Jobs ~= nil and self.Jobs[current] ~= nil then
|
||||||
|
opts = self.Jobs[current]
|
||||||
|
end
|
||||||
for i = 1, #opts do
|
for i = 1, #opts do
|
||||||
if opts[i].id == jobId then
|
if opts[i].id == jobId then
|
||||||
starter = opts[i].starter
|
starter = opts[i].starter or ""
|
||||||
|
tier = opts[i].tier or 2
|
||||||
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
self.PlayerJob = jobId
|
||||||
if starter ~= "" then
|
if starter ~= "" then
|
||||||
table.insert(self.RunDeck, starter)
|
table.insert(self.RunDeck, starter)
|
||||||
local sc = self.Cards[starter]
|
local sc = self.Cards[starter]
|
||||||
if sc ~= nil then
|
if sc ~= nil then
|
||||||
self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name)
|
self:Toast(tostring(tier) .. "차 전직: " .. self:JobLabel() .. "! 신규 카드 - " .. sc.name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
|
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", false)
|
||||||
self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]),
|
self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]),
|
||||||
];
|
];
|
||||||
|
|||||||
21
tools/deck/cb/layout.mjs
Normal file
21
tools/deck/cb/layout.mjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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 layoutMethods = [
|
||||||
|
method('PositionMonsterSlot', `local monster = self.Monsters[slot]
|
||||||
|
if monster == nil or monster.entity == nil or not isvalid(monster.entity) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local transform = monster.entity.TransformComponent
|
||||||
|
if transform == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local worldPos = transform.WorldPosition
|
||||||
|
local screen = _UILogic:WorldToScreenPosition(Vector2(worldPos.x, worldPos.y + ${HEAD_OFFSET_Y}))
|
||||||
|
local uipos = _UILogic:ScreenToUIPosition(screen)
|
||||||
|
local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(slot))
|
||||||
|
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
|
||||||
|
slotEntity.UITransformComponent.anchoredPosition = uipos
|
||||||
|
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||||
|
];
|
||||||
@@ -115,7 +115,7 @@ for i = 1, #list do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
||||||
method('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id
|
method('RenderMapNode', `local base = "/ui/RunUIGroup/MapHud/Node_" .. id
|
||||||
local e = _EntityService:GetEntityByPath(base)
|
local e = _EntityService:GetEntityByPath(base)
|
||||||
if e == nil then
|
if e == nil then
|
||||||
return
|
return
|
||||||
@@ -151,7 +151,7 @@ if e.SpriteGUIRendererComponent ~= nil then
|
|||||||
e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85)
|
e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if e.ButtonComponent ~= nil then
|
if (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then
|
||||||
e.ButtonComponent.Enable = reachable
|
e.ButtonComponent.Enable = reachable
|
||||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||||
method('RenderMapDots', `local node = self.MapNodes[fromId]
|
method('RenderMapDots', `local node = self.MapNodes[fromId]
|
||||||
@@ -162,7 +162,7 @@ if node ~= nil then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
for k = 1, 3 do
|
for k = 1, 3 do
|
||||||
local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
|
local d = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
|
||||||
if d ~= nil then
|
if d ~= nil then
|
||||||
d.Enable = has
|
d.Enable = has
|
||||||
if has == true and d.SpriteGUIRendererComponent ~= nil then
|
if has == true and d.SpriteGUIRendererComponent ~= nil then
|
||||||
@@ -210,7 +210,7 @@ if self.VisitedNodes == nil then
|
|||||||
self.VisitedNodes = {}
|
self.VisitedNodes = {}
|
||||||
end
|
end
|
||||||
table.insert(self.VisitedNodes, id)
|
table.insert(self.VisitedNodes, id)
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
|
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = false
|
hud.Enable = false
|
||||||
end
|
end
|
||||||
|
|||||||
34
tools/deck/cb/navigation.mjs
Normal file
34
tools/deck/cb/navigation.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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 navigationMethods = [
|
||||||
|
method('GoLobbyMap', `self.LobbyTpTries = 0
|
||||||
|
local eventId = 0
|
||||||
|
local function tryTeleport()
|
||||||
|
self.LobbyTpTries = self.LobbyTpTries + 1
|
||||||
|
local localPlayer = _UserService.LocalPlayer
|
||||||
|
if localPlayer ~= nil then
|
||||||
|
if localPlayer.CurrentMapName ~= "${LOBBY_MAP}" then
|
||||||
|
_TeleportService:TeleportToMapPosition(localPlayer, ${LOBBY_SPAWN}, "${LOBBY_MAP}")
|
||||||
|
end
|
||||||
|
_TimerService:ClearTimer(eventId)
|
||||||
|
elseif self.LobbyTpTries > 50 then
|
||||||
|
_TimerService:ClearTimer(eventId)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
eventId = _TimerService:SetTimerRepeat(tryTeleport, 0.1)`),
|
||||||
|
method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((mapName) => `"${mapName}"`).join(', ')} }
|
||||||
|
local target = maps[self.Floor]
|
||||||
|
if target == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local localPlayer = _UserService.LocalPlayer
|
||||||
|
if localPlayer == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if localPlayer.CurrentMapName == target then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
_TeleportService:TeleportToMapPosition(localPlayer, Vector3(-6, 0.03, 0), target)`),
|
||||||
|
];
|
||||||
18
tools/deck/cb/npc.mjs
Normal file
18
tools/deck/cb/npc.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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 npcMethods = [
|
||||||
|
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' }]),
|
||||||
|
];
|
||||||
@@ -15,7 +15,7 @@ return table.concat(parts, " ")`, [
|
|||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
|
||||||
], 0, 'string'),
|
], 0, 'string'),
|
||||||
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
|
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
|
||||||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)
|
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i)
|
||||||
local m = self.Monsters[i]
|
local m = self.Monsters[i]
|
||||||
if m ~= nil and m.alive == true then
|
if m ~= nil and m.alive == true then
|
||||||
self:SetEntityEnabled(base, true)
|
self:SetEntityEnabled(base, true)
|
||||||
@@ -41,7 +41,6 @@ return table.concat(parts, " ")`, [
|
|||||||
local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
|
local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
|
||||||
local shownTarget = self.TargetIndex
|
local shownTarget = self.TargetIndex
|
||||||
if dragActive == true then shownTarget = self.DragTargetIndex end
|
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", i == shownTarget and dragActive)
|
||||||
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
|
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
|
||||||
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
|
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
|
||||||
@@ -64,11 +63,15 @@ return table.concat(parts, " ")`, [
|
|||||||
self:SetEntityEnabled(base, false)
|
self:SetEntityEnabled(base, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
|
self:SetText("/ui/RunUIGroup/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:SetHpBar("/ui/RunUIGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
|
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
|
||||||
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
|
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
|
||||||
|
if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then
|
||||||
|
if pb ~= "" then pb = pb .. " " end
|
||||||
|
pb = pb .. "불가침" .. tostring(self.PlayerIntangible)
|
||||||
|
end
|
||||||
if self.PlayerDex ~= nil and self.PlayerDex > 0 then
|
if self.PlayerDex ~= nil and self.PlayerDex > 0 then
|
||||||
if pb ~= "" then pb = pb .. " " end
|
if pb ~= "" then pb = pb .. " " end
|
||||||
pb = pb .. "민첩+" .. tostring(self.PlayerDex)
|
pb = pb .. "민첩+" .. tostring(self.PlayerDex)
|
||||||
@@ -86,10 +89,10 @@ if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
|
|||||||
if pb ~= "" then pb = pb .. " · " end
|
if pb ~= "" then pb = pb .. " · " end
|
||||||
pb = pb .. table.concat(names, " ")
|
pb = pb .. table.concat(names, " ")
|
||||||
end
|
end
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb)
|
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb)
|
||||||
self:RenderRun()`),
|
self:RenderRun()`),
|
||||||
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
|
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
|
||||||
local base = "/ui/DefaultGroup/CombatHud/DmgPop" .. slotKey
|
local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey
|
||||||
local pop = _EntityService:GetEntityByPath(base)
|
local pop = _EntityService:GetEntityByPath(base)
|
||||||
if pop == nil then
|
if pop == nil then
|
||||||
return
|
return
|
||||||
@@ -134,7 +137,7 @@ if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComp
|
|||||||
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45}))
|
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45}))
|
||||||
popPos = _UILogic:ScreenToUIPosition(screen)
|
popPos = _UILogic:ScreenToUIPosition(screen)
|
||||||
else
|
else
|
||||||
local slotEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. slotKey)
|
local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey)
|
||||||
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
|
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
|
||||||
local sp = slotEntity.UITransformComponent.anchoredPosition
|
local sp = slotEntity.UITransformComponent.anchoredPosition
|
||||||
popPos = Vector2(sp.x, sp.y + 76)
|
popPos = Vector2(sp.x, sp.y + 76)
|
||||||
@@ -169,7 +172,7 @@ end, 0.48)`, [
|
|||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
]),
|
]),
|
||||||
method('ShowPlayerDmgPop', `local base = "/ui/DefaultGroup/CombatHud/PlayerPanel/DmgPop"
|
method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop"
|
||||||
if amount > 0 then
|
if amount > 0 then
|
||||||
self:SetText(base, "-" .. string.format("%d", amount))
|
self:SetText(base, "-" .. string.format("%d", amount))
|
||||||
else
|
else
|
||||||
@@ -280,21 +283,6 @@ e.UITransformComponent.RectSize = Vector2(w, 14)`, [
|
|||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
|
{ 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
|
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
|
||||||
self.TargetIndex = slot
|
self.TargetIndex = slot
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
@@ -303,6 +291,6 @@ end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], N
|
|||||||
if self.AscensionLevel > 0 then
|
if self.AscensionLevel > 0 then
|
||||||
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
|
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
|
||||||
end
|
end
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Floor", floorText)
|
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Floor", floorText)
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
|
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER,
|
|||||||
export const rewardMethods = [
|
export const rewardMethods = [
|
||||||
method('CardPool', `local pool = {}
|
method('CardPool', `local pool = {}
|
||||||
for id, c in pairs(self.Cards) do
|
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
|
if c.token ~= true and self:CanUseClassCard(c.class) == true then
|
||||||
table.insert(pool, id)
|
table.insert(pool, id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(pool)
|
table.sort(pool)
|
||||||
return pool`, [], 0, 'any'),
|
return pool`, [], 0, 'any'),
|
||||||
method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
method('OfferReward', `self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", false)
|
||||||
local pool = self:CardPool()
|
local pool = self:CardPool()
|
||||||
local byRarity = {}
|
local byRarity = {}
|
||||||
for _, id in ipairs(pool) do
|
for _, id in ipairs(pool) do
|
||||||
@@ -30,15 +30,15 @@ for i = 1, 3 do
|
|||||||
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
|
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
|
||||||
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
||||||
end
|
end
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = true
|
hud.Enable = true
|
||||||
end`),
|
end`),
|
||||||
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
|
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/RunUIGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if slot ~= 0 and self.RewardChoices ~= nil then
|
if slot ~= 0 and self.RewardChoices ~= nil then
|
||||||
@@ -47,7 +47,12 @@ if slot ~= 0 and self.RewardChoices ~= nil then
|
|||||||
table.insert(self.RunDeck, id)
|
table.insert(self.RunDeck, id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
if self.BonusRewardScreens ~= nil and self.BonusRewardScreens > 0 and slot ~= 0 then
|
||||||
|
self.BonusRewardScreens = self.BonusRewardScreens - 1
|
||||||
|
self:OfferReward()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = false
|
hud.Enable = false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
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 { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, ACT_DIFFICULTY_MULTIPLIERS, 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 { CARDS, ENEMIES, CLASSES, JOBS, JOB_META, CLASS_GROUPS, CLASS_LINEAGES, 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, luaClassGroupsTable, luaClassLineagesTable, luaJobMetaTable, 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';
|
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 = [
|
export const runMethods = [
|
||||||
method('StartRun', `if self.SelectedClass == "magician" then
|
method('StartRun', `if self.SelectedClass == "magician" then
|
||||||
self.PlayerMaxHp = ${CLASSES.magician.maxHp}
|
self.PlayerMaxHp = ${CLASSES.magician.maxHp}
|
||||||
self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} }
|
self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} }
|
||||||
elseif self.SelectedClass == "bandit" then
|
elseif self.SelectedClass == "rogue" then
|
||||||
self.PlayerMaxHp = ${CLASSES.bandit.maxHp}
|
self.PlayerMaxHp = ${CLASSES.rogue.maxHp}
|
||||||
self.RunDeck = { ${CARDS.starterDecks.bandit.map(luaStr).join(', ')} }
|
self.RunDeck = { ${CARDS.starterDecks.rogue.map(luaStr).join(', ')} }
|
||||||
else
|
else
|
||||||
self.PlayerMaxHp = ${CLASSES.warrior.maxHp}
|
self.PlayerMaxHp = ${CLASSES.warrior.maxHp}
|
||||||
self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} }
|
self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} }
|
||||||
@@ -30,8 +30,12 @@ self.CurrentNodeId = ""
|
|||||||
self.CurrentEnemyId = ""
|
self.CurrentEnemyId = ""
|
||||||
self.PlayerJob = ""
|
self.PlayerJob = ""
|
||||||
${luaJobsTable(JOBS)}
|
${luaJobsTable(JOBS)}
|
||||||
|
${luaJobMetaTable(JOB_META)}
|
||||||
|
${luaClassGroupsTable(CLASS_GROUPS)}
|
||||||
|
${luaClassLineagesTable(CLASS_LINEAGES)}
|
||||||
${luaFramesTable()}
|
${luaFramesTable()}
|
||||||
${luaNodeIconsTable()}
|
${luaNodeIconsTable()}
|
||||||
|
${luaCharsTable()}
|
||||||
self:GenerateMap()
|
self:GenerateMap()
|
||||||
self:BindButtons()
|
self:BindButtons()
|
||||||
self:AddRelic("${RELICS.startingRelic}")
|
self:AddRelic("${RELICS.startingRelic}")
|
||||||
@@ -58,21 +62,45 @@ _TimerService:SetTimerOnce(function()
|
|||||||
end, 0.2)`),
|
end, 0.2)`),
|
||||||
method('StartCombat', `self:ShowState("combat")
|
method('StartCombat', `self:ShowState("combat")
|
||||||
self:KickCombatCamera()
|
self:KickCombatCamera()
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/Result", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/TooltipBox", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/DiscardPrompt", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/DiscardPrompt", false)
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||||||
self.MaxEnergy = 3
|
self.MaxEnergy = 3
|
||||||
self.Turn = 0
|
self.Turn = 0
|
||||||
self.PlayerBlock = 0
|
self.PlayerBlock = 0
|
||||||
|
self.BlockGainMultiplier = 1
|
||||||
|
self.CardsDrawnThisCombat = 0
|
||||||
|
self.HandCostZeroThisTurn = false
|
||||||
|
self.DrawDisabledThisTurn = false
|
||||||
|
self.NextSkillCostZero = false
|
||||||
|
self.NextSkillRepeatCount = 0
|
||||||
|
self.SkillCostReductionThisTurn = 0
|
||||||
|
self.CombatCardCostReduction = {}
|
||||||
|
self.SkillSlyOnPlayCards = {}
|
||||||
|
self.TurnSkillSlyCards = {}
|
||||||
|
self.ShivFirstDamageBonusUsed = false
|
||||||
|
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||||
|
self.DrawDamageThisTurn = 0
|
||||||
|
self.DrawPoisonThisTurn = 0
|
||||||
|
self.ShivAoeThisCombat = false
|
||||||
|
self.PoisonApplicationsThisCombat = 0
|
||||||
|
self.EnemyStrengthLossThisTurn = 0
|
||||||
self.PlayerStr = 0
|
self.PlayerStr = 0
|
||||||
self.PlayerDex = 0
|
self.PlayerDex = 0
|
||||||
self.PlayerThorns = 0
|
self.PlayerThorns = 0
|
||||||
self.PlayerWeak = 0
|
self.PlayerWeak = 0
|
||||||
self.PlayerVuln = 0
|
self.PlayerVuln = 0
|
||||||
|
self.PlayerIntangible = 0
|
||||||
|
self.BonusRewardScreens = 0
|
||||||
|
self.ActiveKillReward = 0
|
||||||
self.PlayerPowers = {}
|
self.PlayerPowers = {}
|
||||||
self.FightAttackCount = 0
|
self.FightAttackCount = 0
|
||||||
|
self.TurnAttackCardsPlayed = 0
|
||||||
|
self.TurnDiscardedCards = 0
|
||||||
|
self.TurnCardsPlayedThisTurn = 0
|
||||||
|
self.DamageDealtThisTurn = 0
|
||||||
self.DmgPopSeq = 0
|
self.DmgPopSeq = 0
|
||||||
self.FirstHpLossDone = false
|
self.FirstHpLossDone = false
|
||||||
self.ClayBlockNext = 0
|
self.ClayBlockNext = 0
|
||||||
@@ -80,6 +108,16 @@ self.DiscardSelectRemaining = 0
|
|||||||
self.DiscardSelectTotal = 0
|
self.DiscardSelectTotal = 0
|
||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
|
self.RetainSelectActive = false
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnBlock = 0
|
||||||
|
self.NextTurnDraw = 0
|
||||||
|
self.NextTurnKeepBlock = false
|
||||||
|
self.NextTurnAttackMultiplier = 1
|
||||||
|
self.TurnAttackMultiplier = 1
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
self.CombatOver = false
|
self.CombatOver = false
|
||||||
self.DiscardPile = {}
|
self.DiscardPile = {}
|
||||||
self.ExhaustPile = {}
|
self.ExhaustPile = {}
|
||||||
@@ -90,11 +128,24 @@ for i = 1, #self.RunDeck do
|
|||||||
self.DrawPile[i] = self.RunDeck[i]
|
self.DrawPile[i] = self.RunDeck[i]
|
||||||
end
|
end
|
||||||
self:Shuffle(self.DrawPile)
|
self:Shuffle(self.DrawPile)
|
||||||
|
self:PrepareCombatDrawPile()
|
||||||
self:BuildMonsters()
|
self:BuildMonsters()
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:StartPlayerTurn()
|
self:StartPlayerTurn()
|
||||||
self:ApplyRelics("combatStart")
|
self:ApplyRelics("combatStart")
|
||||||
self:RenderCombat()`),
|
self:RenderCombat()
|
||||||
|
local slotTid = 0
|
||||||
|
slotTid = _TimerService:SetTimerRepeat(function()
|
||||||
|
if self.CombatOver == true or self.Monsters == nil or #self.Monsters == 0 then
|
||||||
|
_TimerService:ClearTimer(slotTid)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for i = 1, #self.Monsters do
|
||||||
|
if self.Monsters[i] ~= nil and self.Monsters[i].alive == true then
|
||||||
|
self:PositionMonsterSlot(i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end, 0.15)`),
|
||||||
method('RegisterMonster', `if self.Registered == nil then
|
method('RegisterMonster', `if self.Registered == nil then
|
||||||
self.Registered = {}
|
self.Registered = {}
|
||||||
end
|
end
|
||||||
@@ -160,7 +211,8 @@ end
|
|||||||
if #chosen == 0 then takeFrom(g, 1) end
|
if #chosen == 0 then takeFrom(g, 1) end
|
||||||
if #chosen == 0 then takeFrom("combat", 1) end
|
if #chosen == 0 then takeFrom("combat", 1) end
|
||||||
table.sort(chosen, function(a, b) return a.x < b.x end)
|
table.sort(chosen, function(a, b) return a.x < b.x end)
|
||||||
local mult = 1 + (self.Floor - 1) * 0.45
|
local actMultipliers = { ${ACT_DIFFICULTY_MULTIPLIERS.join(', ')} }
|
||||||
|
local mult = actMultipliers[self.Floor] or actMultipliers[#actMultipliers]
|
||||||
if g == "elite" or g == "boss" then
|
if g == "elite" or g == "boss" then
|
||||||
mult = mult + self:AscEliteBonus()
|
mult = mult + self:AscEliteBonus()
|
||||||
end
|
end
|
||||||
@@ -192,7 +244,7 @@ for i = 1, n do
|
|||||||
local startIdx = 1
|
local startIdx = 1
|
||||||
if #intents > 0 then startIdx = math.random(1, #intents) end
|
if #intents > 0 then startIdx = math.random(1, #intents) end
|
||||||
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
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,
|
hp = maxHp, maxHp = maxHp, block = 0, str = e.str or 0, weak = 0, vuln = 0, poison = 0, artifact = e.artifact or 0,
|
||||||
hitClip = hitClip, standClip = standClip, motionBusy = false,
|
hitClip = hitClip, standClip = standClip, motionBusy = false,
|
||||||
intents = intents, intentIdx = startIdx, alive = true, slot = i }
|
intents = intents, intentIdx = startIdx, alive = true, slot = i }
|
||||||
self:ReviveMonsterEntity(item.entity)
|
self:ReviveMonsterEntity(item.entity)
|
||||||
|
|||||||
@@ -3,21 +3,8 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
|
|||||||
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 { 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 = [
|
export const runEndMethods = [
|
||||||
method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} }
|
method('ShowResult', `self:SetText("/ui/RunUIGroup/CombatHud/Result", text)
|
||||||
local target = maps[self.Floor]
|
local entity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/Result")
|
||||||
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
|
if entity ~= nil then
|
||||||
entity.Enable = true
|
entity.Enable = true
|
||||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||||||
|
|||||||
@@ -2,41 +2,49 @@ import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, A
|
|||||||
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 { 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';
|
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 = [
|
export const screensMethods = [
|
||||||
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
|
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false)
|
self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false)
|
self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/RewardHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/MapHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/RestHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
|
self:SetEntityEnabled("/ui/SelectUIGroup/JobChoiceHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
|
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false)
|
self:SetEntityEnabled("/ui/DeckUIGroup/DeckInspectHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)
|
self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
|
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
|
self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
|
self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)`),
|
||||||
|
method('ActivateUIGroups', `local function enableGroup(name)
|
||||||
|
local group = _EntityService:GetEntityByPath("/ui/" .. name)
|
||||||
|
if group ~= nil then group:SetEnable(true) end
|
||||||
|
end
|
||||||
|
enableGroup("SelectUIGroup")
|
||||||
|
enableGroup("LobbyUIGroup")
|
||||||
|
enableGroup("RunUIGroup")
|
||||||
|
enableGroup("DeckUIGroup")`, [], 2),
|
||||||
method('ShowState', `self:HideGameHud()
|
method('ShowState', `self:HideGameHud()
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
|
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect")
|
self:SetEntityEnabled("/ui/SelectUIGroup/CharacterSelectHud", state == "charselect")
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", state == "lobby")
|
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", state == "lobby")
|
||||||
if state == "map" then
|
if state == "map" then
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/MapHud", true)
|
||||||
elseif state == "combat" then
|
elseif state == "combat" then
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud", true)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", true)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/CardHand", true)
|
||||||
elseif state == "shop" then
|
elseif state == "shop" then
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud", true)
|
||||||
elseif state == "rest" then
|
elseif state == "rest" then
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/RestHud", true)
|
||||||
elseif state == "treasure" then
|
elseif state == "treasure" then
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud", true)
|
||||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
|
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
|
||||||
method('ShowMainMenu', `self.SelectedClass = ""
|
method('ShowMainMenu', `self.SelectedClass = ""
|
||||||
self:RenderAscension()
|
self:RenderAscension()
|
||||||
@@ -46,39 +54,39 @@ self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱
|
|||||||
self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임")
|
self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임")
|
||||||
self:BindMenuButtons()`),
|
self:BindMenuButtons()`),
|
||||||
method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton")
|
method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton")
|
||||||
if buttonEntity ~= nil and buttonEntity.ButtonComponent ~= nil then
|
if buttonEntity ~= nil and (buttonEntity.ButtonComponent ~= nil or buttonEntity:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.NewGameHandler ~= nil then
|
if self.NewGameHandler ~= nil then
|
||||||
buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)
|
buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)
|
||||||
self.NewGameHandler = nil
|
self.NewGameHandler = nil
|
||||||
end
|
end
|
||||||
self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowCharacterSelect() end)
|
self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
|
||||||
end
|
end
|
||||||
local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
|
local warrior = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/WarriorButton")
|
||||||
if warrior ~= nil and warrior.ButtonComponent ~= nil then
|
if warrior ~= nil and (warrior.ButtonComponent ~= nil or warrior:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.WarriorSelectHandler ~= nil then
|
if self.WarriorSelectHandler ~= nil then
|
||||||
warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler)
|
warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler)
|
||||||
self.WarriorSelectHandler = nil
|
self.WarriorSelectHandler = nil
|
||||||
end
|
end
|
||||||
self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end)
|
self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end)
|
||||||
end
|
end
|
||||||
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
|
local thief = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/BanditButton")
|
||||||
if thief ~= nil and thief.ButtonComponent ~= nil then
|
if thief ~= nil and (thief.ButtonComponent ~= nil or thief:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.ThiefSelectHandler ~= nil then
|
if self.ThiefSelectHandler ~= nil then
|
||||||
thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler)
|
thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler)
|
||||||
self.ThiefSelectHandler = nil
|
self.ThiefSelectHandler = nil
|
||||||
end
|
end
|
||||||
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") end)
|
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("rogue") end)
|
||||||
end
|
end
|
||||||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
local mage = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/MageButton")
|
||||||
if mage ~= nil and mage.ButtonComponent ~= nil then
|
if mage ~= nil and (mage.ButtonComponent ~= nil or mage:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.MageSelectHandler ~= nil then
|
if self.MageSelectHandler ~= nil then
|
||||||
mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler)
|
mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler)
|
||||||
self.MageSelectHandler = nil
|
self.MageSelectHandler = nil
|
||||||
end
|
end
|
||||||
self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end)
|
self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end)
|
||||||
end
|
end
|
||||||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
local allDeckClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
|
||||||
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.AllDeckCloseHandler ~= nil then
|
if self.AllDeckCloseHandler ~= nil then
|
||||||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||||
self.AllDeckCloseHandler = nil
|
self.AllDeckCloseHandler = nil
|
||||||
@@ -86,16 +94,16 @@ if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
|||||||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||||
end
|
end
|
||||||
self:BindClassDeckTabs()
|
self:BindClassDeckTabs()
|
||||||
local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton")
|
local start = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/StartButton")
|
||||||
if start ~= nil and start.ButtonComponent ~= nil then
|
if start ~= nil and (start.ButtonComponent ~= nil or start:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.StartGameHandler ~= nil then
|
if self.StartGameHandler ~= nil then
|
||||||
start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler)
|
start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler)
|
||||||
self.StartGameHandler = nil
|
self.StartGameHandler = nil
|
||||||
end
|
end
|
||||||
self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)
|
self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)
|
||||||
end
|
end
|
||||||
local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton")
|
local charBack = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/BackButton")
|
||||||
if charBack ~= nil and charBack.ButtonComponent ~= nil then
|
if charBack ~= nil and (charBack.ButtonComponent ~= nil or charBack:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.CharBackHandler ~= nil then
|
if self.CharBackHandler ~= nil then
|
||||||
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
|
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
|
||||||
self.CharBackHandler = nil
|
self.CharBackHandler = nil
|
||||||
@@ -103,7 +111,7 @@ if charBack ~= nil and charBack.ButtonComponent ~= nil then
|
|||||||
self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
|
self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
|
||||||
end
|
end
|
||||||
local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus")
|
local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus")
|
||||||
if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then
|
if ascMinus ~= nil and (ascMinus.ButtonComponent ~= nil or ascMinus:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.AscMinusHandler ~= nil then
|
if self.AscMinusHandler ~= nil then
|
||||||
ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler)
|
ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler)
|
||||||
self.AscMinusHandler = nil
|
self.AscMinusHandler = nil
|
||||||
@@ -111,7 +119,7 @@ if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then
|
|||||||
self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end)
|
self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end)
|
||||||
end
|
end
|
||||||
local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus")
|
local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus")
|
||||||
if ascPlus ~= nil and ascPlus.ButtonComponent ~= nil then
|
if ascPlus ~= nil and (ascPlus.ButtonComponent ~= nil or ascPlus:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.AscPlusHandler ~= nil then
|
if self.AscPlusHandler ~= nil then
|
||||||
ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler)
|
ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler)
|
||||||
self.AscPlusHandler = nil
|
self.AscPlusHandler = nil
|
||||||
@@ -122,72 +130,45 @@ end`),
|
|||||||
self:RenderAscension()
|
self:RenderAscension()
|
||||||
self:RenderSoulLabel()
|
self:RenderSoulLabel()
|
||||||
self:ShowState("lobby")
|
self:ShowState("lobby")
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
|
self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)
|
self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)
|
||||||
self:BindLobbyButtons()
|
self:BindLobbyButtons()
|
||||||
self:BindMenuButtons()
|
self:BindMenuButtons()
|
||||||
self:GoLobbyMap()`),
|
self:GoLobbyMap()`),
|
||||||
method('GoLobbyMap', `self.LobbyTpTries = 0
|
method('RenderSoulLabel', `local soulPoints = self.SoulPoints or 0
|
||||||
local eventId = 0
|
self:SetText("/ui/LobbyUIGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", soulPoints))
|
||||||
local function go()
|
self:SetText("/ui/LobbyUIGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", soulPoints))`),
|
||||||
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
|
method('BindLobbyButtons', `if self.LobbyBound == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.LobbyBound = true
|
self.LobbyBound = true
|
||||||
local function bindClick(path, fn)
|
local function bindClick(path, handler)
|
||||||
local e = _EntityService:GetEntityByPath(path)
|
local entity = _EntityService:GetEntityByPath(path)
|
||||||
if e ~= nil and e.ButtonComponent ~= nil then
|
if entity ~= nil and (entity.ButtonComponent ~= nil or entity:AddComponent("ButtonComponent") ~= nil) then
|
||||||
e:ConnectEvent(ButtonClickEvent, fn)
|
entity:ConnectEvent(ButtonClickEvent, handler)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
bindClick("/ui/DefaultGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end)
|
bindClick("/ui/LobbyUIGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end)
|
||||||
bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end)
|
bindClick("/ui/LobbyUIGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end)
|
||||||
bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end)
|
bindClick("/ui/LobbyUIGroup/BoardHud/Close", function() self:CloseBoard() end)
|
||||||
bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`),
|
bindClick("/ui/LobbyUIGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`),
|
||||||
method('ShowCodex', `self.CodexMode = true
|
method('ShowCodex', `self.CodexMode = true
|
||||||
self.ClassDeckMode = true
|
self.ClassDeckMode = true
|
||||||
local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
local close = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
|
||||||
if close ~= nil and close.ButtonComponent ~= nil then
|
if close ~= nil and (close.ButtonComponent ~= nil or close:AddComponent("ButtonComponent") ~= nil) then
|
||||||
if self.AllDeckCloseHandler ~= nil then
|
if self.AllDeckCloseHandler ~= nil then
|
||||||
close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||||
end
|
end
|
||||||
self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||||
end
|
end
|
||||||
self:BindClassDeckTabs()
|
self:BindClassDeckTabs()
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
|
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", false)
|
||||||
self:SetClassDeckTab("warrior")
|
self:SetClassDeckTab("warrior")
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = true
|
hud.Enable = true
|
||||||
end
|
end
|
||||||
self:RenderAllDeck()`),
|
self:RenderAllDeck()`),
|
||||||
method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`),
|
method('ShowBoard', `self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", true)`),
|
||||||
method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`),
|
method('CloseBoard', `self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false)`),
|
||||||
];
|
];
|
||||||
@@ -20,11 +20,11 @@ self.ShopPotion = pkeys[math.random(1, #pkeys)]
|
|||||||
self.ShopPotionBought = false
|
self.ShopPotionBought = false
|
||||||
self:RenderShop()
|
self:RenderShop()
|
||||||
self:ShowState("shop")`),
|
self:ShowState("shop")`),
|
||||||
method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold))
|
method('RenderShop', `self:SetText("/ui/RunUIGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold))
|
||||||
for i = 1, 3 do
|
for i = 1, 3 do
|
||||||
local cid = self.ShopChoices[i]
|
local cid = self.ShopChoices[i]
|
||||||
local c = self.Cards[cid]
|
local c = self.Cards[cid]
|
||||||
local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
local base = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i)
|
||||||
if c ~= nil then
|
if c ~= nil then
|
||||||
self:ApplyCardFace(base, cid)
|
self:ApplyCardFace(base, cid)
|
||||||
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
|
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
|
||||||
@@ -38,9 +38,9 @@ for i = 1, 3 do
|
|||||||
end
|
end
|
||||||
local rr = self.Relics[self.ShopRelic]
|
local rr = self.Relics[self.ShopRelic]
|
||||||
if rr ~= nil then
|
if rr ~= nil then
|
||||||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
|
self:SetText("/ui/RunUIGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
|
||||||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
|
self:SetText("/ui/RunUIGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
|
||||||
local re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
local re = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Relic")
|
||||||
if re ~= nil and re.SpriteGUIRendererComponent ~= nil then
|
if re ~= nil and re.SpriteGUIRendererComponent ~= nil then
|
||||||
if self.ShopRelicBought == true then
|
if self.ShopRelicBought == true then
|
||||||
re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||||
@@ -51,9 +51,9 @@ if rr ~= nil then
|
|||||||
end
|
end
|
||||||
local pp = self.Potions[self.ShopPotion]
|
local pp = self.Potions[self.ShopPotion]
|
||||||
if pp ~= nil then
|
if pp ~= nil then
|
||||||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc)
|
self:SetText("/ui/RunUIGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc)
|
||||||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소")
|
self:SetText("/ui/RunUIGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소")
|
||||||
local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
local pe = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Potion")
|
||||||
if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then
|
if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then
|
||||||
if self.ShopPotionBought == true then
|
if self.ShopPotionBought == true then
|
||||||
pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||||
@@ -106,24 +106,24 @@ if self.PlayerHp > self.PlayerMaxHp then
|
|||||||
self.PlayerHp = self.PlayerMaxHp
|
self.PlayerHp = self.PlayerMaxHp
|
||||||
end
|
end
|
||||||
local healed = self.PlayerHp - old
|
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:SetText("/ui/RunUIGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")")
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:ShowState("rest")`),
|
self:ShowState("rest")`),
|
||||||
method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud")
|
method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud")
|
||||||
if s ~= nil then
|
if s ~= nil then
|
||||||
s.Enable = false
|
s.Enable = false
|
||||||
end
|
end
|
||||||
local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud")
|
local r = _EntityService:GetEntityByPath("/ui/RunUIGroup/RestHud")
|
||||||
if r ~= nil then
|
if r ~= nil then
|
||||||
r.Enable = false
|
r.Enable = false
|
||||||
end
|
end
|
||||||
local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud")
|
local t = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud")
|
||||||
if t ~= nil then
|
if t ~= nil then
|
||||||
t.Enable = false
|
t.Enable = false
|
||||||
end
|
end
|
||||||
self:ShowMap()`),
|
self:ShowMap()`),
|
||||||
method('ShowTreasure', `self.ChestOpened = false
|
method('ShowTreasure', `self.ChestOpened = false
|
||||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
|
||||||
if chest ~= nil then
|
if chest ~= nil then
|
||||||
if chest.SpriteGUIRendererComponent ~= nil then
|
if chest.SpriteGUIRendererComponent ~= nil then
|
||||||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}"
|
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}"
|
||||||
@@ -132,15 +132,15 @@ if chest ~= nil then
|
|||||||
chest.UITransformComponent.anchoredPosition = Vector2(0, 40)
|
chest.UITransformComponent.anchoredPosition = Vector2(0, 40)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Reward", false)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Hint", true)
|
||||||
self:ShowState("treasure")`),
|
self:ShowState("treasure")`),
|
||||||
method('OpenChest', `if self.ChestOpened == true then
|
method('OpenChest', `if self.ChestOpened == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.ChestOpened = true
|
self.ChestOpened = true
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false)
|
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Hint", false)
|
||||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
|
||||||
local steps = { 10, -10, 8, -8, 5, 0 }
|
local steps = { 10, -10, 8, -8, 5, 0 }
|
||||||
for i = 1, #steps do
|
for i = 1, #steps do
|
||||||
local dx = steps[i]
|
local dx = steps[i]
|
||||||
@@ -167,7 +167,7 @@ _TimerService:SetTimerOnce(function()
|
|||||||
end
|
end
|
||||||
self.Gold = self.Gold + g
|
self.Gold = self.Gold + g
|
||||||
self:RenderRun()
|
self:RenderRun()
|
||||||
self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg)
|
self:SetText("/ui/RunUIGroup/TreasureHud/Reward", msg)
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true)
|
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Reward", true)
|
||||||
end, 0.55)`),
|
end, 0.55)`),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export const soulMethods = [
|
|||||||
method('ShowSoulShop', `self:RenderSoulLabel()
|
method('ShowSoulShop', `self:RenderSoulLabel()
|
||||||
self:RenderSoulShop()
|
self:RenderSoulShop()
|
||||||
self:BindSoulShopButtons()
|
self:BindSoulShopButtons()
|
||||||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`),
|
self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", true)`),
|
||||||
method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
|
method('CloseSoulShop', `self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)`),
|
||||||
method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||||
local e1, pts = ds:GetAndWait("soulPoints")
|
local e1, pts = ds:GetAndWait("soulPoints")
|
||||||
local e2, unl = ds:GetAndWait("soulUnlocks")
|
local e2, unl = ds:GetAndWait("soulUnlocks")
|
||||||
@@ -63,7 +63,7 @@ self:RenderSoulLabel()
|
|||||||
self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]),
|
self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]),
|
||||||
method('RenderSoulShop', `local defs = self.SoulShopDef or {}
|
method('RenderSoulShop', `local defs = self.SoulShopDef or {}
|
||||||
for i = 1, 4 do
|
for i = 1, 4 do
|
||||||
local base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)
|
local base = "/ui/LobbyUIGroup/SoulShopHud/Item" .. tostring(i)
|
||||||
local d = defs[i]
|
local d = defs[i]
|
||||||
if d == nil then
|
if d == nil then
|
||||||
self:SetEntityEnabled(base, false)
|
self:SetEntityEnabled(base, false)
|
||||||
@@ -87,8 +87,8 @@ end
|
|||||||
self.SoulShopBound = true
|
self.SoulShopBound = true
|
||||||
for i = 1, 4 do
|
for i = 1, 4 do
|
||||||
local idx = i
|
local idx = i
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i))
|
local e = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/SoulShopHud/Item" .. tostring(i))
|
||||||
if e ~= nil and e.ButtonComponent ~= nil then
|
if e ~= nil and (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then
|
||||||
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
|
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
|
||||||
end
|
end
|
||||||
end`),
|
end`),
|
||||||
|
|||||||
@@ -3,6 +3,47 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
|
|||||||
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 { 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 = [
|
export const tooltipMethods = [
|
||||||
|
method('FormatCardDescription', `if desc == nil or desc == "" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
local function replacePlain(text, needle, replacement)
|
||||||
|
local out = ""
|
||||||
|
local pos = 1
|
||||||
|
while true do
|
||||||
|
local s, e = string.find(text, needle, pos, true)
|
||||||
|
if s == nil then
|
||||||
|
out = out .. string.sub(text, pos)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
out = out .. string.sub(text, pos, s - 1) .. replacement
|
||||||
|
pos = e + 1
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
local terms = {
|
||||||
|
"교활",
|
||||||
|
"보존",
|
||||||
|
"민첩",
|
||||||
|
"가시",
|
||||||
|
"소멸",
|
||||||
|
"선천성",
|
||||||
|
"취약",
|
||||||
|
"약화",
|
||||||
|
"독",
|
||||||
|
"광역",
|
||||||
|
"관통",
|
||||||
|
"방어도",
|
||||||
|
"힘",
|
||||||
|
"스킬",
|
||||||
|
"공격",
|
||||||
|
"파워",
|
||||||
|
}
|
||||||
|
local out = desc
|
||||||
|
for i = 1, #terms do
|
||||||
|
local term = terms[i]
|
||||||
|
out = replacePlain(out, term, "<color=#70D6FF>" .. term .. "</color>")
|
||||||
|
end
|
||||||
|
return out`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }], 0, 'string'),
|
||||||
method('BuildCardKeywordTooltip', `if c == nil then
|
method('BuildCardKeywordTooltip', `if c == nil then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
@@ -64,11 +105,14 @@ return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [
|
|||||||
method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then
|
method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self.Hand == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
local cardId = self.Hand[slot]
|
local cardId = self.Hand[slot]
|
||||||
if cardId == nil then
|
if cardId == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
local tx = 0
|
local tx = 0
|
||||||
if e ~= nil and e.UITransformComponent ~= nil then
|
if e ~= nil and e.UITransformComponent ~= nil then
|
||||||
tx = e.UITransformComponent.anchoredPosition.x
|
tx = e.UITransformComponent.anchoredPosition.x
|
||||||
@@ -87,7 +131,7 @@ if c ~= nil then
|
|||||||
self:HideTooltip()
|
self:HideTooltip()
|
||||||
end
|
end
|
||||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||||
method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||||
if e ~= nil and e.UITransformComponent ~= nil then
|
if e ~= nil and e.UITransformComponent ~= nil then
|
||||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||||
end
|
end
|
||||||
@@ -97,9 +141,9 @@ self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, At
|
|||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
|
{ 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: 'x' },
|
||||||
]),
|
]),
|
||||||
method('ShowTooltipAt', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name)
|
method('ShowTooltipAt', `self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Name", name)
|
||||||
self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc)
|
self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Desc", desc)
|
||||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox")
|
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TooltipBox")
|
||||||
if e ~= nil then
|
if e ~= nil then
|
||||||
if e.UITransformComponent ~= nil then
|
if e.UITransformComponent ~= nil then
|
||||||
e.UITransformComponent.anchoredPosition = Vector2(x, y)
|
e.UITransformComponent.anchoredPosition = Vector2(x, y)
|
||||||
@@ -111,5 +155,5 @@ end`, [
|
|||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' },
|
||||||
]),
|
]),
|
||||||
method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`),
|
method('HideTooltip', `self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/TooltipBox", false)`),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { readFileSync, writeFileSync } from 'node:fs';
|
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 { POTIONS } 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 { prop, codeblock, RUN_LENGTH } from './lib/codeblock.mjs';
|
||||||
import { bootMethods } from './cb/boot.mjs';
|
import { bootMethods } from './cb/boot.mjs';
|
||||||
import { stateMethods } from './cb/state.mjs';
|
import { screensMethods } from './cb/screens.mjs';
|
||||||
|
import { npcMethods } from './cb/npc.mjs';
|
||||||
|
import { navigationMethods } from './cb/navigation.mjs';
|
||||||
|
import { layoutMethods } from './cb/layout.mjs';
|
||||||
import { soulMethods } from './cb/soul.mjs';
|
import { soulMethods } from './cb/soul.mjs';
|
||||||
import { charSelectMethods } from './cb/charselect.mjs';
|
import { charSelectMethods } from './cb/charselect.mjs';
|
||||||
import { runMethods } from './cb/run.mjs';
|
import { runMethods } from './cb/run.mjs';
|
||||||
@@ -19,243 +22,7 @@ import { itemMethods } from './cb/items.mjs';
|
|||||||
import { tooltipMethods } from './cb/tooltip.mjs';
|
import { tooltipMethods } from './cb/tooltip.mjs';
|
||||||
import { mapMethods } from './cb/map.mjs';
|
import { mapMethods } from './cb/map.mjs';
|
||||||
import { shopMethods } from './cb/shop.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 { COMMON_FILE } 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';
|
|
||||||
import { buildCharSelect } from './hud/charselect.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());
|
|
||||||
emit('CharacterSelectHud', buildCharSelect());
|
|
||||||
|
|
||||||
// ── 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() {
|
function writeCodeblocks() {
|
||||||
const combat = codeblock('SlayDeckController', 'SlayDeckController', [
|
const combat = codeblock('SlayDeckController', 'SlayDeckController', [
|
||||||
@@ -281,6 +48,9 @@ function writeCodeblocks() {
|
|||||||
prop('any', 'AscPlusHandler'),
|
prop('any', 'AscPlusHandler'),
|
||||||
prop('any', 'JobOpts'),
|
prop('any', 'JobOpts'),
|
||||||
prop('any', 'Jobs'),
|
prop('any', 'Jobs'),
|
||||||
|
prop('any', 'JobMeta'),
|
||||||
|
prop('any', 'ClassGroups'),
|
||||||
|
prop('any', 'ClassLineages'),
|
||||||
prop('number', 'AscensionLevel', '0'),
|
prop('number', 'AscensionLevel', '0'),
|
||||||
prop('number', 'AscensionUnlocked', '0'),
|
prop('number', 'AscensionUnlocked', '0'),
|
||||||
prop('any', 'StartGameHandler'),
|
prop('any', 'StartGameHandler'),
|
||||||
@@ -294,10 +64,14 @@ function writeCodeblocks() {
|
|||||||
prop('any', 'AllDeckCloseHandler'),
|
prop('any', 'AllDeckCloseHandler'),
|
||||||
prop('number', 'SoulPoints', '0'),
|
prop('number', 'SoulPoints', '0'),
|
||||||
prop('boolean', 'LobbyBound', 'false'),
|
prop('boolean', 'LobbyBound', 'false'),
|
||||||
|
prop('boolean', 'ButtonsBound', 'false'),
|
||||||
prop('number', 'LobbyTpTries', '0'),
|
prop('number', 'LobbyTpTries', '0'),
|
||||||
prop('boolean', 'CodexMode', 'false'),
|
prop('boolean', 'CodexMode', 'false'),
|
||||||
prop('any', 'CodexCards'),
|
prop('any', 'CodexCards'),
|
||||||
prop('boolean', 'ClassDeckMode', 'false'),
|
prop('boolean', 'ClassDeckMode', 'false'),
|
||||||
|
prop('boolean', 'DebugCardPickerMode', 'false'),
|
||||||
|
prop('boolean', 'DebugCtrlDown', 'false'),
|
||||||
|
prop('boolean', 'DebugShiftDown', 'false'),
|
||||||
prop('any', 'ClassDeckCards'),
|
prop('any', 'ClassDeckCards'),
|
||||||
prop('string', 'ClassDeckTitle', '""'),
|
prop('string', 'ClassDeckTitle', '""'),
|
||||||
prop('string', 'ClassDeckClass', '""'),
|
prop('string', 'ClassDeckClass', '""'),
|
||||||
@@ -309,10 +83,12 @@ function writeCodeblocks() {
|
|||||||
prop('any', 'Cards'),
|
prop('any', 'Cards'),
|
||||||
prop('any', 'CardFrames'),
|
prop('any', 'CardFrames'),
|
||||||
prop('any', 'NodeIcons'),
|
prop('any', 'NodeIcons'),
|
||||||
|
prop('any', 'ClassPortraits'),
|
||||||
prop('any', 'ClassToFrame'),
|
prop('any', 'ClassToFrame'),
|
||||||
prop('number', 'PlayerHp', '0'),
|
prop('number', 'PlayerHp', '0'),
|
||||||
prop('number', 'PlayerMaxHp', '80'),
|
prop('number', 'PlayerMaxHp', '80'),
|
||||||
prop('number', 'PlayerBlock', '0'),
|
prop('number', 'PlayerBlock', '0'),
|
||||||
|
prop('number', 'BlockGainMultiplier', '1'),
|
||||||
prop('number', 'PlayerDex', '0'),
|
prop('number', 'PlayerDex', '0'),
|
||||||
prop('number', 'PlayerThorns', '0'),
|
prop('number', 'PlayerThorns', '0'),
|
||||||
prop('boolean', 'CombatOver', 'false'),
|
prop('boolean', 'CombatOver', 'false'),
|
||||||
@@ -344,13 +120,34 @@ function writeCodeblocks() {
|
|||||||
prop('number', 'PlayerStr', '0'),
|
prop('number', 'PlayerStr', '0'),
|
||||||
prop('number', 'PlayerWeak', '0'),
|
prop('number', 'PlayerWeak', '0'),
|
||||||
prop('number', 'PlayerVuln', '0'),
|
prop('number', 'PlayerVuln', '0'),
|
||||||
|
prop('number', 'PlayerIntangible', '0'),
|
||||||
prop('any', 'PlayerPowers'),
|
prop('any', 'PlayerPowers'),
|
||||||
prop('any', 'Potions'),
|
prop('any', 'Potions'),
|
||||||
prop('any', 'RunPotions'),
|
prop('any', 'RunPotions'),
|
||||||
prop('number', 'PotionSlots', String(POTIONS.baseSlots)),
|
prop('number', 'PotionSlots', String(POTIONS.baseSlots)),
|
||||||
prop('string', 'ShopPotion', '""'),
|
prop('string', 'ShopPotion', '""'),
|
||||||
prop('boolean', 'ShopPotionBought', 'false'),
|
prop('boolean', 'ShopPotionBought', 'false'),
|
||||||
|
prop('number', 'CardsDrawnThisCombat', '0'),
|
||||||
|
prop('boolean', 'HandCostZeroThisTurn', 'false'),
|
||||||
|
prop('boolean', 'DrawDisabledThisTurn', 'false'),
|
||||||
|
prop('number', 'SkillCostReductionThisTurn', '0'),
|
||||||
|
prop('any', 'SkillSlyOnPlayCards'),
|
||||||
|
prop('any', 'TurnSkillSlyCards'),
|
||||||
|
prop('boolean', 'ShivFirstDamageBonusUsed', 'false'),
|
||||||
|
prop('any', 'CombatCardCostReduction'),
|
||||||
|
prop('number', 'ActiveAttackDamageVsWeakMultiplier', '1'),
|
||||||
|
prop('number', 'DrawDamageThisTurn', '0'),
|
||||||
|
prop('number', 'DrawPoisonThisTurn', '0'),
|
||||||
|
prop('boolean', 'ShivAoeThisCombat', 'false'),
|
||||||
|
prop('number', 'PoisonApplicationsThisCombat', '0'),
|
||||||
|
prop('number', 'EnemyStrengthLossThisTurn', '0'),
|
||||||
|
prop('number', 'ActiveKillReward', '0'),
|
||||||
|
prop('number', 'BonusRewardScreens', '0'),
|
||||||
prop('number', 'FightAttackCount', '0'),
|
prop('number', 'FightAttackCount', '0'),
|
||||||
|
prop('number', 'TurnAttackCardsPlayed', '0'),
|
||||||
|
prop('number', 'TurnCardsPlayedThisTurn', '0'),
|
||||||
|
prop('number', 'DamageDealtThisTurn', '0'),
|
||||||
|
prop('number', 'TurnDiscardedCards', '0'),
|
||||||
prop('boolean', 'FirstHpLossDone', 'false'),
|
prop('boolean', 'FirstHpLossDone', 'false'),
|
||||||
prop('number', 'ClayBlockNext', '0'),
|
prop('number', 'ClayBlockNext', '0'),
|
||||||
prop('number', 'PotionMenuSlot', '0'),
|
prop('number', 'PotionMenuSlot', '0'),
|
||||||
@@ -362,9 +159,25 @@ function writeCodeblocks() {
|
|||||||
prop('number', 'DiscardSelectTotal', '0'),
|
prop('number', 'DiscardSelectTotal', '0'),
|
||||||
prop('number', 'DiscardPostShiv', '0'),
|
prop('number', 'DiscardPostShiv', '0'),
|
||||||
prop('number', 'DiscardShivPerPick', '0'),
|
prop('number', 'DiscardShivPerPick', '0'),
|
||||||
|
prop('number', 'DiscardPostDraw', '0'),
|
||||||
|
prop('number', 'DiscardDrawPerPick', '0'),
|
||||||
|
prop('boolean', 'RetainSelectActive', 'false'),
|
||||||
|
prop('boolean', 'ReserveSelectActive', 'false'),
|
||||||
|
prop('number', 'NextTurnBlock', '0'),
|
||||||
|
prop('number', 'NextTurnDraw', '0'),
|
||||||
|
prop('boolean', 'NextTurnKeepBlock', 'false'),
|
||||||
|
prop('number', 'NextTurnAttackMultiplier', '1'),
|
||||||
|
prop('number', 'TurnAttackMultiplier', '1'),
|
||||||
|
prop('string', 'NextTurnSelectPrompt', '""'),
|
||||||
|
prop('number', 'NextTurnSelectCopies', '0'),
|
||||||
|
prop('boolean', 'NextSkillCostZero', 'false'),
|
||||||
|
prop('number', 'NextSkillRepeatCount', '0'),
|
||||||
|
prop('any', 'NextTurnAddCards'),
|
||||||
], [
|
], [
|
||||||
...bootMethods,
|
...bootMethods,
|
||||||
...stateMethods,
|
...screensMethods,
|
||||||
|
...npcMethods,
|
||||||
|
...navigationMethods,
|
||||||
...soulMethods,
|
...soulMethods,
|
||||||
...charSelectMethods,
|
...charSelectMethods,
|
||||||
...runMethods,
|
...runMethods,
|
||||||
@@ -375,6 +188,7 @@ function writeCodeblocks() {
|
|||||||
...jobMethods,
|
...jobMethods,
|
||||||
...runEndMethods,
|
...runEndMethods,
|
||||||
...renderMethods,
|
...renderMethods,
|
||||||
|
...layoutMethods,
|
||||||
...rewardMethods,
|
...rewardMethods,
|
||||||
...itemMethods,
|
...itemMethods,
|
||||||
...tooltipMethods,
|
...tooltipMethods,
|
||||||
@@ -398,8 +212,7 @@ function patchCommon() {
|
|||||||
writeFileSync(COMMON_FILE, JSON.stringify(common, null, 2), 'utf8');
|
writeFileSync(COMMON_FILE, JSON.stringify(common, null, 2), 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertUi();
|
|
||||||
writeCodeblocks();
|
writeCodeblocks();
|
||||||
patchCommon();
|
patchCommon();
|
||||||
|
|
||||||
console.log('Slay deck UI and combat codeblocks generated.');
|
console.log('SlayDeckController/common 생성 완료 (UI는 메이커 저작 — 생성기 미접근).');
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
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 buildCharSelect() {
|
|
||||||
const select = [];
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 100),
|
|
||||||
path: '/ui/DefaultGroup/CharacterSelectHud',
|
|
||||||
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: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
|
||||||
sprite({ color: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 190),
|
|
||||||
path: '/ui/DefaultGroup/CharacterSelectHud/OpaqueBackdrop',
|
|
||||||
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: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
|
||||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 101),
|
|
||||||
path: '/ui/DefaultGroup/CharacterSelectHud/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: 72 }, pos: { x: 0, y: 355 }, align: ALIGN_CENTER }),
|
|
||||||
sprite({ color: TRANSPARENT }),
|
|
||||||
text({ value: '\uCE90\uB9AD\uD130 \uC120\uD0DD', fontSize: 42, bold: true, color: GOLD, alignment: 0 }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 102),
|
|
||||||
path: '/ui/DefaultGroup/CharacterSelectHud/Status',
|
|
||||||
modelId: 'uitext',
|
|
||||||
entryId: 'UIText',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,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: 680, y: 44 }, pos: { x: 0, y: 298 }, align: ALIGN_CENTER }),
|
|
||||||
sprite({ color: TRANSPARENT }),
|
|
||||||
text({ value: '\uC804\uC0AC\uB97C \uC120\uD0DD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694', fontSize: 22, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
const classCards = [
|
|
||||||
{ key: 'Warrior', classId: 'warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } },
|
|
||||||
{ key: 'Thief', classId: 'bandit', label: '\uB3C4\uC801', desc: '\uB3C5\u00B7\uB2E8\uAC80\u00B7\uB4DC\uB85C\uC6B0', x: 0, enabled: true, tint: { r: 0.26, g: 0.5, b: 0.34, a: 1 } },
|
|
||||||
{ key: 'Mage', classId: 'magician', label: '\uB9C8\uBC95\uC0AC', desc: '\uB9C8\uBC95 \uC6D0\uAC70\uB9AC \uB51C\uB7EC', x: 360, enabled: true, tint: { r: 0.3, g: 0.4, b: 0.75, a: 1 } },
|
|
||||||
];
|
|
||||||
for (let i = 0; i < classCards.length; i++) {
|
|
||||||
const cls = classCards[i];
|
|
||||||
const base = `/ui/DefaultGroup/CharacterSelectHud/${cls.key}Button`;
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 110 + i),
|
|
||||||
path: base,
|
|
||||||
modelId: 'uibutton',
|
|
||||||
entryId: 'UIButton',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
|
||||||
displayOrder: 10 + i,
|
|
||||||
components: [
|
|
||||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 270, y: 330 }, pos: { x: cls.x, y: 40 }, align: ALIGN_CENTER }),
|
|
||||||
sprite({ color: cls.enabled ? { r: 0.16, g: 0.2, b: 0.26, a: 1 } : { r: 0.11, g: 0.12, b: 0.14, a: 1 }, type: 1, raycast: cls.enabled }),
|
|
||||||
button({ enabled: cls.enabled }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 200 + i),
|
|
||||||
path: `${base}/Art`,
|
|
||||||
modelId: 'uisprite',
|
|
||||||
entryId: 'UISprite',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
|
||||||
displayOrder: 0,
|
|
||||||
components: [
|
|
||||||
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 318 }, pos: { x: 0, y: 0 } }),
|
|
||||||
sprite({ dataId: CHARS.portraits[cls.classId], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 210 + i),
|
|
||||||
path: `${base}/NameBanner`,
|
|
||||||
modelId: 'uisprite',
|
|
||||||
entryId: 'UISprite',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
|
||||||
displayOrder: 1,
|
|
||||||
components: [
|
|
||||||
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 60 }, pos: { x: 0, y: -137 } }),
|
|
||||||
sprite({ color: { r: 0, g: 0, b: 0, a: 0.55 }, type: 1, raycast: false }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 120 + i),
|
|
||||||
path: `${base}/Name`,
|
|
||||||
modelId: 'uitext',
|
|
||||||
entryId: 'UIText',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
|
||||||
displayOrder: 2,
|
|
||||||
components: [
|
|
||||||
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 54 }, pos: { x: 0, y: -137 } }),
|
|
||||||
sprite({ color: TRANSPARENT }),
|
|
||||||
text({ value: cls.label, fontSize: 34, bold: true, color: cls.enabled ? GOLD : { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 4 }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
if (!cls.enabled) {
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 150 + i),
|
|
||||||
path: `${base}/LockBody`,
|
|
||||||
modelId: 'uisprite',
|
|
||||||
entryId: 'UISprite',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
|
||||||
displayOrder: 3,
|
|
||||||
components: [
|
|
||||||
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 76, y: 58 }, pos: { x: 0, y: 4 } }),
|
|
||||||
sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 160 + i),
|
|
||||||
path: `${base}/LockShackle`,
|
|
||||||
modelId: 'uisprite',
|
|
||||||
entryId: 'UISprite',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
|
||||||
displayOrder: 4,
|
|
||||||
components: [
|
|
||||||
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 54, y: 42 }, pos: { x: 0, y: 48 } }),
|
|
||||||
sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 180),
|
|
||||||
path: '/ui/DefaultGroup/CharacterSelectHud/StartButton',
|
|
||||||
modelId: 'uibutton',
|
|
||||||
entryId: 'UIButton',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
|
||||||
displayOrder: 20,
|
|
||||||
components: [
|
|
||||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 68 }, pos: { x: 720, y: -360 }, align: ALIGN_CENTER }),
|
|
||||||
sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }),
|
|
||||||
button(),
|
|
||||||
text({ value: '\uC2DC\uC791', fontSize: 30, bold: true, color: GOLD, alignment: 0 }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select.push(entity({
|
|
||||||
id: guid('menu', 230),
|
|
||||||
path: '/ui/DefaultGroup/CharacterSelectHud/BackButton',
|
|
||||||
modelId: 'uibutton',
|
|
||||||
entryId: 'UIButton',
|
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,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: 180, y: 56 }, pos: { x: -800, y: 430 }, align: ALIGN_CENTER }),
|
|
||||||
sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }),
|
|
||||||
button(),
|
|
||||||
text({ value: '\u2190 \uB4A4\uB85C', fontSize: 26, bold: true, color: GOLD, alignment: 0 }),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
select[0].jsonString.enable = false;
|
|
||||||
return select;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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() {
|
export function buildBoard() {
|
||||||
const board = [];
|
const board = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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() {
|
export function buildCombat() {
|
||||||
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
|
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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() {
|
export function buildDeckAll() {
|
||||||
const allDeck = [];
|
const allDeck = [];
|
||||||
@@ -116,11 +116,12 @@ export function buildDeckAll() {
|
|||||||
path: cardPath,
|
path: cardPath,
|
||||||
modelId: 'uisprite',
|
modelId: 'uisprite',
|
||||||
entryId: 'UISprite',
|
entryId: 'UISprite',
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||||
displayOrder: i,
|
displayOrder: i,
|
||||||
components: [
|
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 } }),
|
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 }),
|
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }),
|
||||||
|
button(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
card.jsonString.enable = false;
|
card.jsonString.enable = false;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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() {
|
export function buildDeckHud() {
|
||||||
const hud = [];
|
const hud = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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() {
|
export function buildDeckInspect() {
|
||||||
const inspect = [];
|
const inspect = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildJobChoice() {
|
export function buildJobChoice() {
|
||||||
const jobChoice = [];
|
const jobChoice = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildJobSelect() {
|
export function buildJobSelect() {
|
||||||
const jobSelect = [];
|
const jobSelect = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildLobby() {
|
export function buildLobby() {
|
||||||
const lobby = [];
|
const lobby = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildMainMenu() {
|
export function buildMainMenu() {
|
||||||
const menu = [];
|
const menu = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildMap() {
|
export function buildMap() {
|
||||||
const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스', shop: '상점', rest: '휴식' };
|
const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스', shop: '상점', rest: '휴식' };
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildRest() {
|
export function buildRest() {
|
||||||
const rest = [];
|
const rest = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildReward() {
|
export function buildReward() {
|
||||||
const reward = [];
|
const reward = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildShop() {
|
export function buildShop() {
|
||||||
const shop = [];
|
const shop = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildSoulShop() {
|
export function buildSoulShop() {
|
||||||
const soulShop = [];
|
const soulShop = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
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 buildTreasure() {
|
export function buildTreasure() {
|
||||||
const treasure = [];
|
const treasure = [];
|
||||||
246
tools/deck/legacy/upsert-ui.mjs
Normal file
246
tools/deck/legacy/upsert-ui.mjs
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { CARDS, frameRuid } from '../lib/data.mjs';
|
||||||
|
import { UI_FILE, isGeneratedUiEntity, DISABLED_STOCK_CONTROLS, uiPath, guid, entity, transform, sprite, button, text, WHITE, TRANSPARENT, CARD_W, CARD_H, cardFaceLayout, UI_APPEND_ORDER, 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';
|
||||||
|
|
||||||
|
// ⚠️ 휴면(LEGACY): 메이커 저작 전환 후 생성기는 .ui를 안 만든다. 이 파일은 옛 DefaultGroup.ui
|
||||||
|
// 단일 저작 로직의 롤백/참조용. import는 무해(함수만 정의), 직접 실행할 때만 .ui를 옛 생성본으로 덮어쓴다.
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 롤백/참조용 직접 실행 시에만 동작 (import 시에는 실행 안 함)
|
||||||
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||||
|
upsertUi();
|
||||||
|
}
|
||||||
@@ -54,7 +54,8 @@ const REST_HEAL = 30;
|
|||||||
const RELIC_PRICE = 60;
|
const RELIC_PRICE = 60;
|
||||||
const ACT_COUNT = 5;
|
const ACT_COUNT = 5;
|
||||||
const ACT_MAPS = ['map01', 'map02', 'map03', 'map04', 'map05'];
|
const ACT_MAPS = ['map01', 'map02', 'map03', 'map04', 'map05'];
|
||||||
|
const ACT_DIFFICULTY_MULTIPLIERS = [1, 1.075, 1.15, 1.3, 1.45];
|
||||||
const LOBBY_MAP = 'lobby';
|
const LOBBY_MAP = 'lobby';
|
||||||
const LOBBY_SPAWN = 'Vector3(-5, 0.03, 0)'; // 정찰: map01 지면 좌측
|
const LOBBY_SPAWN = 'Vector3(-5, 0.03, 0)'; // 정찰: map01 지면 좌측
|
||||||
|
|
||||||
export { prop, method, codeblock, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN };
|
export { prop, method, codeblock, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, ACT_DIFFICULTY_MULTIPLIERS, LOBBY_MAP, LOBBY_SPAWN };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
|
|||||||
// 검증 (fail-fast): 잘못된 데이터면 생성 중단
|
// 검증 (fail-fast): 잘못된 데이터면 생성 중단
|
||||||
const CLASSES = {
|
const CLASSES = {
|
||||||
warrior: { label: '전사', maxHp: 80 },
|
warrior: { label: '전사', maxHp: 80 },
|
||||||
bandit: { label: '도적', maxHp: 70 },
|
rogue: { label: '도적', maxHp: 70 },
|
||||||
magician: { label: '마법사', maxHp: 70 },
|
magician: { label: '마법사', maxHp: 70 },
|
||||||
};
|
};
|
||||||
for (const cls of Object.keys(CLASSES)) {
|
for (const cls of Object.keys(CLASSES)) {
|
||||||
@@ -15,22 +15,28 @@ for (const cls of Object.keys(CLASSES)) {
|
|||||||
if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`);
|
if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드)
|
|
||||||
|
// 전직 옵션
|
||||||
const JOBS = {
|
const JOBS = {
|
||||||
warrior: [
|
warrior: [
|
||||||
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' },
|
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack', tier: 2, parent: 'warrior' },
|
||||||
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' },
|
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge', tier: 2, parent: 'warrior' },
|
||||||
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' },
|
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce', tier: 2, parent: 'warrior' },
|
||||||
],
|
],
|
||||||
magician: [
|
magician: [
|
||||||
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' },
|
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow', tier: 2, parent: 'magician' },
|
||||||
{ id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' },
|
{ id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt', tier: 2, parent: 'magician' },
|
||||||
{ id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' },
|
{ id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal', tier: 2, parent: 'magician' },
|
||||||
],
|
],
|
||||||
bandit: [
|
rogue: [
|
||||||
{ id: 'shiv', name: 'Shiv', desc: 'Many small attacks\nBlade Dance\nAccuracy · After Image', starter: 'BladeDance' },
|
{ id: 'assassin', name: 'Assassin', desc: '표창 중심 전직\n표창 생성과 연속 공격\n빠른 마무리', starter: 'JavelinAcceleration', tier: 2, parent: 'rogue' },
|
||||||
{ id: 'poisoner', name: 'Poison', desc: 'Poison scaling\nDeadly Poison\nCatalyst · Noxious Fumes', starter: 'DeadlyPoison' },
|
{ id: 'thief', name: 'Thief', desc: '단검 중심 전직\n드로우와 운영 강화\n빠른 연계', starter: 'DaggerAcceleration', tier: 2, parent: 'rogue' },
|
||||||
{ id: 'trickster', name: 'Trickster', desc: 'Draw and tempo\nAcrobatics\nAdrenaline · Tools', starter: 'Acrobatics' },
|
],
|
||||||
|
assassin: [
|
||||||
|
{ id: 'hermit', name: 'Hermit', desc: 'Assassin의 3차 전직\n표창 생성과 강화 심화\n연속 공격 완성', starter: 'SpiritJavelin', tier: 3, parent: 'assassin' },
|
||||||
|
],
|
||||||
|
thief: [
|
||||||
|
{ id: 'thiefmaster', name: 'Thief Master', desc: 'Thief의 3차 전직\n단검·교활·중독 심화\n연계 운영 완성', starter: 'Venom', tier: 3, parent: 'thief' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
for (const [cls, jobs] of Object.entries(JOBS)) {
|
for (const [cls, jobs] of Object.entries(JOBS)) {
|
||||||
@@ -38,6 +44,42 @@ for (const [cls, jobs] of Object.entries(JOBS)) {
|
|||||||
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
|
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CLASS_GROUPS = {
|
||||||
|
warrior: ['warrior', 'fighter', 'page', 'spearman'],
|
||||||
|
magician: ['magician', 'firepoison', 'icelightning', 'cleric'],
|
||||||
|
rogue: ['rogue', 'assassin', 'hermit', 'thief', 'thiefmaster'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLASS_LINEAGES = {
|
||||||
|
warrior: ['warrior'],
|
||||||
|
fighter: ['warrior', 'fighter'],
|
||||||
|
page: ['warrior', 'page'],
|
||||||
|
spearman: ['warrior', 'spearman'],
|
||||||
|
magician: ['magician'],
|
||||||
|
firepoison: ['magician', 'firepoison'],
|
||||||
|
icelightning: ['magician', 'icelightning'],
|
||||||
|
cleric: ['magician', 'cleric'],
|
||||||
|
rogue: ['rogue'],
|
||||||
|
assassin: ['rogue', 'assassin'],
|
||||||
|
hermit: ['rogue', 'assassin', 'hermit'],
|
||||||
|
thief: ['rogue', 'thief'],
|
||||||
|
thiefmaster: ['rogue', 'thief', 'thiefmaster'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const JOB_META = {};
|
||||||
|
for (const [sourceClass, jobs] of Object.entries(JOBS)) {
|
||||||
|
for (const job of jobs) {
|
||||||
|
JOB_META[job.id] = {
|
||||||
|
name: job.name,
|
||||||
|
starter: job.starter,
|
||||||
|
tier: job.tier ?? 2,
|
||||||
|
parent: job.parent ?? sourceClass,
|
||||||
|
sourceClass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점
|
// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점
|
||||||
const SOUL_UNLOCKS = [
|
const SOUL_UNLOCKS = [
|
||||||
{ key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 },
|
{ key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 },
|
||||||
@@ -79,29 +121,29 @@ function luaNodeIconsTable() {
|
|||||||
const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n');
|
const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n');
|
||||||
return `self.NodeIcons = {\n${rows}\n}`;
|
return `self.NodeIcons = {\n${rows}\n}`;
|
||||||
}
|
}
|
||||||
|
function luaCharsTable() {
|
||||||
|
const rows = Object.entries(CHARS.portraits).map(([c, ruid]) => `\t${c} = ${luaStr(ruid)},`).join('\n');
|
||||||
|
return `self.ClassPortraits = {\n${rows}\n}`;
|
||||||
|
}
|
||||||
|
|
||||||
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
|
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
|
||||||
const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7)
|
const MAP_ROWS = 6;
|
||||||
const MAP_COLS = 4;
|
const MAP_COLS = 4;
|
||||||
|
|
||||||
// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별)
|
|
||||||
const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71';
|
const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71';
|
||||||
const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759';
|
const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759';
|
||||||
|
|
||||||
// 노드 맵 아이콘/배경 (공식 maplestory RUID, data/nodeicons.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
|
|
||||||
const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8'));
|
const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8'));
|
||||||
for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) {
|
for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) {
|
||||||
if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`);
|
if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`);
|
||||||
}
|
}
|
||||||
if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류');
|
if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류');
|
||||||
|
|
||||||
// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
|
|
||||||
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
|
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
|
||||||
for (const c of ['warrior', 'magician', 'bandit']) {
|
for (const c of ['warrior', 'magician', 'rogue']) {
|
||||||
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
|
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전투 카메라 고정값(StS2: 플레이어 좌·몬스터 우). KickCombatCamera가 StartCombat에서 재confine에 사용.
|
|
||||||
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
|
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
|
||||||
|
|
||||||
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
||||||
@@ -139,25 +181,60 @@ function luaEnemiesTable(enemies) {
|
|||||||
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
|
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
|
||||||
return `self.Enemies = {\n${lines.join('\n')}\n}`;
|
return `self.Enemies = {\n${lines.join('\n')}\n}`;
|
||||||
}
|
}
|
||||||
// Lua 직렬화 헬퍼
|
|
||||||
function luaStr(s) {
|
function luaStr(s) {
|
||||||
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
||||||
}
|
}
|
||||||
function luaJobsTable(jobs) {
|
function luaJobsTable(jobs) {
|
||||||
const cls = Object.entries(jobs).map(([clsId, list]) => {
|
const cls = Object.entries(jobs).map(([clsId, list]) => {
|
||||||
const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n');
|
const items = list.map((j) =>
|
||||||
|
`\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)}, tier = ${j.tier ?? 2}, parent = ${luaStr(j.parent ?? clsId)} },`).join('\n');
|
||||||
return `\t${clsId} = {\n${items}\n\t},`;
|
return `\t${clsId} = {\n${items}\n\t},`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
return `self.Jobs = {\n${cls}\n}`;
|
return `self.Jobs = {\n${cls}\n}`;
|
||||||
}
|
}
|
||||||
|
function luaClassGroupsTable(groups) {
|
||||||
|
const rows = Object.entries(groups).map(([clsId, list]) =>
|
||||||
|
`\t${clsId} = { ${list.map(luaStr).join(', ')} },`).join('\n');
|
||||||
|
return `self.ClassGroups = {\n${rows}\n}`;
|
||||||
|
}
|
||||||
|
function luaClassLineagesTable(lineages) {
|
||||||
|
const rows = Object.entries(lineages).map(([clsId, list]) =>
|
||||||
|
`\t${clsId} = { ${list.map(luaStr).join(', ')} },`).join('\n');
|
||||||
|
return `self.ClassLineages = {\n${rows}\n}`;
|
||||||
|
}
|
||||||
|
function luaJobMetaTable(meta) {
|
||||||
|
const rows = Object.entries(meta).map(([jobId, entry]) =>
|
||||||
|
`\t${jobId} = { name = ${luaStr(entry.name)}, starter = ${luaStr(entry.starter)}, tier = ${entry.tier}, parent = ${luaStr(entry.parent)}, sourceClass = ${luaStr(entry.sourceClass)} },`);
|
||||||
|
return `self.JobMeta = {\n${rows.join('\n')}\n}`;
|
||||||
|
}
|
||||||
function luaCardsTable(cards) {
|
function luaCardsTable(cards) {
|
||||||
const lines = Object.entries(cards).map(([id, c]) => {
|
const lines = Object.entries(cards).map(([id, c]) => {
|
||||||
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
|
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
|
||||||
if (c.damage != null) fields.push(`damage = ${c.damage}`);
|
if (c.damage != null) fields.push(`damage = ${c.damage}`);
|
||||||
|
if (c.damagePerOtherHandCard != null) fields.push(`damagePerOtherHandCard = ${c.damagePerOtherHandCard}`);
|
||||||
|
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`);
|
||||||
|
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`);
|
||||||
|
if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${c.damagePerSkillInHand}`);
|
||||||
|
if (c.damagePerCardDrawnThisCombat != null) fields.push(`damagePerCardDrawnThisCombat = ${c.damagePerCardDrawnThisCombat}`);
|
||||||
|
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`);
|
||||||
|
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
|
||||||
|
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
|
||||||
|
if (c.firstCardDamageBonus != null) fields.push(`firstCardDamageBonus = ${c.firstCardDamageBonus}`);
|
||||||
|
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`);
|
||||||
|
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
|
||||||
|
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
|
||||||
|
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`);
|
||||||
|
if (c.attackPoison != null) fields.push(`attackPoison = ${c.attackPoison}`);
|
||||||
|
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`);
|
||||||
|
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`);
|
||||||
if (c.block != null) fields.push(`block = ${c.block}`);
|
if (c.block != null) fields.push(`block = ${c.block}`);
|
||||||
|
if (c.blockGainMultiplier != null) fields.push(`blockGainMultiplier = ${c.blockGainMultiplier}`);
|
||||||
|
if (c.blockPerDamageDealtThisTurn != null) fields.push(`blockPerDamageDealtThisTurn = ${c.blockPerDamageDealtThisTurn}`);
|
||||||
if (c.strength != null) fields.push(`strength = ${c.strength}`);
|
if (c.strength != null) fields.push(`strength = ${c.strength}`);
|
||||||
if (c.dex != null) fields.push(`dex = ${c.dex}`);
|
if (c.dex != null) fields.push(`dex = ${c.dex}`);
|
||||||
if (c.thorns != null) fields.push(`thorns = ${c.thorns}`);
|
if (c.thorns != null) fields.push(`thorns = ${c.thorns}`);
|
||||||
|
if (c.cardPlayedBlock != null) fields.push(`cardPlayedBlock = ${c.cardPlayedBlock}`);
|
||||||
if (c.weak != null) fields.push(`weak = ${c.weak}`);
|
if (c.weak != null) fields.push(`weak = ${c.weak}`);
|
||||||
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
|
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
|
||||||
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
|
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
|
||||||
@@ -169,13 +246,58 @@ function luaCardsTable(cards) {
|
|||||||
if (c.pierce === true) fields.push('pierce = true');
|
if (c.pierce === true) fields.push('pierce = true');
|
||||||
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
||||||
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
||||||
|
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
|
||||||
|
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${c.drawSkillBlock}`);
|
||||||
|
if (c.drawDamage != null) fields.push(`drawDamage = ${c.drawDamage}`);
|
||||||
|
if (c.drawPoison != null) fields.push(`drawPoison = ${c.drawPoison}`);
|
||||||
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
||||||
|
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
|
||||||
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
||||||
if (c.discard != null) fields.push(`discard = ${c.discard}`);
|
if (c.discard != null) fields.push(`discard = ${c.discard}`);
|
||||||
if (c.discardAll === true) fields.push('discardAll = true');
|
if (c.discardAll === true) fields.push('discardAll = true');
|
||||||
|
if (c.drawPerDiscarded != null) fields.push(`drawPerDiscarded = ${c.drawPerDiscarded}`);
|
||||||
if (c.addShiv != null) fields.push(`addShiv = ${c.addShiv}`);
|
if (c.addShiv != null) fields.push(`addShiv = ${c.addShiv}`);
|
||||||
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`);
|
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`);
|
||||||
|
if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${c.turnStartDraw}`);
|
||||||
|
if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${c.turnStartDiscard}`);
|
||||||
|
if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = true');
|
||||||
|
if (c.drawDisabledThisTurn === true) fields.push('drawDisabledThisTurn = true');
|
||||||
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
|
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
|
||||||
|
if (c.useAllEnergy === true) fields.push('useAllEnergy = true');
|
||||||
|
if (c.shivDamageBonus != null) fields.push(`shivDamageBonus = ${c.shivDamageBonus}`);
|
||||||
|
if (c.firstShivDamageBonus != null) fields.push(`firstShivDamageBonus = ${c.firstShivDamageBonus}`);
|
||||||
|
if (c.shivRetain === true) fields.push('shivRetain = true');
|
||||||
|
if (c.shivAoe === true) fields.push('shivAoe = true');
|
||||||
|
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${c.attackDamageVsWeakMultiplier}`);
|
||||||
|
if (c.poisonHits != null) fields.push(`poisonHits = ${c.poisonHits}`);
|
||||||
|
if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = true');
|
||||||
|
if (c.poisonIfTargetPoisoned === true) fields.push('poisonIfTargetPoisoned = true');
|
||||||
|
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${c.xDamagePerEnergy}`);
|
||||||
|
if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${c.xWeakPerEnergy}`);
|
||||||
|
if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${c.nextTurnBlock}`);
|
||||||
|
if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`);
|
||||||
|
if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true');
|
||||||
|
if (c.nextTurnAttackMultiplier != null) fields.push(`nextTurnAttackMultiplier = ${c.nextTurnAttackMultiplier}`);
|
||||||
|
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
|
||||||
|
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
|
||||||
|
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
|
||||||
|
if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${c.nextSkillRepeatCount}`);
|
||||||
|
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
|
||||||
|
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`);
|
||||||
|
if (c.skillSlyOnPlay === true) fields.push('skillSlyOnPlay = true');
|
||||||
|
if (c.turnHandSlyCount != null) fields.push(`turnHandSlyCount = ${c.turnHandSlyCount}`);
|
||||||
|
if (c.combatCostReductionOnPlay != null) fields.push(`combatCostReductionOnPlay = ${c.combatCostReductionOnPlay}`);
|
||||||
|
if (c.randomTargetEachHit === true) fields.push('randomTargetEachHit = true');
|
||||||
|
if (c.repeatOnKill === true) fields.push('repeatOnKill = true');
|
||||||
|
if (c.affectsAllEnemies === true) fields.push('affectsAllEnemies = true');
|
||||||
|
if (c.removeEnemyBlock === true) fields.push('removeEnemyBlock = true');
|
||||||
|
if (c.removeEnemyArtifact === true) fields.push('removeEnemyArtifact = true');
|
||||||
|
if (c.enemyStrengthLossThisTurn != null) fields.push(`enemyStrengthLossThisTurn = ${c.enemyStrengthLossThisTurn}`);
|
||||||
|
if (c.extraPoisonTicks != null) fields.push(`extraPoisonTicks = ${c.extraPoisonTicks}`);
|
||||||
|
if (c.poisonApplicationBurstEvery != null) fields.push(`poisonApplicationBurstEvery = ${c.poisonApplicationBurstEvery}`);
|
||||||
|
if (c.poisonApplicationBurstDamage != null) fields.push(`poisonApplicationBurstDamage = ${c.poisonApplicationBurstDamage}`);
|
||||||
|
if (c.innate === true) fields.push('innate = true');
|
||||||
|
if (c.playableWhenDrawPileEmpty === true) fields.push('playableWhenDrawPileEmpty = true');
|
||||||
if (c.sly === true) fields.push('sly = true');
|
if (c.sly === true) fields.push('sly = true');
|
||||||
if (c.retain === true) fields.push('retain = true');
|
if (c.retain === true) fields.push('retain = true');
|
||||||
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) fields.push('exhaust = true');
|
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) fields.push('exhaust = true');
|
||||||
@@ -194,4 +316,11 @@ function luaDeckTable(deck) {
|
|||||||
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
|
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { 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 };
|
export {
|
||||||
|
CARDS, ENEMIES, CLASSES, JOBS, JOB_META, CLASS_GROUPS, CLASS_LINEAGES, SOUL_UNLOCKS,
|
||||||
|
luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable,
|
||||||
|
luaCharsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS,
|
||||||
|
CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable,
|
||||||
|
luaStr, luaJobsTable, luaClassGroupsTable, luaClassLineagesTable, luaJobMetaTable,
|
||||||
|
luaCardsTable, luaDeckTable,
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const GENERATED_UI_SECTIONS = [
|
|||||||
'JobChoiceHud',
|
'JobChoiceHud',
|
||||||
'JobSelectHud',
|
'JobSelectHud',
|
||||||
'MainMenu',
|
'MainMenu',
|
||||||
'CharacterSelectHud',
|
|
||||||
'LobbyHud',
|
'LobbyHud',
|
||||||
'BoardHud',
|
'BoardHud',
|
||||||
'SoulShopHud',
|
'SoulShopHud',
|
||||||
@@ -32,7 +31,6 @@ const UI_APPEND_ORDER = [
|
|||||||
'DeckInspectHud',
|
'DeckInspectHud',
|
||||||
'DeckAllHud',
|
'DeckAllHud',
|
||||||
'MainMenu',
|
'MainMenu',
|
||||||
'CharacterSelectHud',
|
|
||||||
'LobbyHud',
|
'LobbyHud',
|
||||||
'BoardHud',
|
'BoardHud',
|
||||||
'SoulShopHud',
|
'SoulShopHud',
|
||||||
|
|||||||
28
tools/deck/reconnect-ui-paths.mjs
Normal file
28
tools/deck/reconnect-ui-paths.mjs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
// 일회성·멱등 마이그레이션: cb/*.mjs의 UI 경로 리터럴을 메이커 재편 UIGroup으로 재연결.
|
||||||
|
// 이미 이동된 경로는 매치 안 됨(멱등). MainMenu·Button_Attack/Jump·UIJoystick(=DefaultGroup 잔류)은 미변경.
|
||||||
|
// 섹션→UIGroup 매핑은 tools/verify/uimap.mjs 탐색으로 검증된 실제 .ui 분포 기준.
|
||||||
|
const MOVE = {
|
||||||
|
CharacterSelectHud: 'SelectUIGroup', JobChoiceHud: 'SelectUIGroup', JobSelectHud: 'SelectUIGroup',
|
||||||
|
LobbyHud: 'LobbyUIGroup', BoardHud: 'LobbyUIGroup', SoulShopHud: 'LobbyUIGroup',
|
||||||
|
CombatHud: 'RunUIGroup', DeckHud: 'RunUIGroup', CardHand: 'RunUIGroup', MapHud: 'RunUIGroup',
|
||||||
|
RewardHud: 'RunUIGroup', ShopHud: 'RunUIGroup', RestHud: 'RunUIGroup', TreasureHud: 'RunUIGroup',
|
||||||
|
DeckInspectHud: 'DeckUIGroup', DeckAllHud: 'DeckUIGroup',
|
||||||
|
};
|
||||||
|
const CB_DIR = 'tools/deck/cb';
|
||||||
|
let n = 0;
|
||||||
|
for (const f of readdirSync(CB_DIR).filter((x) => x.endsWith('.mjs'))) {
|
||||||
|
const p = join(CB_DIR, f);
|
||||||
|
const before = readFileSync(p, 'utf8');
|
||||||
|
let s = before;
|
||||||
|
// 1) 몬스터 슬롯: 그룹+이름 동시 (CombatHud 일반 remap보다 먼저). 슬롯 5→4는 MAX_MONSTERS(=4)가 이미 반영.
|
||||||
|
s = s.split('/ui/DefaultGroup/CombatHud/MonsterSlot').join('/ui/RunUIGroup/CombatHud/MonsterStatus');
|
||||||
|
// 2) 섹션별 그룹 접두사 remap
|
||||||
|
for (const [section, group] of Object.entries(MOVE)) {
|
||||||
|
s = s.split(`/ui/DefaultGroup/${section}`).join(`/ui/${group}/${section}`);
|
||||||
|
}
|
||||||
|
if (s !== before) { writeFileSync(p, s, 'utf8'); n++; console.log(' remapped', f); }
|
||||||
|
}
|
||||||
|
console.log(`reconnect-ui-paths: ${n} files updated`);
|
||||||
42
tools/verify/cardkinds.mjs
Normal file
42
tools/verify/cardkinds.mjs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// 카드 kind ↔ 효과 정합성 정적 검사 (협업자/codex가 카드 추가 후 실행).
|
||||||
|
// 배경(2026-06-30): kind가 효과와 안 맞으면 카드가 사용불가/死카드가 된다.
|
||||||
|
// - ResolveCardDrop 라우팅: Attack=몬스터 위 드롭(FindMonsterAtTouch>0 필요) / Skill·Power=위로 스윕 / Status=unplayable.
|
||||||
|
// → block·유틸만 있고 데미지 없는 카드를 Attack으로 두면 위로 스윕으로 못 쓴다(아이언 바디 사고).
|
||||||
|
// - PlayCard의 Power 분기는 PlayerPowers 등록만 하고 damage/aoe를 무시한다.
|
||||||
|
// → Power인데 powerEffect도 power필드도 없으면 재생 시 아무 효과 없는 死카드(분노 사고).
|
||||||
|
// 사용: node tools/verify/cardkinds.mjs (이상 0 → exit 0, 있으면 목록 + exit 1)
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const cards = JSON.parse(readFileSync('data/cards.json', 'utf8')).cards;
|
||||||
|
|
||||||
|
// Power 카드를 실제로 기능하게 하는 필드(powerEffect 지속효과 + 온플레이/지속 power 필드).
|
||||||
|
// damage/aoe/block 같은 Attack/Skill 전용 필드는 Power 분기서 무시되므로 제외.
|
||||||
|
const POWER_FIELDS = [
|
||||||
|
'powerEffect', 'strength', 'dex', 'thorns', 'intangible',
|
||||||
|
'turnStartShiv', 'turnStartDraw', 'turnStartDiscard',
|
||||||
|
'shivDamageBonus', 'firstShivDamageBonus', 'shivRetain', 'shivAoe',
|
||||||
|
'attackPoison', 'drawDamage', 'drawPoison', 'attackDamageVsWeakMultiplier',
|
||||||
|
'cardPlayedBlock', 'cardPlayedDamage', 'cardPlayedRandomDamage',
|
||||||
|
'extraPoisonTicks', 'poisonApplicationBurstEvery', 'poisonApplicationBurstDamage',
|
||||||
|
'skillSlyOnPlay', 'endTurnDexLoss',
|
||||||
|
];
|
||||||
|
const VALID_KINDS = ['Attack', 'Skill', 'Power', 'Status'];
|
||||||
|
|
||||||
|
const issues = [];
|
||||||
|
for (const [id, c] of Object.entries(cards)) {
|
||||||
|
if (!VALID_KINDS.includes(c.kind)) {
|
||||||
|
issues.push(`${id}(${c.name}): 미지원 kind="${c.kind}"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c.kind === 'Attack' && c.damage == null && c.xDamagePerEnergy == null) {
|
||||||
|
issues.push(`${id}(${c.name}): kind=Attack인데 damage 없음 → 몬스터 드롭 라우팅 불가(방어/유틸이면 kind=Skill)`);
|
||||||
|
}
|
||||||
|
if (c.kind === 'Power' && !POWER_FIELDS.some((f) => c[f] != null)) {
|
||||||
|
issues.push(`${id}(${c.name}): kind=Power인데 power효과 없음(死카드) → damage/aoe는 Power 분기서 무시, kind 재검토`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`카드 ${Object.keys(cards).length}장 kind↔효과 정합성: 이상 ${issues.length}`);
|
||||||
|
for (const i of issues) console.log(' ⚠️ ' + i);
|
||||||
|
console.log(issues.length ? 'RESULT: 정합성 위반 (위 카드 kind 수정 필요)' : 'RESULT: 모든 카드 kind↔효과 일치 ✓');
|
||||||
|
process.exit(issues.length ? 1 : 0);
|
||||||
69
tools/verify/cbgap.mjs
Normal file
69
tools/verify/cbgap.mjs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { readFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
// cb 컨트롤러가 참조하는 /ui/DefaultGroup/... 경로를, 사용자가 메이커에서 재편한
|
||||||
|
// 새 UIGroup(.ui) 구조에 대조해 "그대로 옮겨지면 해결(resolved)" vs "이름/구조가 바뀌어
|
||||||
|
// 못 찾음(GAP)"을 분류. GAP = 사용자에게 구→신 매핑을 받아야 할 항목.
|
||||||
|
// 출력은 경로 이름 + EXISTS/GAP boolean 뿐(엔티티 값 본문 미출력 → RULES §2 준수).
|
||||||
|
|
||||||
|
// 섹션(=DefaultGroup 다음 첫 세그먼트) → 이동된 UIGroup (uimap.mjs 탐색 결과 기반)
|
||||||
|
const SECTION_TO_GROUP = {
|
||||||
|
MainMenu: 'DefaultGroup', Button_Attack: 'DefaultGroup', Button_Jump: 'DefaultGroup', UIJoystick: 'DefaultGroup',
|
||||||
|
CharacterSelectHud: 'SelectUIGroup', JobChoiceHud: 'SelectUIGroup', JobSelectHud: 'SelectUIGroup',
|
||||||
|
LobbyHud: 'LobbyUIGroup', BoardHud: 'LobbyUIGroup', SoulShopHud: 'LobbyUIGroup',
|
||||||
|
CombatHud: 'RunUIGroup', DeckHud: 'RunUIGroup', CardHand: 'RunUIGroup', MapHud: 'RunUIGroup',
|
||||||
|
RewardHud: 'RunUIGroup', ShopHud: 'RunUIGroup', RestHud: 'RunUIGroup', TreasureHud: 'RunUIGroup',
|
||||||
|
DeckInspectHud: 'DeckUIGroup', DeckAllHud: 'DeckUIGroup',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 새 .ui 로드
|
||||||
|
const UI_DIR = 'ui';
|
||||||
|
const ui = {};
|
||||||
|
for (const f of readdirSync(UI_DIR).filter((x) => x.endsWith('.ui'))) {
|
||||||
|
ui[f.replace('.ui', '')] = readFileSync(join(UI_DIR, f), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// cb 소스에서 /ui/DefaultGroup/... 경로 리터럴 추출 (템플릿 ${...} 포함)
|
||||||
|
const CB_DIR = 'tools/deck/cb';
|
||||||
|
const re = /\/ui\/DefaultGroup\/[^"'`\s),]+/g;
|
||||||
|
const paths = new Set();
|
||||||
|
for (const f of readdirSync(CB_DIR).filter((x) => x.endsWith('.mjs'))) {
|
||||||
|
const src = readFileSync(join(CB_DIR, f), 'utf8');
|
||||||
|
for (const m of src.match(re) || []) paths.add(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 동적 세그먼트(${...}) 앞 정적 prefix만 취해 존재검사 (e.g. .../Card${i} → .../Card)
|
||||||
|
function staticPrefix(p) {
|
||||||
|
const i = p.indexOf('${');
|
||||||
|
if (i === -1) return { p, dyn: false };
|
||||||
|
// ${...} 직전까지 (마지막 세그먼트의 정적 앞부분 포함)
|
||||||
|
return { p: p.slice(0, i), dyn: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const bySection = {};
|
||||||
|
for (const p of [...paths].sort()) {
|
||||||
|
const rest = p.slice('/ui/DefaultGroup/'.length); // Section/child/...
|
||||||
|
const section = rest.split('/')[0].split('${')[0];
|
||||||
|
const group = SECTION_TO_GROUP[section] || '??';
|
||||||
|
const newPath = group === '??' ? p : p.replace('/ui/DefaultGroup/', `/ui/${group}/`);
|
||||||
|
const { p: probe } = staticPrefix(newPath);
|
||||||
|
const blob = ui[group] || '';
|
||||||
|
const exists = group !== '??' && blob.includes(probe);
|
||||||
|
(bySection[section] ||= []).push({ old: p, neu: newPath, exists, group });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보고
|
||||||
|
let totResolved = 0, totGap = 0;
|
||||||
|
for (const section of Object.keys(bySection).sort()) {
|
||||||
|
const rows = bySection[section];
|
||||||
|
const group = rows[0].group;
|
||||||
|
const gaps = rows.filter((r) => !r.exists);
|
||||||
|
const ok = rows.length - gaps.length;
|
||||||
|
totResolved += ok; totGap += gaps.length;
|
||||||
|
console.log(`\n[${section}] → ${group} 해결 ${ok} / GAP ${gaps.length} (총 ${rows.length})`);
|
||||||
|
for (const g of gaps) {
|
||||||
|
// GAP만 상세 출력 (사용자가 신규 이름 채워야 할 대상)
|
||||||
|
console.log(` GAP ${g.old.replace('/ui/DefaultGroup/', '')} →(없음) ${g.neu.replace(`/ui/${group}/`, `${group}: `)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`\n=== 합계: 자동해결 ${totResolved} / GAP ${totGap} (distinct 경로 ${paths.size}) ===`);
|
||||||
34
tools/verify/cbprops.mjs
Normal file
34
tools/verify/cbprops.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// cb/*.mjs의 `self.<Field> = ` 대입 ↔ gen-slaydeck.mjs 선언 prop 대조.
|
||||||
|
// 미선언 prop에 대입하면 MSW 런타임 "cannot set X, no such field" → 그 후보를 정적 검출.
|
||||||
|
// 사용: node tools/verify/cbprops.mjs
|
||||||
|
import { readFileSync, readdirSync } from 'node:fs';
|
||||||
|
|
||||||
|
const orch = readFileSync('tools/deck/gen-slaydeck.mjs', 'utf8');
|
||||||
|
const declared = new Set();
|
||||||
|
for (const m of orch.matchAll(/prop\(\s*'[^']*'\s*,\s*'([^']+)'/g)) declared.add(m[1]);
|
||||||
|
|
||||||
|
// MSW 빌트인/설정으로 대입 가능한 self 필드(프롭 아님) — 오탐 제외 화이트리스트.
|
||||||
|
const BUILTIN = new Set(['Entity']);
|
||||||
|
|
||||||
|
const dir = 'tools/deck/cb';
|
||||||
|
const files = readdirSync(dir).filter((f) => f.endsWith('.mjs'));
|
||||||
|
const assigns = new Map(); // name -> Set(files)
|
||||||
|
for (const f of files) {
|
||||||
|
const src = readFileSync(`${dir}/${f}`, 'utf8');
|
||||||
|
// self.Name = (단, == / ~= / .Y= / [..]= 는 제외)
|
||||||
|
for (const m of src.matchAll(/self\.([A-Za-z_]\w*)\s*=(?!=)/g)) {
|
||||||
|
const name = m[1];
|
||||||
|
if (!assigns.has(name)) assigns.set(name, new Set());
|
||||||
|
assigns.get(name).add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const missing = [...assigns.keys()]
|
||||||
|
.filter((n) => !declared.has(n) && !BUILTIN.has(n))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
console.log(`선언 prop: ${declared.size} | 대입된 self.X distinct: ${assigns.size}`);
|
||||||
|
console.log(`미선언 대입 (no such field 후보): ${missing.length}`);
|
||||||
|
for (const n of missing) console.log(` - ${n} [${[...assigns.get(n)].join(', ')}]`);
|
||||||
|
console.log(missing.length ? 'RESULT: MISSING PROPS ABOVE' : 'RESULT: 모든 self 대입이 선언됨 ✓');
|
||||||
|
process.exit(missing.length ? 1 : 0);
|
||||||
40
tools/verify/cbset.mjs
Normal file
40
tools/verify/cbset.mjs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// 순서 무관 codeblock 메서드 집합 비교. 본문 미출력 — 이름·차이 카운트만.
|
||||||
|
// 메서드 이동 리팩터의 무손실 검증용: 워킹트리 codeblock vs ref(기본 HEAD).
|
||||||
|
// 사용: node tools/verify/cbset.mjs [ref]
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
|
||||||
|
const PATH = 'RootDesk/MyDesk/SlayDeckController.codeblock';
|
||||||
|
const ref = process.argv[2] || 'HEAD';
|
||||||
|
|
||||||
|
function methodsOf(jsonText) {
|
||||||
|
const obj = JSON.parse(jsonText);
|
||||||
|
const arr = obj.ContentProto.Json.Methods;
|
||||||
|
const map = new Map();
|
||||||
|
for (const m of arr) {
|
||||||
|
map.set(m.Name, { code: m.Code, exec: m.ExecSpace, params: JSON.stringify(m.Parameters || []) });
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
const work = methodsOf(readFileSync(PATH, 'utf8'));
|
||||||
|
const base = methodsOf(execSync(`git show ${ref}:${PATH}`, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }));
|
||||||
|
|
||||||
|
const onlyWork = [...work.keys()].filter((k) => !base.has(k));
|
||||||
|
const onlyBase = [...base.keys()].filter((k) => !work.has(k));
|
||||||
|
const changed = [];
|
||||||
|
for (const k of work.keys()) {
|
||||||
|
if (!base.has(k)) continue;
|
||||||
|
const a = work.get(k);
|
||||||
|
const b = base.get(k);
|
||||||
|
if (a.code !== b.code || a.exec !== b.exec || a.params !== b.params) changed.push(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`ref=${ref} work=${work.size} base=${base.size}`);
|
||||||
|
console.log(`only-in-work (${onlyWork.length}): ${onlyWork.join(', ') || '-'}`);
|
||||||
|
console.log(`only-in-base (${onlyBase.length}): ${onlyBase.join(', ') || '-'}`);
|
||||||
|
console.log(`body/exec/params changed (${changed.length}): ${changed.join(', ') || '-'}`);
|
||||||
|
|
||||||
|
const ok = onlyWork.length === 0 && onlyBase.length === 0 && changed.length === 0;
|
||||||
|
console.log(ok ? 'RESULT: IDENTICAL SET (무손실)' : 'RESULT: DIFFERENCES ABOVE');
|
||||||
|
process.exit(ok ? 0 : 1);
|
||||||
82
tools/verify/rogue-card-names.mjs
Normal file
82
tools/verify/rogue-card-names.mjs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const cards = JSON.parse(readFileSync('data/cards.json', 'utf8')).cards;
|
||||||
|
const rogueClasses = new Set(['rogue', 'thief', 'thiefmaster', 'assassin', 'hermit']);
|
||||||
|
|
||||||
|
const mapleSkillCards = {
|
||||||
|
DoubleStab: '더블 스탭',
|
||||||
|
LuckySeven: '럭키 세븐',
|
||||||
|
Haste: '헤이스트',
|
||||||
|
DarkSight: '다크 사이트',
|
||||||
|
FlashJump: '플래시 점프',
|
||||||
|
NimbleBody: '님블 바디',
|
||||||
|
SavageBlow: '새비지 블로우',
|
||||||
|
CriticalEdge: '크리티컬 엣지',
|
||||||
|
Steal: '스틸',
|
||||||
|
DaggerAcceleration: '대거 액셀레이션',
|
||||||
|
Karma: '카르마',
|
||||||
|
DaggerMastery: '대거 마스터리',
|
||||||
|
PhysicalTraining: '피지컬 트레이닝',
|
||||||
|
ShieldMastery: '실드 마스터리',
|
||||||
|
ThiefAgility: '시프 어질리티',
|
||||||
|
EdgeCarnival: '엣지 카니발',
|
||||||
|
MuspelHeim: '무스펠 하임',
|
||||||
|
MesoExplosion: '메소 익스플로젼',
|
||||||
|
DarkFlare: '다크 플레어',
|
||||||
|
PickPocket: '픽 파킷',
|
||||||
|
ShadowPartner: '쉐도우 파트너',
|
||||||
|
AdvancedDarkSight: '어드밴스드 다크 사이트',
|
||||||
|
IntoDarkness: '인투 다크니스',
|
||||||
|
Venom: '베놈',
|
||||||
|
Grid: '그리드',
|
||||||
|
RadicalDarkness: '래디컬 다크니스',
|
||||||
|
ShurikenBurst: '슈리켄 버스트',
|
||||||
|
WindTalisman: '윈드 탈리스만',
|
||||||
|
MarkOfAssassin: '마크 오브 어쌔신',
|
||||||
|
ShadowRush: '쉐도우 러쉬',
|
||||||
|
ShadowLeap: '쉐도우 리프',
|
||||||
|
ShadowBlink: '쉐도우 블링크',
|
||||||
|
JavelinMastery: '자벨린 마스터리',
|
||||||
|
JavelinAcceleration: '자벨린 액셀레이션',
|
||||||
|
CriticalThrow: '크리티컬 스로우',
|
||||||
|
AssassinPhysicalTraining: '피지컬 트레이닝',
|
||||||
|
TripleThrow: '트리플 스로우',
|
||||||
|
ShurikenChallenge: '슈리켄 챌린지',
|
||||||
|
HermitDarkFlare: '다크 플레어',
|
||||||
|
HermitShadowPartner: '쉐도우 파트너',
|
||||||
|
SpiritJavelin: '스피릿 자벨린',
|
||||||
|
HermitRadicalDarkness: '래디컬 다크니스',
|
||||||
|
HermitVenom: '베놈',
|
||||||
|
SkilledJavelin: '숙련된 표창술',
|
||||||
|
HermitAdrenaline: '아드레날린',
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
for (const [id, expectedName] of Object.entries(mapleSkillCards)) {
|
||||||
|
if (!cards[id]) errors.push(`원본 스킬 카드 없음: ${id}`);
|
||||||
|
else if (cards[id].name !== expectedName) errors.push(`원본 스킬명 변경: ${id} (${cards[id].name} != ${expectedName})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customCards = Object.entries(cards).filter(([id, card]) => rogueClasses.has(card.class) && !mapleSkillCards[id]);
|
||||||
|
if (customCards.length !== 78) errors.push(`도적 비스킬 카드 수 불일치: ${customCards.length} != 78`);
|
||||||
|
|
||||||
|
const names = new Map();
|
||||||
|
for (const [id, card] of Object.entries(cards)) {
|
||||||
|
if (!names.has(card.name)) names.set(card.name, []);
|
||||||
|
names.get(card.name).push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonRogueNames = new Set(Object.values(cards).filter((card) => !rogueClasses.has(card.class) && card.class !== 'shiv').map((card) => card.name));
|
||||||
|
for (const [id, card] of customCards) {
|
||||||
|
const sameNameIds = names.get(card.name) || [];
|
||||||
|
if (sameNameIds.length > 1) errors.push(`비스킬 카드명 중복: ${id} ${card.name} (${sameNameIds.join(', ')})`);
|
||||||
|
if (nonRogueNames.has(card.name)) errors.push(`다른 직업 카드명 충돌: ${id} ${card.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`메이플 원본 스킬명 고정 ${Object.keys(mapleSkillCards).length}장 | 도적 비스킬 고유 이름 ${customCards.length}장`);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
for (const error of errors) console.error(`ERROR: ${error}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
console.log('RESULT: 도적 카드 이름 규칙 이상 0');
|
||||||
|
}
|
||||||
35
tools/verify/uimap.mjs
Normal file
35
tools/verify/uimap.mjs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { readFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
// UIGroup(.ui) 매핑 헬퍼 (RULES §2: 내용 출력 금지·카운트만).
|
||||||
|
// 어떤 섹션/엔티티 이름이 어느 .ui 파일에 들어있는지 카운트 매트릭스로 보고.
|
||||||
|
// 산출물 경로를 명령줄에 노출하지 않아(디렉토리 스캔) deny 회피.
|
||||||
|
//
|
||||||
|
// 사용: node tools/verify/uimap.mjs <pattern> [<pattern> ...]
|
||||||
|
// 각 pattern은 정규식. 출력은 "pattern | file=count ..." 형식(본문 미출력).
|
||||||
|
const UI_DIR = 'ui';
|
||||||
|
const files = readdirSync(UI_DIR).filter((f) => f.endsWith('.ui'));
|
||||||
|
const cache = {};
|
||||||
|
for (const f of files) {
|
||||||
|
cache[f] = readFileSync(join(UI_DIR, f), 'utf8');
|
||||||
|
}
|
||||||
|
// 파일별 크기/JSON 유효성 헤더
|
||||||
|
console.log('=== ui/*.ui ===');
|
||||||
|
for (const f of files) {
|
||||||
|
let ok = false;
|
||||||
|
try { JSON.parse(cache[f]); ok = true; } catch { ok = false; }
|
||||||
|
console.log(` ${f} bytes=${cache[f].length} jsonValid=${ok}`);
|
||||||
|
}
|
||||||
|
const pats = process.argv.slice(2);
|
||||||
|
if (pats.length === 0) process.exit(0);
|
||||||
|
console.log('=== matches (file=count, 0 생략) ===');
|
||||||
|
for (const pat of pats) {
|
||||||
|
const re = new RegExp(pat, 'g');
|
||||||
|
const hits = [];
|
||||||
|
for (const f of files) {
|
||||||
|
const m = cache[f].match(re);
|
||||||
|
const n = m ? m.length : 0;
|
||||||
|
if (n > 0) hits.push(`${f}=${n}`);
|
||||||
|
}
|
||||||
|
console.log(` /${pat}/ ${hits.length ? hits.join(' ') : '(none)'}`);
|
||||||
|
}
|
||||||
157957
ui/DeckUIGroup.ui
Normal file
157957
ui/DeckUIGroup.ui
Normal file
File diff suppressed because it is too large
Load Diff
243873
ui/DefaultGroup.ui
243873
ui/DefaultGroup.ui
File diff suppressed because it is too large
Load Diff
5414
ui/LobbyUIGroup.ui
Normal file
5414
ui/LobbyUIGroup.ui
Normal file
File diff suppressed because it is too large
Load Diff
100
ui/PopupGroup.ui
100
ui/PopupGroup.ui
@@ -97,7 +97,7 @@
|
|||||||
{
|
{
|
||||||
"@type": "MOD.Core.UIGroupComponent",
|
"@type": "MOD.Core.UIGroupComponent",
|
||||||
"DefaultShow": false,
|
"DefaultShow": false,
|
||||||
"GroupOrder": 1,
|
"GroupOrder": 2,
|
||||||
"GroupType": 1,
|
"GroupType": 1,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
},
|
},
|
||||||
@@ -585,7 +585,7 @@
|
|||||||
{
|
{
|
||||||
"id": "94a274e4-4111-40f1-924d-c95a3a1f14d5",
|
"id": "94a274e4-4111-40f1-924d-c95a3a1f14d5",
|
||||||
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnOK",
|
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnOK",
|
||||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent",
|
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||||
"jsonString": {
|
"jsonString": {
|
||||||
"name": "PopupBtnOK",
|
"name": "PopupBtnOK",
|
||||||
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnOK",
|
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnOK",
|
||||||
@@ -719,53 +719,6 @@
|
|||||||
"Type": 1,
|
"Type": 1,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"@type": "MOD.Core.ButtonComponent",
|
|
||||||
"Colors": {
|
|
||||||
"NormalColor": {
|
|
||||||
"r": 1.0,
|
|
||||||
"g": 1.0,
|
|
||||||
"b": 1.0,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"HighlightedColor": {
|
|
||||||
"r": 0.9607843,
|
|
||||||
"g": 0.9607843,
|
|
||||||
"b": 0.9607843,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"PressedColor": {
|
|
||||||
"r": 0.784313738,
|
|
||||||
"g": 0.784313738,
|
|
||||||
"b": 0.784313738,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"SelectedColor": {
|
|
||||||
"r": 0.9607843,
|
|
||||||
"g": 0.9607843,
|
|
||||||
"b": 0.9607843,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"DisabledColor": {
|
|
||||||
"r": 0.784313738,
|
|
||||||
"g": 0.784313738,
|
|
||||||
"b": 0.784313738,
|
|
||||||
"a": 0.5019608
|
|
||||||
},
|
|
||||||
"ColorMultiplier": 1.0,
|
|
||||||
"FadeDuration": 0.1
|
|
||||||
},
|
|
||||||
"ImageRUIDs": {
|
|
||||||
"HighlightedSprite": null,
|
|
||||||
"PressedSprite": null,
|
|
||||||
"SelectedSprite": null,
|
|
||||||
"DisabledSprite": null
|
|
||||||
},
|
|
||||||
"KeyCode": 0,
|
|
||||||
"OverrideSorting": false,
|
|
||||||
"Transition": 1,
|
|
||||||
"Enable": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"@type": "MOD.Core.TextComponent",
|
"@type": "MOD.Core.TextComponent",
|
||||||
"Alignment": 4,
|
"Alignment": 4,
|
||||||
@@ -820,7 +773,7 @@
|
|||||||
{
|
{
|
||||||
"id": "0f5de49b-2adc-409a-816d-15aa43df8e0d",
|
"id": "0f5de49b-2adc-409a-816d-15aa43df8e0d",
|
||||||
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnCancel",
|
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnCancel",
|
||||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent",
|
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||||
"jsonString": {
|
"jsonString": {
|
||||||
"name": "PopupBtnCancel",
|
"name": "PopupBtnCancel",
|
||||||
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnCancel",
|
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnCancel",
|
||||||
@@ -954,53 +907,6 @@
|
|||||||
"Type": 1,
|
"Type": 1,
|
||||||
"Enable": true
|
"Enable": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"@type": "MOD.Core.ButtonComponent",
|
|
||||||
"Colors": {
|
|
||||||
"NormalColor": {
|
|
||||||
"r": 1.0,
|
|
||||||
"g": 1.0,
|
|
||||||
"b": 1.0,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"HighlightedColor": {
|
|
||||||
"r": 0.9607843,
|
|
||||||
"g": 0.9607843,
|
|
||||||
"b": 0.9607843,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"PressedColor": {
|
|
||||||
"r": 0.784313738,
|
|
||||||
"g": 0.784313738,
|
|
||||||
"b": 0.784313738,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"SelectedColor": {
|
|
||||||
"r": 0.9607843,
|
|
||||||
"g": 0.9607843,
|
|
||||||
"b": 0.9607843,
|
|
||||||
"a": 1.0
|
|
||||||
},
|
|
||||||
"DisabledColor": {
|
|
||||||
"r": 0.784313738,
|
|
||||||
"g": 0.784313738,
|
|
||||||
"b": 0.784313738,
|
|
||||||
"a": 0.5019608
|
|
||||||
},
|
|
||||||
"ColorMultiplier": 1.0,
|
|
||||||
"FadeDuration": 0.1
|
|
||||||
},
|
|
||||||
"ImageRUIDs": {
|
|
||||||
"HighlightedSprite": null,
|
|
||||||
"PressedSprite": null,
|
|
||||||
"SelectedSprite": null,
|
|
||||||
"DisabledSprite": null
|
|
||||||
},
|
|
||||||
"KeyCode": 0,
|
|
||||||
"OverrideSorting": false,
|
|
||||||
"Transition": 1,
|
|
||||||
"Enable": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"@type": "MOD.Core.TextComponent",
|
"@type": "MOD.Core.TextComponent",
|
||||||
"Alignment": 4,
|
"Alignment": 4,
|
||||||
|
|||||||
69868
ui/RunUIGroup.ui
Normal file
69868
ui/RunUIGroup.ui
Normal file
File diff suppressed because it is too large
Load Diff
6311
ui/SelectUIGroup.ui
Normal file
6311
ui/SelectUIGroup.ui
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user