Compare commits
153 Commits
1a5953050f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 30c73c9899 | |||
| ffcec649fa | |||
| 3b678e35d1 | |||
| 47e954266c | |||
| 8b4ece766a | |||
| ee68fb5bb0 | |||
| a7f949fad8 | |||
| 9fbf8e8574 | |||
| d61628d359 | |||
| 18da7a7983 | |||
| 695f048c2d | |||
| 6e82d0f128 | |||
| 1390b9ec50 | |||
| cdfc79cd57 | |||
| bfb9ee5bef | |||
| 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 | |||
| fbf4d8d02d | |||
| a141939675 | |||
| 42eb33b579 | |||
| 9f7713267c | |||
| bfa86f0f28 | |||
| c5bb8c18a9 | |||
| 2f9c325c96 | |||
| 420cce561c | |||
| d265c8f918 | |||
| 255781d969 | |||
| b904b29503 | |||
| 0435a76fc1 | |||
| d82e98f832 | |||
| eafd6747a7 | |||
| bc266b1885 | |||
| e6a397cc55 | |||
| fcc103227c | |||
| 44878bab9e | |||
| 064d81d424 | |||
| 62187db5dd | |||
| a902cb8bce |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,6 +7,8 @@
|
||||
# Claude Code 로컬 설정 — 단, 팀 공유 하네스 설정(settings.json)은 커밋 (RULES.md 참조)
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
# 개인 스킬(superpowers) 브레인스토밍/계획 산출물 — 로컬 전용, 협업 공유 X (프로젝트 설계 문서 docs/*.md 는 추적 유지)
|
||||
docs/superpowers/
|
||||
|
||||
# === OS / 에디터 잡파일 ===
|
||||
Thumbs.db
|
||||
@@ -23,3 +25,5 @@ AGENTS.md
|
||||
Environment/
|
||||
McpScreenshots/
|
||||
*.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",
|
||||
"Enable": true,
|
||||
"Energy": 0.0,
|
||||
"MaxEnergy": 3.0,
|
||||
"Turn": 0.0,
|
||||
"TweenEventId": 0.0
|
||||
"Energy": 0,
|
||||
"MaxEnergy": 3,
|
||||
"Turn": 0,
|
||||
"TweenEventId": 0
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
81
README.md
81
README.md
@@ -44,11 +44,14 @@ git pull
|
||||
```
|
||||
slaymaple/
|
||||
├── data/ # 게임 데이터 단일 소스 (생성기가 읽어 주입). 맵은 정적 데이터 없음(절차 생성)
|
||||
│ ├── cards.json # 카드 122장(클래스·2차전직별 + 저주) + 클래스별 시작 덱
|
||||
│ ├── enemies.json # 적 12종(일반/정예/보스, 디버프 인텐트 포함)
|
||||
│ ├── cards.json # 카드 166장(1~3차 전직 계열별 + 저주) + 클래스별 시작 덱
|
||||
│ ├── enemies.json # 적 18종(일반/정예/보스, 디버프 인텐트 + 외형 appearance)
|
||||
│ ├── encounters.json # 맵별 몬스터 로스터(map01~05 × combat/elite/boss)
|
||||
│ ├── potions.json # 물약 6종 + 드랍률·슬롯·상점가
|
||||
│ ├── relics.json # 유물 19종(StS 효과 × 메이플 장비) + 시작 유물 + 풀
|
||||
│ ├── cardframes.json # 커스텀 카드 프레임 3종(전사/마법사/도적 × normal/unique/legend) + 보상 등급 가중치
|
||||
│ ├── characters.json # 클래스별 초상화 RUID
|
||||
│ ├── cards.xlsx # cards.json 왕복 편집용 엑셀(excel_to_cards.bat / cards_to_excel.bat)
|
||||
│ └── camera.json # 맵별 카메라 설정값(줌·오프셋·고정 영역)
|
||||
├── Global/ # 월드 전역 설정 · 공용 모델 · 게임로직
|
||||
│ ├── common.gamelogic # SlayDeckController 부착 지점 (산출물)
|
||||
@@ -66,20 +69,21 @@ slaymaple/
|
||||
│ ├── MapCamera.codeblock # 맵별 카메라 적용
|
||||
│ ├── PlayerLock.codeblock # 전투맵 플레이어 입력·이동 잠금
|
||||
│ ├── LobbyNpc.codeblock # 로비 NPC 상호작용(근접·클릭)
|
||||
│ └── LobbyMobility.codeblock # 로비 이동·공격 해제 + 카메라 추종
|
||||
│ ├── LobbyMobility.codeblock # 로비 이동·공격 해제 + 카메라 추종
|
||||
│ └── Models/Monsters/ # 적 종별 모델 <enemyId>.model (산출물 — 외형·EnemyId 베이크)
|
||||
├── map/ # 맵 6종 (산출물)
|
||||
│ ├── lobby.map # 로비 허브 맵 (마을 배경, NPC 4종, 전투 없음)
|
||||
│ └── map01.map ~ map05.map # 5막 전투/맵 노드 (공식 배경 + STS풍 우측 배치)
|
||||
├── tools/ # 결정적 생성기·도구 (주체별 폴더, 단일 소스)
|
||||
│ ├── deck/ # gen-slaydeck.mjs(★게임 전체 생성: 카드/덱·전투·맵노드·상점·유물·로비·메뉴 UI + SlayDeckController + common) · gen-cardhand.mjs
|
||||
│ ├── map/ # gen-maps.mjs(맵 배경/타일) · gen-lobby-map.mjs(로비 맵+NPC) · gen-map-encounters.mjs(노드별 몬스터 그룹) · rogue-map.mjs(절차 생성 JS 미러)+test
|
||||
│ ├── 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(encounters.json 로스터 기반 종별 모델 인스턴스 배치) · rogue-map.mjs(절차 생성 JS 미러)+test
|
||||
│ ├── camera/ # gen-camera.mjs(맵별 고정 카메라 codeblock)
|
||||
│ ├── player/ # gen-player-lock.mjs(전투맵 입력 잠금) · freeze-turn-player.mjs(모델 이동 정지) · gen-lobby-npc.mjs(LobbyNpc·LobbyMobility codeblock)
|
||||
│ ├── monster/ # gen-combat-monster.mjs(EnemyId 마커) · freeze-turn-monsters.mjs(필드 AI 정지)
|
||||
│ ├── monster/ # gen-monster-models.mjs(적 종별 .model) · gen-combat-monster.mjs(자기등록 codeblock) · freeze-turn-monsters.mjs(레거시 AI 정지) · lib/monster-model.mjs(공용 빌더)+test
|
||||
│ ├── 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 참조)
|
||||
├── ui/ # UI 그룹 (DefaultGroup 8.3MB 산출물 / PopupGroup / ToastGroup)
|
||||
├── ui/ # UIGroup 7종 — 메이커 저작(Default/Select/Lobby/Run/Deck/Popup/Toast)
|
||||
├── docs/
|
||||
│ ├── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서
|
||||
│ ├── ui-generation-structure.md # UI 생성 구조 문서
|
||||
@@ -89,14 +93,22 @@ slaymaple/
|
||||
└── 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`). 각자 로컬에서 직접 구성하세요.
|
||||
|
||||
---
|
||||
|
||||
## 직업 컨셉
|
||||
|
||||
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막)
|
||||
@@ -104,28 +116,30 @@ 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종 월드 엔티티** — 모험가(런 시작)·사서(카드 도감)·상인(영혼 상점)·안내원(게시판). 근접 시 머리 위 마크 + `↑`키 **또는 직접 클릭**으로 상호작용. **이동·공격 모션은 로비 맵에서만** 풀림(전투맵은 잠금), 카메라는 로비에서 **플레이어 추종**(전투맵은 고정) |
|
||||
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택, 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**2차 전직**] — 각 클래스 3종(전사→파이터/페이지/스피어맨, 법사→위자드불독/위자드썬콜/클레릭, 도적→Shiv/Poison/Trickster). 전용 카드는 해당 클래스 풀만 획득 |
|
||||
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬=위로 스윕). 카드 **122장** — kind **Attack/Skill/Power/Status**. 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
|
||||
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택(**초상화·직업 설명·선택 테두리 강조** 캐릭터 선택 UI), 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**전직**] — 전사→파이터/페이지/스피어맨, 법사→위자드(불·독)/위자드(썬·콜)/클레릭 (2차 3종씩), **도적→어쌔신·시프(2차) → 헤르밋·시프 마스터(3차)**. 전직 시 대표 카드 지급, 전용 카드는 해당 계열 풀만 획득 |
|
||||
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬/파워=위로 스윕). 카드 **166장** — kind **Attack(59)/Skill(74)/Power(31)/Status(2)**. kind↔효과 정합성 정적 검증(`cardkinds.mjs`). 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
|
||||
| **도적 카드 공용 효과** | 카드 효과를 **카드명 하드코딩 대신 `data/cards.json` 공용 필드**로 표현(재사용). **불가침**·**x-cost**(에너지 비례 피해/약화)·드로우 수 비례 데미지·**다음 스킬 반복**·**처치 보상/반복**·카드 설명 **키워드 하이라이트**·드로우 연동(`drawSkillBlock`·`drawPoison`)·독 버스트·랜덤 타깃 등. **Lua + JS 미러 양쪽 구현**. 필드 사전 [`docs/card-effect-fields.md`](docs/card-effect-fields.md) |
|
||||
| **버프/디버프** | StS 표준 — **힘**(+N 영구)·**약화**(주는 피해 −25%)·**취약**(받는 피해 +50%)·**독**(매 행동 틱). 양방향(적 디버프 인텐트 포함), 인텐트는 최종 예상치 표시 |
|
||||
| **전투 연출** | 공격 이펙트·**몬스터 데미지 팝업(자릿수 스킨)**·드래그 타깃 마커·적 개별 차례·**공격/피격/독뎀 모션**(아바타 상태 전이·몬스터 hit 클립·런지/넉백) |
|
||||
| **절차 생성 맵** | 막 시작마다 **경로 생성**(런마다 다름, **가로 진행**). 층 규칙: 1~2층 전투만 → 3층~ 상점/휴식 → 4층~ 엘리트/**유물 방** → 보스 수렴. 점선 경로·상태 4단·층 카운터. 노드 타입별 **몬스터 랜덤 구성**(일반 1~3 / 엘리트 / 보스) + intent 랜덤 행동 |
|
||||
| **몬스터 종별 모델** | 적 종별 전용 `.model`(프리팹) — 외형(stand/hit/die)·EnemyId 베이크, 태생 AI-free. 맵 배치는 **`data/encounters.json` 맵별 로스터**대로 해당 모델 인스턴스 생성(외형=정체성 고정). 능력치·행동은 `enemies.json`, 외형은 `appearance`, 배치는 `encounters.json`로 관심사 분리 |
|
||||
| **유물 19종 / 물약 6종** | 유물: StS 효과 × 메이플 장비 외형, TopBar 아이콘 + 마우스오버 툴팁, 8종 훅. 물약: 승리 40% 드랍·상점·슬롯 메뉴. 보물 방=상자 연출 → 유물+메소 |
|
||||
| **카드 프레임·등급** | 커스텀 프레임 3종(전사/마법사/도적 × normal/unique/legend), 카드 5개 사이트 통합 레이아웃. 보상 등급 가중 추첨 70/25/5 |
|
||||
| **영혼(Soul) 메타 성장** | 승천과 별개의 영구 강화 화폐. 2차 전직 상태로 보스 클리어 시 적립 → 로비 영혼 상점 4종 해금(시작 메소 +60·HP +15·덱 정제·시작 유물 +1). **UserDataStorage 영구 저장** |
|
||||
| **승천(Ascension)** | A1~A10 누적 모디파이어(적 강화·시작 HP 감소·보상 감소). UserDataStorage 유저별 영구 저장, 런 클리어 시 다음 단계 해금 |
|
||||
| **멀티 act** | **5막** 진행(보스 클리어→다음 막 텔레포트, 맵·인카운터 변경, 적 스케일 `1+(막-1)*0.45`), 5막 클리어 시 런 종료 |
|
||||
| **경제** | 화폐 표기 **메소**(코인 아이콘), 카드/유물/물약 메소 가격. 내부 식별자는 Gold 유지 |
|
||||
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트 |
|
||||
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트(현 97종) |
|
||||
|
||||
> ⚠️ 수치(적 스탯·경제·승천 배율)는 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 컨텍스트)에서:
|
||||
@@ -135,49 +149,68 @@ local c = _EntityService:GetEntityByPath("/common").SlayDeckController
|
||||
c:OnLobbyNpcInteract("run") -- 모험가(런 시작) / "codex"(도감) / "shop"(영혼상점) / "board"(게시판)
|
||||
c:ShowLobby() -- 로비 맵 복귀 + 상태 초기화
|
||||
-- 런
|
||||
c:SelectClass("warrior") -- "warrior" / "bandit" / "magician"
|
||||
c:SelectClass("warrior") -- "warrior" / "rogue" / "magician"
|
||||
c:StartNewGame() -- 캐릭터 선택 → 런 시작(map01 텔레포트)
|
||||
c:PickNode("r1c2") -- 맵 노드 선택(절차 생성 그리드 id) / "boss"
|
||||
c:PlayCard(1) -- 손패 slot 카드 사용
|
||||
c:EndPlayerTurn() -- 턴 종료 → 적 턴 → 다음 턴
|
||||
c:PickReward(1) -- 보상 카드 1택(0=건너뛰기)
|
||||
c:BuyCard(1) / c:BuyRelic() / c:BuyPotion() -- 상점 구매(메소)
|
||||
c:SetJob("fighter") -- 전직 (보스 보상 선택 화면)
|
||||
c:SetJob("fighter") -- 전직 (보스 보상 화면) — 2차: fighter/page/spearman·firepoison/icelightning/cleric·assassin/thief, 3차: hermit/thiefmaster
|
||||
c:AdjustAscension(1) -- 메뉴에서 승천 단계 +1
|
||||
```
|
||||
|
||||
밸런스 검증: `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/` 참조.
|
||||
|
||||
### 디버그 단축키
|
||||
|
||||
개발·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
|
||||
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-lobby-map.mjs # 로비 맵 + NPC 배치
|
||||
node tools/player/gen-lobby-npc.mjs # 로비 codeblock(LobbyNpc·LobbyMobility)
|
||||
node tools/camera/gen-camera.mjs # 맵별 카메라
|
||||
node tools/player/gen-player-lock.mjs # 전투맵 입력 잠금
|
||||
node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
|
||||
node tools/monster/gen-monster-models.mjs # 적 종별 모델 .model (외형=enemies.json appearance)
|
||||
node tools/monster/gen-combat-monster.mjs # 자기등록 마커 codeblock
|
||||
node tools/map/gen-map-encounters.mjs # encounters.json 로스터 기반 맵 몬스터 배치
|
||||
```
|
||||
> 산출물 검증은 내용 출력 없이 카운트만: `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).
|
||||
> ⚠️ **카드 `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 완료)**
|
||||
- [ ] **도적 카드 아이콘** — 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 확장, 메뉴 "이어하기" 활성화)
|
||||
- [ ] **카드 제거/업그레이드** — 상점 카드 제거 슬롯, 휴식 노드에서 카드 강화
|
||||
- [ ] **이벤트 노드(?)** — 랜덤 텍스트 이벤트(선택지·리스크/리워드)
|
||||
- [ ] **3차 전직** — 후반 막 보상으로 확장
|
||||
- [ ] **3차 전직 — 전사·법사 확장** (도적은 완료: 헤르밋·시프 마스터), 후반 막 보상으로
|
||||
- [ ] **궁수 등 추가 클래스** — 캐릭터 선택 슬롯 확장
|
||||
- [ ] **정밀 밸런싱** — 첫 인카운터 승률 완화·직업별 카드 효율 튜닝(`sim-balance.mjs` 리포트 기반)
|
||||
- [ ] **상점 보장 규칙** — 막당 상점 최소 1회 등장
|
||||
|
||||
40
RULES.md
40
RULES.md
@@ -11,29 +11,35 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
|
||||
|
||||
| 산출물 (절대 Read/Edit 금지) | 크기 | 단일 소스 (여기만 편집) | 재생성 명령 |
|
||||
|---|---|---|---|
|
||||
| `ui/DefaultGroup.ui` | **~7.1MB** | `data/*.json` + `tools/deck/gen-slaydeck.mjs` | `node tools/deck/gen-slaydeck.mjs` |
|
||||
| `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | 〃 | 〃 |
|
||||
| `ui/*.ui` (Default·Select·Lobby·Run·Deck·Popup·Toast UIGroup 7종) | 9KB~4.5MB | **메이커 저작 (생성기 미생성, 2026-06-17~)** — 메이커에서 시각 편집 | (없음) |
|
||||
| `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | `data/*.json` + `tools/deck/`(`gen-slaydeck.mjs`+`lib/`+`cb/`) | `node tools/deck/gen-slaydeck.mjs` |
|
||||
| `Global/common.gamelogic` | ~1KB | 〃 | 〃 |
|
||||
| `map/map01.map`~`map05.map`, `map/lobby.map` | 각 ~210KB | `tools/map/`·`tools/monster/`·`tools/camera/`·`tools/player/` (↓ 보조 생성기) | 해당 생성기 |
|
||||
| `RootDesk/MyDesk/CombatMonster.codeblock` | ~2KB | `tools/monster/gen-combat-monster.mjs` | `node tools/monster/gen-combat-monster.mjs` |
|
||||
| `RootDesk/MyDesk/Models/Monsters/<enemyId>.model` (적 종별, 최대 18종) | 각 ~5KB | `data/enemies.json`(`appearance`) + `tools/monster/gen-monster-models.mjs` | `node tools/monster/gen-monster-models.mjs` |
|
||||
| `RootDesk/MyDesk/PlayerLock.codeblock` | ~2KB | `tools/player/gen-player-lock.mjs` | `node tools/player/gen-player-lock.mjs` |
|
||||
| `RootDesk/MyDesk/MapCamera.codeblock` | ~2KB | `tools/camera/gen-camera.mjs` (값: `data/camera.json`) | `node tools/camera/gen-camera.mjs` |
|
||||
| `RootDesk/MyDesk/LobbyNpc.codeblock`·`LobbyMobility.codeblock` | 각 ~2-3KB | `tools/player/gen-lobby-npc.mjs` | `node tools/player/gen-lobby-npc.mjs` |
|
||||
| `Global/SectorConfig.config` | ~1KB | `tools/map/gen-maps.mjs`·`gen-lobby-map.mjs` (패치) | 해당 생성기 |
|
||||
|
||||
- `.claude/settings.json`의 permissions.deny가 위 파일의 Read/Edit/Write 도구 사용을 차단한다 (이 저장소를 열면 자동 적용). deny는 **glob** — `ui/*.ui`·`map/*.map`·`RootDesk/MyDesk/*.codeblock`·`Global/common.gamelogic`·`Global/SectorConfig.config`. 따라서 **메이커 저작 codeblock/UI**(`Monster`·`MonsterAttack`·`PlayerAttack`·`PlayerHit`·`UIPopup`·`UIToast`.codeblock, `ui/PopupGroup.ui`·`ui/ToastGroup.ui`)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발.
|
||||
- 게임 로직·UI 수정 = **`tools/deck/gen-slaydeck.mjs`(생성기 JS) 또는 `data/*.json`(데이터)을 수정** → 재생성 → 산출물은 통째로 커밋.
|
||||
- `.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 시 토큰 폭발.
|
||||
- **게임 로직 수정** = `tools/deck/gen-slaydeck.mjs`(오케스트레이터) + `tools/deck/cb/*.mjs`(codeblock Lua) 또는 `data/*.json`(데이터) 수정 → 재생성(`SlayDeckController.codeblock`+`common.gamelogic`만, **`.ui` 미접근**) → 통째로 커밋. **UI 수정 = 메이커에서**(생성기는 UI를 안 만든다).
|
||||
- **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 --`로 복원.
|
||||
- **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` 외):
|
||||
- `tools/camera/gen-camera.mjs` → `MapCamera.codeblock` + map01~05 카메라 부착 (값 `data/camera.json`)
|
||||
- `tools/map/gen-maps.mjs` → `map02~05` + `Global/SectorConfig.config` (map01 템플릿 클론)
|
||||
- `tools/map/gen-lobby-map.mjs` → `map/lobby.map` + `SectorConfig.config`
|
||||
- `tools/map/gen-map-encounters.mjs` → map01~05 노드 타입별 몬스터 그룹 재구성
|
||||
- `tools/monster/gen-combat-monster.mjs` → `CombatMonster.codeblock` + map01~05 부착
|
||||
- `tools/monster/freeze-turn-monsters.mjs` → 몬스터 `.model`·맵 AI 컴포넌트 제거
|
||||
- `tools/monster/gen-monster-models.mjs` → `Models/Monsters/<enemyId>.model`(적 종별, 외형·EnemyId 베이크·태생 AI-free). 단일 소스 `data/enemies.json`의 `appearance`. `appearance` 미보유 적은 스킵(메이커 저작 모델 `HolodragonKing`·`Model_monster-43`은 예외 — 이 생성기 대상 아님). 공용 빌더 `tools/monster/lib/monster-model.mjs`.
|
||||
- `tools/map/gen-map-encounters.mjs` → map01~05에 `data/encounters.json` 로스터 기반 **종별 모델 인스턴스** 배치(외형=정체성 고정). 준비도 가드: 로스터에 `appearance` 미보유 적이 있는 맵은 재생성 스킵(기존 보존). 값 검증 `node --test tools/monster/monster-model.test.mjs`.
|
||||
- `tools/monster/gen-combat-monster.mjs` → `CombatMonster.codeblock`(자기등록 마커)만 생성. 맵 부착값(EnemyId/Group)은 `gen-map-encounters.mjs`가 인스턴스에 직접 기록.
|
||||
- `tools/monster/freeze-turn-monsters.mjs` → 레거시 공용 몬스터 `.model`·맵 AI 컴포넌트 제거(신규 종별 모델은 태생 frozen — 대상 아님).
|
||||
- `tools/player/gen-player-lock.mjs` → `PlayerLock.codeblock` + map01~05 부착
|
||||
- `tools/player/gen-lobby-npc.mjs` → `LobbyNpc.codeblock`·`LobbyMobility.codeblock`
|
||||
- `tools/player/freeze-turn-player.mjs` → `Global/DefaultPlayer.model` 이동 0 고정
|
||||
- `tools/deck/gen-cardhand.mjs` → `DefaultGroup.ui` 카드핸드 보조 패처
|
||||
- (옛 `tools/deck/gen-cardhand.mjs`·`hud/*.mjs`는 `tools/deck/legacy/`로 이관 — 휴면, UI 메이커 저작 전환)
|
||||
|
||||
## 2. 산출물 검증은 카운트로, 내용 출력 금지
|
||||
|
||||
@@ -59,7 +65,10 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
|
||||
- 브랜치 → 커밋(기능 단위) → push → **PR은 반드시 `node tools/git/gitea-pr.mjs`로** (인라인 `curl -d` 한글 본문은 Windows에서 CP949로 깨짐 — PR #34~41 사고).
|
||||
- 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create <spec.json>` / `merge <번호>`.
|
||||
- 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) 연동 주의
|
||||
|
||||
@@ -81,3 +90,18 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
|
||||
- UI 텍스트에서는 정수값인 숫자에 `.0`을 붙이지 않는다. `1.0/1.0`이 아니라 `1/1`처럼 표시한다.
|
||||
- 생성기 내 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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/blue_mushroom.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/blue_mushroom.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-blue_mushroom",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "blue_mushroom",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-blue_mushroom",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "1a176a7afb114fe7aef2bc58ef2d945b"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "1a176a7afb114fe7aef2bc58ef2d945b",
|
||||
"move": "8239541953a6457fbe6d35e17f19f0f8",
|
||||
"hit": "7b405108d05741699893a4dc3d715165",
|
||||
"jump": "a7ea0755262242199ae50ab6a3387034",
|
||||
"die": "9e74e807797d442f9c938ca64aa9f4cd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "blue_mushroom"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/dile.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/dile.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-dile",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "dile",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-dile",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "68070c6f4abe40658899a208ddaf4081"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"move": "426ba2c6fa2d4cdd92bcb0bb37861dcc",
|
||||
"stand": "68070c6f4abe40658899a208ddaf4081",
|
||||
"skill": "4ba2cdc2f11746afa0f542293b0618d5",
|
||||
"hit": "172640e6d4ce444aa1dfbd9bd9523eb1",
|
||||
"die": "5d50d9aa34c745b9b8932c15da919927"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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": 2.2,
|
||||
"y": 1.51
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.220000029,
|
||||
"y": 0.755
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "dile"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
229
RootDesk/MyDesk/Models/Monsters/green_mushroom.model
Normal file
229
RootDesk/MyDesk/Models/Monsters/green_mushroom.model
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-green_mushroom",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "green_mushroom",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-green_mushroom",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "f86992ba9c41487c8480fcb893fcbda6"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "f86992ba9c41487c8480fcb893fcbda6",
|
||||
"hit": "d305b942b1704c8084548108ff3b7a6b",
|
||||
"die": "5a563e5fd98c4132b61057dc6bb8aaf2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.00999999,
|
||||
"y": 0.26
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "green_mushroom"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
229
RootDesk/MyDesk/Models/Monsters/junior_bugi.model
Normal file
229
RootDesk/MyDesk/Models/Monsters/junior_bugi.model
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-junior_bugi",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "junior_bugi",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-junior_bugi",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "a2204a21d88942b281d2cac6053ffbaa"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "a2204a21d88942b281d2cac6053ffbaa",
|
||||
"hit": "afc08936b8a64b26bc3dd8c03ead1f26",
|
||||
"die": "fc1c6d9ba9bc413ab53b6dbfae3ac45b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "junior_bugi"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
229
RootDesk/MyDesk/Models/Monsters/junior_neki.model
Normal file
229
RootDesk/MyDesk/Models/Monsters/junior_neki.model
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-junior_neki",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "junior_neki",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-junior_neki",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "48c10437ae8344a9b2a1d3f36185728f"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "48c10437ae8344a9b2a1d3f36185728f",
|
||||
"hit": "9044063647854f5e9128efcf80e909be",
|
||||
"die": "f414577d18c94cc387c275df4abdbc3b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "junior_neki"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
229
RootDesk/MyDesk/Models/Monsters/kapa_drake.model
Normal file
229
RootDesk/MyDesk/Models/Monsters/kapa_drake.model
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-kapa_drake",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "kapa_drake",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-kapa_drake",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "4ca39dbfa1c6492283ba8bd352d12b0a"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "4ca39dbfa1c6492283ba8bd352d12b0a",
|
||||
"hit": "7ac78511036e4ebe988b97c35fc275d1",
|
||||
"die": "740f3f2b2e7a4b71bec5eac84e8539f9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "kapa_drake"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
233
RootDesk/MyDesk/Models/Monsters/king_slime.model
Normal file
233
RootDesk/MyDesk/Models/Monsters/king_slime.model
Normal file
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-king_slime",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "king_slime",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-king_slime",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "dd9de73d580240faab8cad03b587013b"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"move": "873425127b75475b9944dc86bf77f885",
|
||||
"stand": "dd9de73d580240faab8cad03b587013b",
|
||||
"jump": "6a2b983b7a31417ca19c29c3d1d00817",
|
||||
"attack": "a34d1146057443fd8b578dafeb7c2ed1",
|
||||
"skill": "0b0bb78f0ca44526bad6d994bb16f973",
|
||||
"hit": "d2de42d3233b42a58d9799d5e762a19c",
|
||||
"die": "5bd3969c3bcb4df2bd79c2b940ee03dc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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": 2.19,
|
||||
"y": 1.39
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.335000038,
|
||||
"y": 0.695
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "king_slime"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/mano.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/mano.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-mano",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "mano",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-mano",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "e035bb90c053401b88de2159dfa230eb"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"move": "3dcd0dc63d2d491b9b8d39b3b9d0a214",
|
||||
"stand": "e035bb90c053401b88de2159dfa230eb",
|
||||
"skill": "c05453dd21fd4ed581d193930ab4c331",
|
||||
"hit": "452cb740ddcb4837a46b75d7935e2ffc",
|
||||
"die": "f430051f6fc34f2eb56fe5e62b346eac"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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": 1.05,
|
||||
"y": 0.95
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.004999995,
|
||||
"y": 0.475
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "mano"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
230
RootDesk/MyDesk/Models/Monsters/modified_snail.model
Normal file
230
RootDesk/MyDesk/Models/Monsters/modified_snail.model
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-modified_snail",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "modified_snail",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-modified_snail",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "17b55730c26f4fd6b8fcfa288da388de"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "17b55730c26f4fd6b8fcfa288da388de",
|
||||
"move": "f40108c8b0b84696a67337b801201f7d",
|
||||
"hit": "eac48e84a9fc4580a4018de5cf52ddb3",
|
||||
"die": "51c2f4b59a2c413db26035aa57002fc8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.75,
|
||||
"y": 0.68
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "modified_snail"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
232
RootDesk/MyDesk/Models/Monsters/mushmom.model
Normal file
232
RootDesk/MyDesk/Models/Monsters/mushmom.model
Normal file
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-mushmom",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "mushmom",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-mushmom",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "23c38ef3acad4a30ad59120bb939b008"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "23c38ef3acad4a30ad59120bb939b008",
|
||||
"move": "24d8a3a75f96406ba690ed42d7250b8f",
|
||||
"hit": "c826e36ee89c48bca6aab856aa773f38",
|
||||
"attack": "4d7465e950144dc59c263aad01b14e14",
|
||||
"jump": "b7ddbda71a294141ba134249fc34c7da",
|
||||
"die": "f50664a4524147399359cb90a6f3e80c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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": 1.2,
|
||||
"y": 1.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.02,
|
||||
"y": 0.55
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "mushmom"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
229
RootDesk/MyDesk/Models/Monsters/octopus.model
Normal file
229
RootDesk/MyDesk/Models/Monsters/octopus.model
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-octopus",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "octopus",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-octopus",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "d8f014043ce8418f96700c2b6c9ebf6c"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"hit": "c3cf643b618346c7bfa6574187b396f9",
|
||||
"die": "a88d9b3d60f941e4890dc89a6ccaa8ee"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "octopus"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/orange_mushroom.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/orange_mushroom.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-orange_mushroom",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "orange_mushroom",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-orange_mushroom",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "6d381bea1bcb4504b518a1fbfa0904ac"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"move": "573fe938562a4abf91eebf951f21afd5",
|
||||
"stand": "6d381bea1bcb4504b518a1fbfa0904ac",
|
||||
"jump": "59823e146a034e48b8667ebb6f0724b1",
|
||||
"hit": "642ece38d8d449b29ce4479100e37a54",
|
||||
"die": "3c99d6b9b89b4295a9c2749eb02e28e9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/pig.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/pig.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-pig",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "pig",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-pig",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "528a8638b12f41b8b5781a05360d2949"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "528a8638b12f41b8b5781a05360d2949",
|
||||
"move": "8baad61512be4b33b2a0879fec7a266e",
|
||||
"hit": "60e42a918a0342478903cc71adba1dc5",
|
||||
"jump": "c9e27ce6f8344aefba169c5ca6571def",
|
||||
"die": "0644beff80a44ec7acc011ea0961df57"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "pig"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
230
RootDesk/MyDesk/Models/Monsters/red_snail.model
Normal file
230
RootDesk/MyDesk/Models/Monsters/red_snail.model
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-red_snail",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "red_snail",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-red_snail",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "17b55730c26f4fd6b8fcfa288da388de"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "17b55730c26f4fd6b8fcfa288da388de",
|
||||
"move": "f40108c8b0b84696a67337b801201f7d",
|
||||
"hit": "eac48e84a9fc4580a4018de5cf52ddb3",
|
||||
"die": "51c2f4b59a2c413db26035aa57002fc8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "red_snail"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/slime.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/slime.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-slime",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "slime",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-slime",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "50faf654ee5d479cb2958edce9feaef0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "50faf654ee5d479cb2958edce9feaef0",
|
||||
"move": "dc932872543f4a02bf41e977ab79e5ad",
|
||||
"hit": "61c27025a8f14c478f30ede1b49758bc",
|
||||
"jump": "8b89d86b1a9c4c4288650614c6f30e67",
|
||||
"die": "31ecb6c7cbc24599881f00cb01599f09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "slime"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/slime_boss.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/slime_boss.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-slime_boss",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "slime_boss",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-slime_boss",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "50faf654ee5d479cb2958edce9feaef0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "50faf654ee5d479cb2958edce9feaef0",
|
||||
"move": "dc932872543f4a02bf41e977ab79e5ad",
|
||||
"hit": "61c27025a8f14c478f30ede1b49758bc",
|
||||
"jump": "8b89d86b1a9c4c4288650614c6f30e67",
|
||||
"die": "31ecb6c7cbc24599881f00cb01599f09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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": 1.2,
|
||||
"y": 1.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "slime_boss"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RootDesk/MyDesk/Models/Monsters/slime_elite.model
Normal file
231
RootDesk/MyDesk/Models/Monsters/slime_elite.model
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-slime_elite",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "slime_elite",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-slime_elite",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "50faf654ee5d479cb2958edce9feaef0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "50faf654ee5d479cb2958edce9feaef0",
|
||||
"move": "dc932872543f4a02bf41e977ab79e5ad",
|
||||
"hit": "61c27025a8f14c478f30ede1b49758bc",
|
||||
"jump": "8b89d86b1a9c4c4288650614c6f30e67",
|
||||
"die": "31ecb6c7cbc24599881f00cb01599f09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.85,
|
||||
"y": 0.78
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "slime_elite"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
230
RootDesk/MyDesk/Models/Monsters/stump.model
Normal file
230
RootDesk/MyDesk/Models/Monsters/stump.model
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "model://monster-stump",
|
||||
"ContentType": "x-mod/model",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.3.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"Version": 1,
|
||||
"Name": "stump",
|
||||
"BaseModelId": null,
|
||||
"Id": "monster-stump",
|
||||
"Components": [
|
||||
"MOD.Core.TransformComponent",
|
||||
"MOD.Core.StateAnimationComponent",
|
||||
"MOD.Core.SpriteRendererComponent",
|
||||
"MOD.Core.RigidbodyComponent",
|
||||
"MOD.Core.MovementComponent",
|
||||
"MOD.Core.StateComponent",
|
||||
"MOD.Core.HitComponent",
|
||||
"MOD.Core.DamageSkinSpawnerComponent",
|
||||
"script.Monster",
|
||||
"script.MonsterAttack",
|
||||
"MOD.Core.KinematicbodyComponent",
|
||||
"MOD.Core.SideviewbodyComponent",
|
||||
"MOD.Core.DamageSkinSettingComponent",
|
||||
"script.CombatMonster"
|
||||
],
|
||||
"Properties": [
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "speed",
|
||||
"DisplayName": "speed",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "InputSpeed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "jumpForce",
|
||||
"DisplayName": "jumpForce",
|
||||
"ShowInInspector": true,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MovementComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "JumpForce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "actionSheet",
|
||||
"DisplayName": "actionSheet",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.StateAnimationComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "ActionSheet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Name": "renderguid",
|
||||
"DisplayName": "renderguid",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "SpriteRUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Type": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.RenderSettingType, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Name": "renderSetting",
|
||||
"DisplayName": "renderSetting",
|
||||
"ShowInInspector": false,
|
||||
"Link": {
|
||||
"Target": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.SpriteRendererComponent, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Property": "RenderSetting"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Values": [
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "OrderInLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 2
|
||||
},
|
||||
{
|
||||
"TargetType": null,
|
||||
"Name": "renderguid",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "null"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "CollisionGroup",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.Physics.CollisionGroup, MOD.Core, Version=26.3.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"$type": "MOD.Core.Physics.CollisionGroup, MOD.Core",
|
||||
"Id": "8992acd1e8cd45838db6f10a7b41df09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SpriteRUID",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "ed3908e24d694bb786023fc1ed073489"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.SpriteRendererComponent",
|
||||
"Name": "SortingLayer",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "MapLayer0"
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.StateAnimationComponent",
|
||||
"Name": "ActionSheet",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
},
|
||||
"Value": {
|
||||
"stand": "ed3908e24d694bb786023fc1ed073489",
|
||||
"move": "9a4cad470f304753885e06c043156efb",
|
||||
"hit": "4763c9bebc9245998c9c499b6316aa9f",
|
||||
"die": "b168793b92a844a3a3a6f4ce647a14d2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "BoxSize",
|
||||
"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.63,
|
||||
"y": 0.58
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.HitComponent",
|
||||
"Name": "ColliderOffset",
|
||||
"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.0449999869,
|
||||
"y": 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"TargetType": "MOD.Core.MovementComponent",
|
||||
"Name": "InputSpeed",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": 0
|
||||
},
|
||||
{
|
||||
"TargetType": "script.CombatMonster",
|
||||
"Name": "EnemyId",
|
||||
"ValueType": {
|
||||
"$type": "MODNativeType",
|
||||
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
},
|
||||
"Value": "stump"
|
||||
}
|
||||
],
|
||||
"EventLinks": [],
|
||||
"Children": []
|
||||
}
|
||||
}
|
||||
}
|
||||
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",
|
||||
"legend": "cff71f2e472041ce80c6fbd296f42e2d"
|
||||
},
|
||||
"bandit": {
|
||||
"rogue": {
|
||||
"normal": "9487b06867bc46269ed1d855420f457f",
|
||||
"unique": "b3081fb2fb1445fa90b12b01481a78ef",
|
||||
"legend": "c357d2daf31a489d95b8fa47e50dd879"
|
||||
@@ -25,11 +25,13 @@
|
||||
"firepoison": "magician",
|
||||
"icelightning": "magician",
|
||||
"cleric": "magician",
|
||||
"bandit": "bandit",
|
||||
"curse": "bandit",
|
||||
"shiv": "bandit",
|
||||
"poisoner": "bandit",
|
||||
"trickster": "bandit"
|
||||
"curse": "rogue",
|
||||
"shiv": "rogue",
|
||||
"rogue": "rogue",
|
||||
"assassin": "rogue",
|
||||
"hermit": "rogue",
|
||||
"thief": "rogue",
|
||||
"thiefmaster": "rogue"
|
||||
},
|
||||
"rewardWeights": {
|
||||
"normal": 70,
|
||||
|
||||
1423
data/cards.json
1423
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.
@@ -2,6 +2,6 @@
|
||||
"portraits": {
|
||||
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
|
||||
"magician": "3b9ea1f066a744bb859df47fef817277",
|
||||
"bandit": "efa920e58d31426486ef974106e7dc8b"
|
||||
"rogue": "efa920e58d31426486ef974106e7dc8b"
|
||||
}
|
||||
}
|
||||
|
||||
7
data/encounters.json
Normal file
7
data/encounters.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"map01": { "combat": ["junior_bugi", "kapa_drake", "junior_neki", "octopus", "green_mushroom", "orange_mushroom"], "elite": ["dile", "mano"], "boss": ["king_slime"] },
|
||||
"map02": { "combat": ["pig", "green_mushroom", "blue_mushroom", "orange_mushroom", "slime"], "elite": ["mushmom"], "boss": ["slime_boss"] },
|
||||
"map03": { "combat": ["octopus", "junior_neki", "junior_bugi", "slime"], "elite": ["slime_elite"], "boss": ["king_slime"] },
|
||||
"map04": { "combat": ["kapa_drake", "junior_neki", "junior_bugi", "stump"], "elite": ["dile"], "boss": ["mushmom"] },
|
||||
"map05": { "combat": ["kapa_drake", "octopus", "junior_bugi", "junior_neki"], "elite": ["dile", "slime_elite"], "boss": ["king_slime"] }
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Defend", "value": 8 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "50faf654ee5d479cb2958edce9feaef0", "move": "dc932872543f4a02bf41e977ab79e5ad", "hit": "61c27025a8f14c478f30ede1b49758bc", "jump": "8b89d86b1a9c4c4288650614c6f30e67", "die": "31ecb6c7cbc24599881f00cb01599f09" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"slime_elite": {
|
||||
"name": "정예 슬라임",
|
||||
@@ -17,7 +18,8 @@
|
||||
{ "kind": "Attack", "value": 8 },
|
||||
{ "kind": "Defend", "value": 10 },
|
||||
{ "kind": "Debuff", "effect": "weak", "value": 1 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "50faf654ee5d479cb2958edce9feaef0", "move": "dc932872543f4a02bf41e977ab79e5ad", "hit": "61c27025a8f14c478f30ede1b49758bc", "jump": "8b89d86b1a9c4c4288650614c6f30e67", "die": "31ecb6c7cbc24599881f00cb01599f09" }, "box": { "x": 0.85, "y": 0.78 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"slime_boss": {
|
||||
"name": "슬라임 킹",
|
||||
@@ -28,7 +30,8 @@
|
||||
{ "kind": "Debuff", "effect": "vuln", "value": 2 },
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "Attack", "value": 22 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "50faf654ee5d479cb2958edce9feaef0", "move": "dc932872543f4a02bf41e977ab79e5ad", "hit": "61c27025a8f14c478f30ede1b49758bc", "jump": "8b89d86b1a9c4c4288650614c6f30e67", "die": "31ecb6c7cbc24599881f00cb01599f09" }, "box": { "x": 1.2, "y": 1.1 }, "off": { "x": 0.0449999869, "y": 0.4 } }
|
||||
},
|
||||
"orange_mushroom": {
|
||||
"name": "주황버섯",
|
||||
@@ -38,7 +41,8 @@
|
||||
{ "kind": "Attack", "value": 5 },
|
||||
{ "kind": "Defend", "value": 4 },
|
||||
{ "kind": "Attack", "value": 8 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "move": "573fe938562a4abf91eebf951f21afd5", "stand": "6d381bea1bcb4504b518a1fbfa0904ac", "jump": "59823e146a034e48b8667ebb6f0724b1", "hit": "642ece38d8d449b29ce4479100e37a54", "die": "3c99d6b9b89b4295a9c2749eb02e28e9" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"blue_mushroom": {
|
||||
"name": "파란버섯",
|
||||
@@ -48,7 +52,8 @@
|
||||
{ "kind": "Attack", "value": 4 },
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "AddCard", "card": "Wound", "count": 1 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "1a176a7afb114fe7aef2bc58ef2d945b", "move": "8239541953a6457fbe6d35e17f19f0f8", "hit": "7b405108d05741699893a4dc3d715165", "jump": "a7ea0755262242199ae50ab6a3387034", "die": "9e74e807797d442f9c938ca64aa9f4cd" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"pig": {
|
||||
"name": "돼지",
|
||||
@@ -57,7 +62,8 @@
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Defend", "value": 5 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "528a8638b12f41b8b5781a05360d2949", "move": "8baad61512be4b33b2a0879fec7a266e", "hit": "60e42a918a0342478903cc71adba1dc5", "jump": "c9e27ce6f8344aefba169c5ca6571def", "die": "0644beff80a44ec7acc011ea0961df57" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"green_mushroom": {
|
||||
"name": "초록버섯",
|
||||
@@ -66,7 +72,8 @@
|
||||
{ "kind": "Attack", "value": 7 },
|
||||
{ "kind": "Defend", "value": 3 },
|
||||
{ "kind": "Attack", "value": 9 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "f86992ba9c41487c8480fcb893fcbda6", "hit": "d305b942b1704c8084548108ff3b7a6b", "die": "5a563e5fd98c4132b61057dc6bb8aaf2" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.00999999, "y": 0.26 } }
|
||||
},
|
||||
"red_snail": {
|
||||
"name": "빨간 달팽이",
|
||||
@@ -75,7 +82,8 @@
|
||||
{ "kind": "Attack", "value": 5 },
|
||||
{ "kind": "Defend", "value": 6 },
|
||||
{ "kind": "Attack", "value": 7 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "17b55730c26f4fd6b8fcfa288da388de", "move": "f40108c8b0b84696a67337b801201f7d", "hit": "eac48e84a9fc4580a4018de5cf52ddb3", "die": "51c2f4b59a2c413db26035aa57002fc8" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"stump": {
|
||||
"name": "나무토막",
|
||||
@@ -84,7 +92,8 @@
|
||||
{ "kind": "Defend", "value": 5 },
|
||||
{ "kind": "Attack", "value": 8 },
|
||||
{ "kind": "Attack", "value": 6 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "ed3908e24d694bb786023fc1ed073489", "move": "9a4cad470f304753885e06c043156efb", "hit": "4763c9bebc9245998c9c499b6316aa9f", "die": "b168793b92a844a3a3a6f4ce647a14d2" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"mushmom": {
|
||||
"name": "머쉬맘",
|
||||
@@ -96,7 +105,8 @@
|
||||
{ "kind": "Attack", "value": 9 },
|
||||
{ "kind": "Defend", "value": 6 },
|
||||
{ "kind": "AddCard", "card": "Burn", "count": 1 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "23c38ef3acad4a30ad59120bb939b008", "move": "24d8a3a75f96406ba690ed42d7250b8f", "hit": "c826e36ee89c48bca6aab856aa773f38", "attack": "4d7465e950144dc59c263aad01b14e14", "jump": "b7ddbda71a294141ba134249fc34c7da", "die": "f50664a4524147399359cb90a6f3e80c" }, "box": { "x": 1.2, "y": 1.1 }, "off": { "x": 0.02, "y": 0.55 } }
|
||||
},
|
||||
"modified_snail": {
|
||||
"name": "변형된 달팽이",
|
||||
@@ -107,7 +117,8 @@
|
||||
{ "kind": "Attack", "value": 7 },
|
||||
{ "kind": "Attack", "value": 14 },
|
||||
{ "kind": "Debuff", "effect": "weak", "value": 1 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "17b55730c26f4fd6b8fcfa288da388de", "move": "f40108c8b0b84696a67337b801201f7d", "hit": "eac48e84a9fc4580a4018de5cf52ddb3", "die": "51c2f4b59a2c413db26035aa57002fc8" }, "box": { "x": 0.75, "y": 0.68 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"king_slime": {
|
||||
"name": "킹 슬라임",
|
||||
@@ -118,7 +129,73 @@
|
||||
{ "kind": "Debuff", "effect": "vuln", "value": 2 },
|
||||
{ "kind": "Attack", "value": 12 },
|
||||
{ "kind": "Attack", "value": 24 }
|
||||
]
|
||||
],
|
||||
"appearance": { "sheet": { "move": "873425127b75475b9944dc86bf77f885", "stand": "dd9de73d580240faab8cad03b587013b", "jump": "6a2b983b7a31417ca19c29c3d1d00817", "attack": "a34d1146057443fd8b578dafeb7c2ed1", "skill": "0b0bb78f0ca44526bad6d994bb16f973", "hit": "d2de42d3233b42a58d9799d5e762a19c", "die": "5bd3969c3bcb4df2bd79c2b940ee03dc" }, "box": { "x": 2.19, "y": 1.39 }, "off": { "x": 0.335000038, "y": 0.695 } }
|
||||
},
|
||||
"octopus": {
|
||||
"name": "문어",
|
||||
"maxHp": 15,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 5 },
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Defend", "value": 4 }
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "d8f014043ce8418f96700c2b6c9ebf6c", "hit": "c3cf643b618346c7bfa6574187b396f9", "die": "a88d9b3d60f941e4890dc89a6ccaa8ee" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"kapa_drake": {
|
||||
"name": "카파 드레이크",
|
||||
"maxHp": 24,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 9 },
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Defend", "value": 6 },
|
||||
{ "kind": "Attack", "value": 11 }
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "4ca39dbfa1c6492283ba8bd352d12b0a", "hit": "7ac78511036e4ebe988b97c35fc275d1", "die": "740f3f2b2e7a4b71bec5eac84e8539f9" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"junior_neki": {
|
||||
"name": "주니어 네키",
|
||||
"maxHp": 18,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 6 },
|
||||
{ "kind": "Attack", "value": 8 },
|
||||
{ "kind": "Debuff", "effect": "weak", "value": 1 }
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "48c10437ae8344a9b2a1d3f36185728f", "hit": "9044063647854f5e9128efcf80e909be", "die": "f414577d18c94cc387c275df4abdbc3b" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"junior_bugi": {
|
||||
"name": "주니어 부기",
|
||||
"maxHp": 20,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 7 },
|
||||
{ "kind": "Defend", "value": 5 },
|
||||
{ "kind": "Attack", "value": 9 }
|
||||
],
|
||||
"appearance": { "sheet": { "stand": "a2204a21d88942b281d2cac6053ffbaa", "hit": "afc08936b8a64b26bc3dd8c03ead1f26", "die": "fc1c6d9ba9bc413ab53b6dbfae3ac45b" }, "box": { "x": 0.63, "y": 0.58 }, "off": { "x": 0.0449999869, "y": 0.29 } }
|
||||
},
|
||||
"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 }
|
||||
],
|
||||
"appearance": { "sheet": { "move": "426ba2c6fa2d4cdd92bcb0bb37861dcc", "stand": "68070c6f4abe40658899a208ddaf4081", "skill": "4ba2cdc2f11746afa0f542293b0618d5", "hit": "172640e6d4ce444aa1dfbd9bd9523eb1", "die": "5d50d9aa34c745b9b8932c15da919927" }, "box": { "x": 2.2, "y": 1.51 }, "off": { "x": -0.220000029, "y": 0.755 } }
|
||||
},
|
||||
"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 }
|
||||
],
|
||||
"appearance": { "sheet": { "move": "3dcd0dc63d2d491b9b8d39b3b9d0a214", "stand": "e035bb90c053401b88de2159dfa230eb", "skill": "c05453dd21fd4ed581d193930ab4c331", "hit": "452cb740ddcb4837a46b75d7935e2ffc", "die": "f430051f6fc34f2eb56fe5e62b346eac" }, "box": { "x": 1.05, "y": 0.95 }, "off": { "x": 0.004999995, "y": 0.475 } }
|
||||
}
|
||||
},
|
||||
"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 위치 이동(렌더 무관).
|
||||
127
docs/superpowers/plans/2026-06-16-codeblock-modularization.md
Normal file
127
docs/superpowers/plans/2026-06-16-codeblock-modularization.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Phase 1b — codeblock 메서드 모듈화 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans 로 태스크 단위 구현. 단계는 `- [ ]` 체크박스.
|
||||
|
||||
**Goal:** `gen-slaydeck.mjs` `writeCodeblocks()`의 메서드 161개를 연속-런 모듈 `cb/*.mjs`로 분리하되 출력 `SlayDeckController.codeblock`은 바이트 동일.
|
||||
|
||||
**Architecture:** 단방향 의존 orchestrator→cb→lib. method/prop/codeblock 헬퍼+공유상수를 `lib/codeblock.mjs`로. 메서드는 **원본 순서 보존**을 위해 기능 버킷이 아닌 **연속 구간**으로 나눠 `writeCodeblocks`가 순서대로 spread-concat. prop 103개는 오케스트레이터 유지.
|
||||
|
||||
**Tech Stack:** Node ESM. 검증 = `diffcheck`(codeblock 바이트 동일) + 미러 `node --test`.
|
||||
|
||||
**의존:** Phase 1(#70)의 모듈 gen-slaydeck 위에 스택(`feature/cb-modularization`). #70 머지 후 main에 리베이스.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 검증 게이트 (모든 Task 공통)
|
||||
각 추출 후:
|
||||
```
|
||||
node tools/deck/gen-slaydeck.mjs
|
||||
node tools/verify/diffcheck.mjs
|
||||
```
|
||||
**합격**: `RootDesk/MyDesk/SlayDeckController.codeblock`(+ui·common) **IDENTICAL**. 워킹트리 ` M`은 autocrlf churn → `git checkout --`로 복원, **산출물은 커밋 안 함**(소스만).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `lib/codeblock.mjs` — 헬퍼 + 공유 상수 추출
|
||||
|
||||
**Files:** Create `tools/deck/lib/codeblock.mjs`; Modify `tools/deck/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1:** `lib/codeblock.mjs` 생성. gen-slaydeck.mjs에서 이동:
|
||||
- 함수: `prop`·`method`·`codeblock` (정의 본문 그대로).
|
||||
- `writeCodeblocks` 지역 상수 9개(현 `:292-300`): `RUN_LENGTH`(5) `GOLD_PER_WIN`(25) `CARD_PRICE`(30) `REST_HEAL`(30) `RELIC_PRICE`(60) `ACT_COUNT`(5) `ACT_MAPS`(['map01'..'map05']) `LOBBY_MAP`('lobby') `LOBBY_SPAWN`('Vector3(-5, 0.03, 0)'). → writeCodeblocks 본문에서 제거(import로 대체).
|
||||
- 끝에 `export { prop, method, codeblock, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN };`
|
||||
- ⚠️ `prop`/`method`/`codeblock`이 다른 헬퍼(없음 — 순수)·데이터를 참조하지 않는지 확인. 참조 시 함께 import.
|
||||
|
||||
- [ ] **Step 2:** gen-slaydeck.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';` 추가(기존 lib import 옆).
|
||||
|
||||
- [ ] **Step 3:** 검증 게이트 → codeblock IDENTICAL → churn 복원.
|
||||
|
||||
- [ ] **Step 4:** 커밋
|
||||
```
|
||||
git add tools/deck/lib/codeblock.mjs tools/deck/gen-slaydeck.mjs
|
||||
git commit -m "refactor(cb): lib/codeblock.mjs로 헬퍼·상수 추출 (출력 바이트 동일)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 메서드 추출 공통 레시피 (Task 2~의 각 런)
|
||||
|
||||
`writeCodeblocks`의 `const combat = codeblock('SlayDeckController','SlayDeckController', [<props>], [\n method('OnBeginPlay', …),\n method('ReqLoadAscension', …),\n … 161개 …\n])` 에서 메서드 배열을 런별로 분리:
|
||||
1. `tools/deck/cb/<name>.mjs` 생성:
|
||||
```js
|
||||
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 { /* lib/data.mjs 전체 — 자동 파생 */ } from '../lib/data.mjs';
|
||||
import { /* lib/ui-helpers.mjs 전체 — 자동 파생 */ } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const <name>Methods = [
|
||||
method('<First>', `…`, …),
|
||||
…, // ← 원본 method() 호출 verbatim 이동
|
||||
method('<Last>', `…`, …),
|
||||
];
|
||||
```
|
||||
2. writeCodeblocks의 해당 런 method() 호출 스팬을 제거하고, methods 배열을 spread로 교체:
|
||||
`codeblock(…, [<props>], [ ...bootMethods, ...stateMethods, …, ...shopMethods ])`.
|
||||
3. **검증 게이트**(codeblock IDENTICAL) → churn 복원 → 커밋.
|
||||
4. ⚠️ 메서드가 writeCodeblocks 지역변수/다른 메서드를 **JS레벨로 참조**하면(드묾) undefined throw/diffcheck로 노출 → 그 변수도 lib로 옮기거나 인자화.
|
||||
- import 이름은 lib export에서 **자동 파생**(누락 방지). 메서드 본문은 Lua 문자열이라 보간(`${RUN_LENGTH}`·`${luaCardsTable(...)}`)만 JS평가.
|
||||
|
||||
### 런 → 모듈 경계 (원본 순서, 161개)
|
||||
| 모듈 | export | 첫 메서드 → 끝 메서드 | 수 |
|
||||
|---|---|---|---|
|
||||
| `cb/boot.mjs` | `bootMethods` | `OnBeginPlay` → `AscStartHpPenalty` | 11 |
|
||||
| `cb/state.mjs` | `stateMethods` | `HideGameHud` → `CloseBoard` | 12 |
|
||||
| `cb/soul.mjs` | `soulMethods` | `ShowSoulShop` → `ApplySoulUnlocks` | 11 |
|
||||
| `cb/charselect.mjs` | `charSelectMethods` | `ShowCharacterSelect` → `SetEntityEnabled` | 5 |
|
||||
| `cb/run.mjs` | `runMethods` | `StartRun` → `ReviveMonsterEntity` | 6 |
|
||||
| `cb/deckturn.mjs` | `deckTurnMethods` | `Shuffle` → `RenderPiles` | 8 |
|
||||
| `cb/deckview.mjs` | `deckViewMethods` | `OpenDeckInspect` → `ApplyAllDeckCardVisual` | 12 |
|
||||
| `cb/hand.mjs` | `handMethods` | `GetHandSlotX` → `SelectDiscardSlot` | 18 |
|
||||
| `cb/combat.mjs` | `combatMethods` | `PlayCard` → `ContinueAfterBoss` | 20 |
|
||||
| `cb/jobs.mjs` | `jobMethods` | `ShowJobChoice` → `SetJob` | 5 |
|
||||
| `cb/runend.mjs` | `runEndMethods` | `TeleportToActMap` → `EndRun` | 3 |
|
||||
| `cb/render.mjs` | `renderMethods` | `BuffsLabel` → `RenderRun` | 12 |
|
||||
| `cb/reward.mjs` | `rewardMethods` | `CardPool` → `PickReward` | 4 |
|
||||
| `cb/items.mjs` | `itemMethods` | `HasRelic` → `RenderRelics` | 12 |
|
||||
| `cb/tooltip.mjs` | `tooltipMethods` | `BuildCardKeywordTooltip` → `HideTooltip` | 6 |
|
||||
| `cb/map.mjs` | `mapMethods` | `ShowMap` → `PickNode` | 7 |
|
||||
| `cb/shop.mjs` | `shopMethods` | `ShowShop` → `OpenChest` | 9 |
|
||||
|
||||
최종 concat 순서(= 원본): `[ ...bootMethods, ...stateMethods, ...soulMethods, ...charSelectMethods, ...runMethods, ...deckTurnMethods, ...deckViewMethods, ...handMethods, ...combatMethods, ...jobMethods, ...runEndMethods, ...renderMethods, ...rewardMethods, ...itemMethods, ...tooltipMethods, ...mapMethods, ...shopMethods ]`.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 런 추출 배치 A (말단부터 — 위험 낮은 순)
|
||||
**Files:** Create `cb/shop.mjs map.mjs tooltip.mjs items.mjs reward.mjs`; Modify gen-slaydeck.mjs
|
||||
|
||||
- [ ] 레시피로 **shop → map → tooltip → items → reward** 추출·검증·커밋(런 1개당 또는 묶음당 게이트 통과 필수).
|
||||
|
||||
### Task 3: 런 추출 배치 B
|
||||
**Files:** Create `cb/render.mjs runend.mjs jobs.mjs combat.mjs`; Modify gen-slaydeck.mjs
|
||||
- [ ] 레시피로 **render → runend → jobs → combat** 추출·검증·커밋.
|
||||
|
||||
### Task 4: 런 추출 배치 C
|
||||
**Files:** Create `cb/hand.mjs deckview.mjs deckturn.mjs run.mjs`; Modify gen-slaydeck.mjs
|
||||
- [ ] 레시피로 **hand → deckview → deckturn → run** 추출·검증·커밋.
|
||||
|
||||
### Task 5: 런 추출 배치 D (앞부분 — 마지막)
|
||||
**Files:** Create `cb/charselect.mjs soul.mjs state.mjs boot.mjs`; Modify gen-slaydeck.mjs
|
||||
- [ ] 레시피로 **charselect → soul → state → boot** 추출·검증·커밋. 완료 후 writeCodeblocks는 props 배열 + `[ ...17 spreads ]` + write만 남아야 함.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 마무리 — RULES + 회귀 + PR
|
||||
|
||||
**Files:** Modify `RULES.md`
|
||||
|
||||
- [ ] **Step 1:** RULES §1의 gen-slaydeck 모듈 설명에 `tools/deck/cb/*.mjs`(메서드)·`tools/deck/lib/codeblock.mjs`(헬퍼·상수) 추가. 단일소스 표/보조 생성기 일관성 유지.
|
||||
- [ ] **Step 2:** 회귀: `node --test tools/balance/sim-balance.test.mjs` · `node --test tools/map/rogue-map.test.mjs` (exit 0).
|
||||
- [ ] **Step 3:** 최종 재생성 + 검증 게이트(누적 codeblock IDENTICAL). `git status --short` 산출물 변경 없음.
|
||||
- [ ] **Step 4:** RULES 커밋 → push → PR(`node tools/git/gitea-pr.mjs create <spec.json>`, UTF-8). PR 제목·본문 한국어(RULES §4).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **스펙 커버리지**: lib/codeblock(T1) · 메서드 17런 모듈화(T2~5) · prop 유지(범위 명시) · 바이트동일 게이트(공통) · RULES(T6) · 미러회귀(T6). 누락 없음.
|
||||
- **플레이스홀더**: 런 경계는 첫/끝 메서드로 구체 지정(161개 합), 상수 9개 명시, import 자동 파생. "verbatim 이동"은 리팩터 특성(바이트 검증이 정확성 보장).
|
||||
- **타입 일관성**: export명(`xMethods`)↔concat spread 일치. lib/codeblock export↔orchestrator/cb import 일치.
|
||||
- **리스크**: 메서드 JS레벨 외부참조 → diffcheck/throw 즉시 노출, 증분으로 범위 최소. 단방향 의존 순환 없음.
|
||||
169
docs/superpowers/plans/2026-06-16-generator-modularization.md
Normal file
169
docs/superpowers/plans/2026-06-16-generator-modularization.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 생성기 모듈화 (Phase 1) 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans 로 태스크 단위 구현. 단계는 `- [ ]` 체크박스.
|
||||
|
||||
**Goal:** `tools/deck/gen-slaydeck.mjs`(~6,200줄)의 공유 인프라와 UI emit 16종을 `lib/`·`hud/` 모듈로 분리하되 출력 산출물은 바이트 동일로 유지한다.
|
||||
|
||||
**Architecture:** 단방향 의존 — `gen-slaydeck.mjs`(오케스트레이터) → `hud/*.mjs`(HUD별 build 함수) → `lib/*.mjs`(헬퍼·상수·데이터). `guid(prefix,n)`가 순수 함수라 모듈화해도 emit 순서만 보존하면 출력 불변. codeblock 메서드는 이번 범위 제외.
|
||||
|
||||
**Tech Stack:** Node ESM. 검증 = **바이트 동일 재생성**(git diff 빈 결과) + 미러 `node --test`.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 검증 게이트 (모든 Task 공통)
|
||||
|
||||
각 추출 후 반드시:
|
||||
```
|
||||
node tools/deck/gen-slaydeck.mjs # 성공 메시지 1줄, throw 없음
|
||||
git status --short
|
||||
```
|
||||
**합격 기준**: `ui/DefaultGroup.ui`·`RootDesk/MyDesk/SlayDeckController.codeblock`이 **변경 안 됨**(git status에 안 뜸). `Global/common.gamelogic`만 ` M`이면 LF churn → `git checkout -- Global/common.gamelogic`.
|
||||
- 만약 ui/codeblock이 ` M`로 뜨면 **추출 중 실수**(참조 누락/순서 변경) → `git diff --stat`로 어느 산출물인지 보고 되돌려 원인 수정. (RULES상 산출물 content는 안 봄 — 소스 diff로 원인 파악.)
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조 (목표)
|
||||
```
|
||||
tools/deck/
|
||||
gen-slaydeck.mjs # 오케스트레이터: import → 데이터 로드(lib) → upsertUi(hud 호출) → writeCodeblocks → patchCommon
|
||||
lib/
|
||||
data.mjs # 데이터 로드·검증·luaXxxTable·frameRuid·게임상수
|
||||
ui-helpers.mjs # guid/transform/sprite/button/text/entity/scrollLayoutGroup/cardFaceLayout/applySortingOverride
|
||||
# + UI 상수 + uiPath/sectionRoot/isGeneratedUiEntity/appendUiSection
|
||||
hud/
|
||||
deckhud.mjs deckinspect.mjs deckall.mjs combat.mjs reward.mjs map.mjs
|
||||
shop.mjs rest.mjs treasure.mjs jobchoice.mjs jobselect.mjs mainmenu.mjs
|
||||
charselect.mjs lobby.mjs board.mjs soulshop.mjs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `lib/data.mjs` — 데이터·게임상수·lua 테이블 추출
|
||||
|
||||
**Files:** Create `tools/deck/lib/data.mjs`; Modify `tools/deck/gen-slaydeck.mjs`(상단 데이터/lua 블록 → import)
|
||||
|
||||
- [ ] **Step 1:** `lib/data.mjs` 생성. gen-slaydeck.mjs에서 아래를 **잘라 이동**(정의 본문 그대로):
|
||||
- 데이터 로드+검증: `CARDS`(:3) `ENEMIES`(:4) `CLASSES`(:7~17) `JOBS`(:19~40) `SOUL_UNLOCKS`(:42~47) `CARDFRAMES`+검증(:57~68) `RARITIES`(:58) `NODEICONS`+검증(:92~96) `CHARS`+검증(:99~103) `CAM`(:105) `RELICS`+검증(:107~) `POTIONS`+검증(:118~)
|
||||
- 게임 상수: `MAP_ROWS`(:84) `MAP_COLS`(:85) `CHEST_CLOSED_RUID`(:88) `CHEST_OPEN_RUID`(:89)
|
||||
- 함수: `luaSoulShopTable`(:48) `frameRuid`(:69) `luaFramesTable`(:72) `luaNodeIconsTable`(:78) `luaRelicsTable` `luaPotionsTable` `luaIntentsArray` `luaEnemiesTable` `luaStr` `luaJobsTable` `luaCardsTable` `luaDeckTable`
|
||||
- 맨 위 `import { readFileSync } from 'node:fs';`, 맨 끝 `export { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, NODEICONS, CHARS, CAM, RELICS, POTIONS, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable };`
|
||||
- ⚠️ `luaNodeIconsTable`는 `luaStr`를 쓰므로 `luaStr`도 같이 이동. `luaFramesTable`도 `luaStr` 사용. 상호 참조는 같은 모듈 내라 OK.
|
||||
|
||||
- [ ] **Step 2:** gen-slaydeck.mjs 상단에 `import { CARDS, ENEMIES, ... , luaDeckTable } from './lib/data.mjs';` 추가(이동한 정의 위치에).
|
||||
|
||||
- [ ] **Step 3:** 검증 게이트 실행 → ui/codeblock 0 변경 확인 → common.gamelogic churn 복원.
|
||||
|
||||
- [ ] **Step 4:** 커밋
|
||||
```
|
||||
git add tools/deck/lib/data.mjs tools/deck/gen-slaydeck.mjs
|
||||
git commit -m "refactor(gen): lib/data.mjs로 데이터·lua 테이블 추출 (출력 불변)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `lib/ui-helpers.mjs` — UI 헬퍼·상수 추출
|
||||
|
||||
**Files:** Create `tools/deck/lib/ui-helpers.mjs`; Modify `tools/deck/gen-slaydeck.mjs`
|
||||
|
||||
- [ ] **Step 1:** `lib/ui-helpers.mjs` 생성. gen-slaydeck.mjs에서 이동:
|
||||
- UI 상수: `UI_FILE`(:190) `COMMON_FILE`(:191) `UI_ROOT`(:192) `GENERATED_UI_SECTIONS`(:193) `UI_APPEND_ORDER`(:211) `DISABLED_STOCK_CONTROLS`(:229) `TRANSPARENT DARK GOLD ATTACK DEFEND SKILL`(:231~236) `DAMAGE_DIGIT_RUIDS`(:237) `DAMAGE_POP_*`(:249~252) `MAX_MONSTERS`(:254) `HEAD_OFFSET_Y`(:255) `HP_BAR_W`(:257) `WHITE`(:258) `CARD_NAME_TEXT CARD_DESC_TEXT`(:259~260) `CARD_W CARD_H CARD_SPACING CARD_XS`(:276~279) `ALIGN_CENTER ALIGN_BOTTOM_CENTER`(:281~282)
|
||||
- 헬퍼: `cardFaceLayout`(:264) `guid`(:284) `transform`(:292) `sprite`(:317) `button`(:353) `text`(:378) `scrollLayoutGroup`(:405) `popupLayerFor`(:437) `uiOrderFor`(:443) `displayOrderFor`(:452) `applySortingOverride`(:456) `entity`(:472) `uiPath`(:504) `sectionRoot`(:508) `isGeneratedUiEntity`(:512) `appendUiSection`(:516)
|
||||
- `export { ... }` 전부.
|
||||
- ⚠️ COMMON_FILE은 patchCommon(:6125)도 사용 → export 필요. UI_APPEND_ORDER·GENERATED_UI_SECTIONS는 upsertUi가 사용.
|
||||
|
||||
- [ ] **Step 2:** gen-slaydeck.mjs에 `import { ... } from './lib/ui-helpers.mjs';` 추가.
|
||||
|
||||
- [ ] **Step 3:** 검증 게이트 → 0 변경 → churn 복원.
|
||||
|
||||
- [ ] **Step 4:** 커밋 `git commit -m "refactor(gen): lib/ui-helpers.mjs로 UI 헬퍼·상수 추출 (출력 불변)"`
|
||||
|
||||
---
|
||||
|
||||
### HUD 추출 공통 레시피 (Task 3~6에 반복 적용)
|
||||
|
||||
각 HUD는 현재 `upsertUi()` 안에서 `const <v> = []; const add = (e) => <v>.push(e); add(entity(...)); …; emit('<Name>', <v>);` 형태다. 추출 절차:
|
||||
1. `tools/deck/hud/<name>.mjs` 생성: `import { guid, entity, transform, sprite, button, text, GOLD, ... } from '../lib/ui-helpers.mjs';` + 필요한 데이터는 `'../lib/data.mjs'`. `export function build<Name>() { const e = []; const add = (x)=>e.push(x); add(entity(...)); …; return e; }` — **본문은 기존 라인 그대로 이동**(emit 호출 줄만 제외).
|
||||
2. upsertUi에서 해당 블록을 `emit('<Name>', build<Name>());` 한 줄로 치환.
|
||||
3. **검증 게이트**(ui/codeblock 0 변경) → churn 복원 → 커밋.
|
||||
4. ⚠️ 옮긴 블록이 upsertUi 지역변수(`byPath`/`ui`/`cards`/`previewIds`)를 참조하면 안 됨(HUD 섹션은 헬퍼·데이터만 씀이 확인됨). 참조 시 바이트 diff로 즉시 드러남 → 그 변수를 인자로 받도록 조정.
|
||||
|
||||
추출 대상(순서·소스 라인·emit명·모듈):
|
||||
|
||||
| 모듈 | emit | 소스(블록 시작~emit줄) |
|
||||
|---|---|---|
|
||||
| `hud/deckhud.mjs` → `buildDeckHud` | DeckHud | `:693`(`const hud=[]`)~`:808` |
|
||||
| `hud/deckinspect.mjs` → `buildDeckInspect` | DeckInspectHud | `:810`~`:942` |
|
||||
| `hud/deckall.mjs` → `buildDeckAll` | DeckAllHud | `:944`~`:1097` |
|
||||
| `hud/combat.mjs` → `buildCombat` | CombatHud | `:1100`~`:1587` |
|
||||
| `hud/reward.mjs` → `buildReward` | RewardHud | `:1589`~`:1681` |
|
||||
| `hud/map.mjs` → `buildMap` | MapHud | `:1684`~`:1839` |
|
||||
| `hud/shop.mjs` → `buildShop` | ShopHud | `:1841`~`:2038` |
|
||||
| `hud/rest.mjs` → `buildRest` | RestHud | `:2040`~`:2095` |
|
||||
| `hud/treasure.mjs` → `buildTreasure` | TreasureHud | `:2098`~`:2181` |
|
||||
| `hud/jobchoice.mjs` → `buildJobChoice` | JobChoiceHud | `:2184`~`:2229` |
|
||||
| `hud/jobselect.mjs` → `buildJobSelect` | JobSelectHud | `:2231`~`:2314` |
|
||||
| `hud/mainmenu.mjs` → `buildMainMenu` | MainMenu | `:2316`~`:2616` |
|
||||
| `hud/charselect.mjs` → `buildCharSelect` | CharacterSelectHud | `:2437`~`:2617`(`select[0]…enable=false` 포함) |
|
||||
| `hud/lobby.mjs` → `buildLobby` | LobbyHud | `:2620`~`:2672` |
|
||||
| `hud/board.mjs` → `buildBoard` | BoardHud | `:2675`~`:2727` |
|
||||
| `hud/soulshop.mjs` → `buildSoulShop` | SoulShopHud | `:2729`~`:2814` |
|
||||
|
||||
⚠️ MainMenu/CharacterSelectHud는 `const menu=[]`(:2316)·`const select=[]`(:2437)로 인접 정의 후 `emit('MainMenu', menu); emit('CharacterSelectHud', select);`가 :2616~2617에 연속. 각각 별 모듈로 분리, emit 두 줄로. `select[0].jsonString.enable=false`(:2596)는 buildCharSelect 내부에서 `e[0].jsonString.enable=false`로.
|
||||
⚠️ **CardHand 스톡카드 in-place upsert(`:557~691`)는 추출 안 함** — 기존 .ui 엔티티를 변형하는 특수 로직이라 upsertUi에 잔류(import만 정리).
|
||||
|
||||
---
|
||||
|
||||
### Task 3: HUD 추출 배치 A (말단부터 — 위험 낮은 순)
|
||||
**Files:** Create `hud/soulshop.mjs board.mjs lobby.mjs charselect.mjs`; Modify gen-slaydeck.mjs
|
||||
|
||||
- [ ] **Step 1~4:** 레시피로 **soulshop → board → lobby → charselect** 순서로 하나씩 추출·검증·커밋(HUD 1개당 커밋 1개 권장, 배치로 묶어도 무방). 각 추출 후 검증 게이트 통과 필수.
|
||||
- charselect는 P15/이번 작업으로 검증된 화면이라 패턴 안정. `e[0].jsonString.enable=false` 처리 확인.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: HUD 추출 배치 B
|
||||
**Files:** Create `hud/mainmenu.mjs jobselect.mjs jobchoice.mjs treasure.mjs rest.mjs`; Modify gen-slaydeck.mjs
|
||||
|
||||
- [ ] **Step 1~4:** 레시피로 **mainmenu → jobselect → jobchoice → treasure → rest** 추출·검증·커밋.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: HUD 추출 배치 C
|
||||
**Files:** Create `hud/shop.mjs map.mjs reward.mjs`; Modify gen-slaydeck.mjs
|
||||
|
||||
- [ ] **Step 1~4:** 레시피로 **shop → map → reward** 추출·검증·커밋.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: HUD 추출 배치 D (대형 — 마지막)
|
||||
**Files:** Create `hud/combat.mjs deckall.mjs deckinspect.mjs deckhud.mjs`; Modify gen-slaydeck.mjs
|
||||
|
||||
- [ ] **Step 1~4:** 레시피로 **deckhud → deckinspect → deckall → combat** 추출·검증·커밋. combat(~487줄)이 가장 크니 마지막. 추출 후 upsertUi는 데이터 준비 + CardHand upsert + `emit('X', buildX())` 16줄 + 병합만 남아야 함.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 마무리 — RULES 동기화 + 회귀 + PR
|
||||
|
||||
**Files:** Modify `RULES.md`
|
||||
|
||||
- [ ] **Step 1:** RULES.md §1 보조 생성기/단일소스 표에 반영: `tools/deck/gen-slaydeck.mjs`(오케스트레이터)·`tools/deck/lib/*.mjs`(공유)·`tools/deck/hud/*.mjs`(HUD별)가 함께 `ui/DefaultGroup.ui`·`SlayDeckController.codeblock`의 단일 소스임을 명시.
|
||||
|
||||
- [ ] **Step 2:** 회귀 테스트(무영향 확인)
|
||||
```
|
||||
node --test tools/balance/sim-balance.test.mjs
|
||||
node --test tools/map/rogue-map.test.mjs
|
||||
```
|
||||
Expected: 전부 pass(37/0, 9/0).
|
||||
|
||||
- [ ] **Step 3:** 최종 재생성 + 전체 검증 게이트 → ui/codeblock 0 변경(누적) 최종 확인. `git status --short`에 산출물 변경 없음.
|
||||
|
||||
- [ ] **Step 4:** RULES 커밋 → push → PR(`node tools/git/gitea-pr.mjs create <spec.json>`, UTF-8).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **스펙 커버리지**: lib/data·ui-helpers(T1,T2) · HUD 16종 모듈화(T3~6) · 바이트 동일 게이트(공통 게이트, 매 Task) · codeblock 제외(범위 명시) · RULES 동기화(T7) · 미러 회귀(T7). 누락 없음.
|
||||
- **플레이스홀더**: 이동 대상은 라인 범위로 구체 지정, 검증 명령·합격기준 명시, export/import 목록 구체. "본문 그대로 이동"은 리팩터 특성상 코드 재타이핑 대신 정확한 소스 위치 지정(바이트 검증이 정확성 보장).
|
||||
- **타입 일관성**: build 함수명·emit명·모듈 경로 표로 고정. data.mjs/ui-helpers.mjs export ↔ gen-slaydeck import 일치.
|
||||
- **리스크**: 상수/헬퍼 참조 누락 → 바이트 diff 또는 throw로 즉시 노출. 증분 추출로 실패 범위 최소화. 단방향 의존(orchestrator→hud→lib)로 순환 없음.
|
||||
@@ -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 잔류) |
|
||||
@@ -0,0 +1,69 @@
|
||||
# Phase 1b — codeblock 메서드 모듈화 설계
|
||||
|
||||
작성일: 2026-06-16
|
||||
브랜치: `feature/cb-modularization` (Phase 1 `feature/gen-modularization`/PR #70 위에 스택)
|
||||
|
||||
## 목표
|
||||
|
||||
Phase 1에서 UI emit을 모듈화한 데 이어, `gen-slaydeck.mjs`의 **codeblock 메서드 161개(~3,200줄)**를 기능별 모듈로 분리한다. 출력 `RootDesk/MyDesk/SlayDeckController.codeblock`은 **바이트 동일**(순수 소스 리팩터·무위험). 하이브리드 UI 로드맵 (a) 유지보수 정리의 완결.
|
||||
|
||||
## 현재 구조 (조사 결과)
|
||||
|
||||
- `writeCodeblocks()`(현 `gen-slaydeck.mjs:291`)가 단일 호출로 codeblock을 만든다:
|
||||
`const combat = codeblock('SlayDeckController', 'SlayDeckController', [<prop 103개>], [<method 161개>])` (`:301`~`:3617`, ~3,300줄).
|
||||
- 헬퍼: `prop()`·`method()`·`codeblock()` (현 `:240`~`:290` 부근, 오케스트레이터 잔류분).
|
||||
- `writeCodeblocks` 지역 상수: `RUN_LENGTH`·`GOLD_PER_WIN`·`CARD_PRICE`·`REST_HEAL`·`RELIC_PRICE`·`ACT_COUNT`·`ACT_MAPS`·`LOBBY_MAP`·`LOBBY_SPAWN` 등 — 메서드 Lua 문자열 보간에 쓰임.
|
||||
- 메서드 본문은 **Lua 문자열**. JS 보간(`${RUN_LENGTH}`·`${luaCardsTable(CARDS.cards)}`·`${CAM.zoomRatio}` 등)은 모듈 로드 시점에 평가됨 → 모듈은 보간에 쓰는 상수/데이터/헬퍼를 import해야 한다.
|
||||
- 161 메서드 이름(순서): OnBeginPlay → [ascension 10종] → HideGameHud·ShowState·ShowMainMenu·BindMenuButtons·ShowLobby… → [soul 12종] → [character/job] → StartRun·StartCombat·[combat 다수] → [deck/hand] → [deckview] → [motion] → [relics/potions] → [tooltip] → [reward] → [map] → [shop/rest/treasure]. 자연스러운 **연속 런(run)**으로 묶임.
|
||||
|
||||
## 상세 설계
|
||||
|
||||
### 핵심 제약: 바이트 동일 → 메서드 순서 보존
|
||||
`codeblock`의 methods 배열은 **순서가 직렬화에 반영**된다. 따라서 모듈은 "기능 버킷"이 아니라 **원본 161-메서드 시퀀스의 연속 구간**으로 나눈다(구간을 그 테마로 명명). `writeCodeblocks`가 모듈 배열을 **원본 순서대로 concat** → 바이트 동일. (Phase 1 HUD 분리와 동일 원리: HUD도 upsertUi 내 연속이었음.)
|
||||
|
||||
### 목표 파일 구조
|
||||
```
|
||||
tools/deck/
|
||||
gen-slaydeck.mjs # 오케스트레이터: writeCodeblocks()가 codeblock(…, [props], [...m1, ...m2, …]) concat
|
||||
lib/
|
||||
codeblock.mjs # 신설 — prop()·method()·codeblock() 헬퍼 + writeCodeblocks 지역 상수
|
||||
# (RUN_LENGTH·GOLD_PER_WIN·CARD_PRICE·REST_HEAL·RELIC_PRICE·ACT_COUNT·ACT_MAPS·LOBBY_MAP·LOBBY_SPAWN …)
|
||||
cb/ # 신설 — 메서드 연속구간 모듈 (각 `export const xMethods = [ method(...), … ]`)
|
||||
state.mjs ascension.mjs soul.mjs jobs.mjs run.mjs combat.mjs
|
||||
deck.mjs deckview.mjs motion.mjs items.mjs tooltip.mjs reward.mjs shop.mjs … (~12-14, 실제 런 경계로 확정)
|
||||
```
|
||||
|
||||
### 모듈 계약
|
||||
- 각 `cb/<name>.mjs`: `export const <name>Methods = [ method('A', \`…\`, …), method('B', …), … ];` — **메서드 호출 verbatim 이동**.
|
||||
- import: `lib/codeblock.mjs`(method·prop·codeblock·상수), `lib/data.mjs`(CARDS·luaCardsTable·luaStr·CAM 등 보간용). UI 헬퍼는 메서드 보간에 거의 안 쓰임(필요 구간만 `lib/ui-helpers.mjs`).
|
||||
- `writeCodeblocks()`(오케스트레이터): `codeblock('SlayDeckController','SlayDeckController', [ ...props ], [ ...stateMethods, ...ascensionMethods, … ])` — concat 순서 = 원본 순서.
|
||||
|
||||
### 범위/결정
|
||||
- **메서드 161개만 모듈화.** **prop 103개는 오케스트레이터에 단일 리스트로 유지** — 한 줄짜리라 분리 가치 낮고 prop↔feature 매핑 모호(추후 필요시 별도). 게임 로직·Lua **무변경**(순수 소스 리팩터).
|
||||
- 공유 헬퍼(method/prop/codeblock) + writeCodeblocks 지역 상수 → `lib/codeblock.mjs`. (이 상수들이 메서드 모듈 보간에 필요하므로 lib로.)
|
||||
|
||||
### 검증 (안전망)
|
||||
- 구간 추출마다 `node tools/deck/gen-slaydeck.mjs` → `node tools/verify/diffcheck.mjs` → `SlayDeckController.codeblock` **IDENTICAL**(`ui`·`common` 무영향이나 함께 확인). 증분(구간 1~2개씩) + 커밋.
|
||||
- 미러 테스트 `sim-balance`·`rogue-map` 무영향(회귀 확인차 실행).
|
||||
- 전투규칙·맵생성 Lua 미변경 → 미러 동기화 불필요.
|
||||
|
||||
### 미러/하네스
|
||||
- RULES §1의 gen-slaydeck 단일소스에 `cb/`·`lib/codeblock.mjs` 추가 반영.
|
||||
|
||||
## 범위 밖
|
||||
- prop 모듈화(추후).
|
||||
- Phase 2(메이커 UIGroup 파일럿).
|
||||
- 게임 동작·데이터 변경.
|
||||
|
||||
## 리스크
|
||||
- 메서드가 writeCodeblocks **지역변수/다른 메서드 정의를 JS레벨로 참조**하면(드묾 — 대부분 Lua 문자열 내 `self:Method()` 런타임 호출이라 JS-무관) 추출 시 undefined → diffcheck/throw로 즉시 노출 → 그 구간만 인자/상수 조정.
|
||||
- 모듈 import는 ui-helpers처럼 export 이름 자동 파생로 누락 방지. 단방향 의존 orchestrator→cb→lib(순환 없음).
|
||||
|
||||
## 변경 파일 요약
|
||||
| 파일 | 변경 |
|
||||
|---|---|
|
||||
| `tools/deck/lib/codeblock.mjs` | **신설** — prop/method/codeblock 헬퍼 + 공유 상수 |
|
||||
| `tools/deck/cb/*.mjs` (~12-14) | **신설** — 메서드 연속구간 모듈 |
|
||||
| `tools/deck/gen-slaydeck.mjs` | writeCodeblocks를 import+concat로 축소(메서드 본문 → 모듈) |
|
||||
| `RULES.md` | §1에 cb/·lib/codeblock 반영 |
|
||||
| `SlayDeckController.codeblock`·`ui`·`common` | **무변경**(바이트 동일이 합격 기준) |
|
||||
@@ -0,0 +1,80 @@
|
||||
# 생성기 모듈화 (Phase 1) + 하이브리드 UI 로드맵 설계
|
||||
|
||||
작성일: 2026-06-16
|
||||
브랜치: `feature/gen-modularization`
|
||||
|
||||
## 배경 / 동기
|
||||
|
||||
`DefaultGroup.ui`에 모든 UI(캐릭터 선택·상점·전체 덱·전투…)가 들어 있어, 사용자가 (a) **생성기 코드 유지보수**와 (b) **메이커에서 기능별 시각 편집**을 원함.
|
||||
|
||||
핵심 제약(브레인스토밍에서 확정): MSW에서 "Layer"는 렌더 z순서일 뿐 논리 분리 도구가 아니고, 실제 UI 그룹 단위는 **UIGroup**(`.ui` 파일 = UIGroup, 현재 Default/Popup/Toast 3개). 그리고 **같은 UI를 '생성'과 '메이커 수동 편집' 둘 다로 둘 수 없음**(재생성이 수동 편집을 덮어씀).
|
||||
|
||||
→ **소유 모델 = 하이브리드·단계적**(사용자 승인): 정적 레이아웃은 메이커 저작(시각 편집), 동적 내용은 컨트롤러가 런타임 주입. 단, 한 번에 안 하고 단계적으로.
|
||||
|
||||
## 로드맵 (단계적 — 각 Phase가 자체 spec→plan)
|
||||
|
||||
- **Phase 1 (이 문서)**: `gen-slaydeck.mjs`(~6,200줄)의 **UI emit을 기능별 모듈로 분리**. 출력 `.ui`/`codeblock` **바이트 동일**(순수 리팩터·무위험). (a) 충족 + (b) 토대(화면별 파일).
|
||||
- **Phase 2 (후속 spec)**: 화면 1개(**캐릭터 선택**) 파일럿 — 정적 레이아웃을 메이커 저작 UIGroup으로 이관, 생성기는 그 화면 emit 중단, `SlayDeckController`가 경로로 내용(이미지·텍스트) 주입. (b) 패턴 검증.
|
||||
- **Phase 3 (후속 spec)**: 검증되면 상점·전체덱 등으로 확장.
|
||||
|
||||
## 현재 구조 (조사 결과)
|
||||
|
||||
- **공유 인프라**(~48–530): `luaSoulShopTable`/`luaFramesTable`/`luaNodeIconsTable`/`luaRelicsTable`/`luaPotionsTable`/`luaIntentsArray`/`luaEnemiesTable`/`luaStr`/`luaJobsTable`/`luaCardsTable`/`luaDeckTable`/`frameRuid`/`cardFaceLayout`/`guid`/`transform`/`sprite`/`button`/`text`/`scrollLayoutGroup`/`entity`/`uiPath`/`sectionRoot`/`isGeneratedUiEntity`/`appendUiSection`. 데이터 로드 상수(CARDS/CHARS/ENEMIES/RELICS/POTIONS/CARDFRAMES/NODEICONS/CAM) 및 색·치수 상수(GOLD/WHITE/TRANSPARENT/ALIGN_*/CARD_W/CARD_H 등).
|
||||
- **`guid(prefix, n)`은 순수 함수**(`:284`, 내부 카운터 없음; ns는 prefix→바이트 매핑). **모듈 호출 순서와 무관하게 동일 guid** → 분리해도 바이트 동일.
|
||||
- **`upsertUi()`**(`:529`)가 UI 오케스트레이터: 기존 `DefaultGroup.ui` 로드 → 생성 섹션 필터(stock 보존) → 로컬 `emit(section, entities)` 클로저로 누적 → CardHand 스톡카드 in-place upsert(`:565–691`, 특수) → HUD별 `const x=[]; const add=…; add(entity(...)); …; emit('X', x)` → (말미) 병합·기록.
|
||||
- **HUD emit 16종(순서·라인)**: DeckHud(`:808`) · DeckInspectHud(`:942`) · DeckAllHud(`:1097`) · CombatHud(`:1587`) · RewardHud(`:1681`) · MapHud(`:1839`) · ShopHud(`:2038`) · RestHud(`:2095`) · TreasureHud(`:2181`) · JobChoiceHud(`:2229`) · JobSelectHud(`:2314`) · MainMenu(`:2616`) · CharacterSelectHud(`:2617`) · LobbyHud(`:2672`) · BoardHud(`:2727`) · SoulShopHud(`:2814`). **각 섹션은 서로의 지역변수 비참조**(헬퍼·데이터 상수만 사용).
|
||||
- **codeblock 메서드**(`prop`/`method`/`codeblock`/`writeCodeblocks` `:2836–6124`, ~3,200줄) + **patchCommon**(`:6125`). **Phase 1 범위 제외.**
|
||||
|
||||
## Phase 1 상세 설계
|
||||
|
||||
### 목표 파일 구조
|
||||
```
|
||||
tools/deck/
|
||||
gen-slaydeck.mjs # 오케스트레이터(축소): import lib+hud → 데이터 로드 → upsertUi(HUD 모듈 순차) → writeCodeblocks → patchCommon
|
||||
lib/
|
||||
ui-helpers.mjs # guid, transform, sprite, button, text, entity, scrollLayoutGroup,
|
||||
# 상수(GOLD/WHITE/TRANSPARENT/ALIGN_*/CARD_W/CARD_H/UI_ROOT 등), cardFaceLayout,
|
||||
# uiPath/sectionRoot/isGeneratedUiEntity/appendUiSection
|
||||
data.mjs # CARDS/CHARS/ENEMIES/RELICS/POTIONS/CARDFRAMES/NODEICONS/CAM 로드·검증
|
||||
# + luaXxxTable·frameRuid
|
||||
hud/
|
||||
deckhud.mjs deckinspect.mjs deckall.mjs combat.mjs reward.mjs map.mjs
|
||||
shop.mjs rest.mjs treasure.mjs jobchoice.mjs jobselect.mjs mainmenu.mjs
|
||||
charselect.mjs lobby.mjs board.mjs soulshop.mjs
|
||||
```
|
||||
|
||||
### 모듈 계약
|
||||
- 각 `hud/<name>.mjs`: `export function build<Name>()` → 자기 HUD 엔티티 배열 반환. 필요한 헬퍼·상수·데이터는 `lib/`에서 **import**(거대 deps 객체 전달 금지).
|
||||
- `upsertUi()`(오케스트레이터에 잔류)는 기존 **순서 그대로** `emit('DeckHud', buildDeckHud())` … `emit('SoulShopHud', buildSoulShop())` 호출. `emit`·섹션 병합 로직 불변.
|
||||
- **CardHand 스톡카드 in-place upsert**(`:565–691`)는 기존 `.ui` 엔티티를 변형하는 특수 로직 → 오케스트레이터(또는 `hud/cardhand.mjs`)에 그대로 유지. import 경계만 정리.
|
||||
|
||||
### 바이트 동일 불변식 (가장 중요)
|
||||
- 리팩터는 **출력 변경 0**이 목표. 보장 근거: guid 순수·emit 순서·entity 구성 모두 보존, 로직 이동만.
|
||||
- **합격 기준**: 리팩터 후 `node tools/deck/gen-slaydeck.mjs` → `git diff` 결과가 **`ui/DefaultGroup.ui`·`SlayDeckController.codeblock`에 0 변경**(`Global/common.gamelogic`은 LF churn만 허용 → `git checkout`).
|
||||
|
||||
### 증분 실행 전략
|
||||
- 한 번에 16개 다 옮기지 말고 **HUD 1~2개씩 추출 → 재생성 → `git diff` 빈 결과 확인 → 커밋** 반복. 첫 추출(예: SoulShopHud 같은 말단 + lib 골격) 성공 후 패턴 반복.
|
||||
- lib 추출(헬퍼·상수·데이터) 먼저 → 그 다음 HUD 모듈을 하나씩 lib import로 전환.
|
||||
|
||||
### 미러/테스트·하네스
|
||||
- 전투규칙·맵생성 Lua **무변경** → `sim-balance`/`rogue-map` 미러 동기화 불필요(회귀 확인차 `node --test` 실행).
|
||||
- **RULES 동기화**: 생성기가 다중 파일이 되므로 RULES §1 "단일 소스"/보조 생성기 표를 `tools/deck/`(gen-slaydeck + lib/ + hud/)로 갱신.
|
||||
|
||||
## 범위 밖 (명시)
|
||||
- codeblock 메서드(`method()` ~3,200줄) 분리 — 더 크고 (b)와 무관·리스크↑. 원하면 별도 **Phase 1b** spec.
|
||||
- 게임 동작·데이터·런타임 로직 변경. (순수 소스 리팩터)
|
||||
- UIGroup 분할·메이커 저작 이관 — Phase 2 이후.
|
||||
|
||||
## 리스크
|
||||
- 클로저 참조(헬퍼/상수)를 import로 전환하는 광범위·기계적 수정 — 누락 시 런타임 throw 또는 출력 diff로 **즉시** 노출(바이트 검증이 안전망).
|
||||
- 상수 정의 위치 산재(CARD_W·GOLD·UI_ROOT 등 top-level) — lib로 이동 시 누락 주의. 추출 전 `grep`으로 전체 상수 인벤토리 작성.
|
||||
- ESM 순환 import 주의(lib는 hud를 import하지 않음 — 단방향: orchestrator→hud→lib).
|
||||
|
||||
## 변경 파일 요약
|
||||
| 파일 | 변경 |
|
||||
|---|---|
|
||||
| `tools/deck/lib/ui-helpers.mjs`, `lib/data.mjs` | **신설** — 공유 헬퍼·상수·데이터 |
|
||||
| `tools/deck/hud/*.mjs` (16) | **신설** — HUD별 build 함수 |
|
||||
| `tools/deck/gen-slaydeck.mjs` | 오케스트레이터로 축소(데이터/UI emit 본문 → 모듈로 이동, import·호출만) |
|
||||
| `RULES.md` | §1 보조 생성기/단일소스 표에 lib/·hud/ 반영 |
|
||||
| `ui/DefaultGroup.ui`·`SlayDeckController.codeblock` | **무변경**(바이트 동일이 합격 기준) |
|
||||
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
1147
map/map01.map
1147
map/map01.map
File diff suppressed because it is too large
Load Diff
295
map/map02.map
295
map/map02.map
@@ -6365,11 +6365,11 @@
|
||||
},
|
||||
{
|
||||
"id": "000009c4-0000-4000-8000-0000000009c4",
|
||||
"path": "/maps/map02/combat_1",
|
||||
"path": "/maps/map02/pig",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "combat_1",
|
||||
"path": "/maps/map02/combat_1",
|
||||
"name": "pig",
|
||||
"path": "/maps/map02/pig",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6379,12 +6379,12 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"entry_id": "monster-pig",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000009c4-0000-4000-8000-0000000009c4",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"modelId": "monster-pig",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
@@ -6409,9 +6409,11 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"hit": "c3cf643b618346c7bfa6574187b396f9",
|
||||
"die": "a88d9b3d60f941e4890dc89a6ccaa8ee"
|
||||
"stand": "528a8638b12f41b8b5781a05360d2949",
|
||||
"move": "8baad61512be4b33b2a0879fec7a266e",
|
||||
"hit": "60e42a918a0342478903cc71adba1dc5",
|
||||
"jump": "c9e27ce6f8344aefba169c5ca6571def",
|
||||
"die": "0644beff80a44ec7acc011ea0961df57"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6421,7 +6423,7 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "d8f014043ce8418f96700c2b6c9ebf6c",
|
||||
"SpriteRUID": "528a8638b12f41b8b5781a05360d2949",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6517,11 +6519,11 @@
|
||||
},
|
||||
{
|
||||
"id": "000009c5-0000-4000-8000-0000000009c5",
|
||||
"path": "/maps/map02/combat_2",
|
||||
"path": "/maps/map02/green_mushroom",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "combat_2",
|
||||
"path": "/maps/map02/combat_2",
|
||||
"name": "green_mushroom",
|
||||
"path": "/maps/map02/green_mushroom",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6531,17 +6533,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"entry_id": "monster-green_mushroom",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000009c5-0000-4000-8000-0000000009c5",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"modelId": "monster-green_mushroom",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3.8,
|
||||
"x": 3.375,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6607,8 +6609,8 @@
|
||||
"y": 0.58
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
"x": 0.00999999,
|
||||
"y": 0.26
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
@@ -6669,11 +6671,11 @@
|
||||
},
|
||||
{
|
||||
"id": "000009c6-0000-4000-8000-0000000009c6",
|
||||
"path": "/maps/map02/combat_3",
|
||||
"path": "/maps/map02/blue_mushroom",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "combat_3",
|
||||
"path": "/maps/map02/combat_3",
|
||||
"name": "blue_mushroom",
|
||||
"path": "/maps/map02/blue_mushroom",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6683,17 +6685,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"entry_id": "monster-blue_mushroom",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000009c6-0000-4000-8000-0000000009c6",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"modelId": "monster-blue_mushroom",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5.2,
|
||||
"x": 4.449999999999999,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6713,9 +6715,11 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "a2204a21d88942b281d2cac6053ffbaa",
|
||||
"hit": "afc08936b8a64b26bc3dd8c03ead1f26",
|
||||
"die": "fc1c6d9ba9bc413ab53b6dbfae3ac45b"
|
||||
"stand": "1a176a7afb114fe7aef2bc58ef2d945b",
|
||||
"move": "8239541953a6457fbe6d35e17f19f0f8",
|
||||
"hit": "7b405108d05741699893a4dc3d715165",
|
||||
"jump": "a7ea0755262242199ae50ab6a3387034",
|
||||
"die": "9e74e807797d442f9c938ca64aa9f4cd"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6725,7 +6729,7 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "a2204a21d88942b281d2cac6053ffbaa",
|
||||
"SpriteRUID": "1a176a7afb114fe7aef2bc58ef2d945b",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6812,7 +6816,7 @@
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "stump",
|
||||
"EnemyId": "blue_mushroom",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
@@ -6821,11 +6825,11 @@
|
||||
},
|
||||
{
|
||||
"id": "000009c7-0000-4000-8000-0000000009c7",
|
||||
"path": "/maps/map02/elite_4",
|
||||
"path": "/maps/map02/orange_mushroom",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "elite_4",
|
||||
"path": "/maps/map02/elite_4",
|
||||
"name": "orange_mushroom",
|
||||
"path": "/maps/map02/orange_mushroom",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6835,17 +6839,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"entry_id": "monster-orange_mushroom",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000009c7-0000-4000-8000-0000000009c7",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"modelId": "monster-orange_mushroom",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 3,
|
||||
"x": 5.5249999999999995,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -6865,9 +6869,11 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "48c10437ae8344a9b2a1d3f36185728f",
|
||||
"hit": "9044063647854f5e9128efcf80e909be",
|
||||
"die": "f414577d18c94cc387c275df4abdbc3b"
|
||||
"move": "573fe938562a4abf91eebf951f21afd5",
|
||||
"stand": "6d381bea1bcb4504b518a1fbfa0904ac",
|
||||
"jump": "59823e146a034e48b8667ebb6f0724b1",
|
||||
"hit": "642ece38d8d449b29ce4479100e37a54",
|
||||
"die": "3c99d6b9b89b4295a9c2749eb02e28e9"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6877,7 +6883,7 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "48c10437ae8344a9b2a1d3f36185728f",
|
||||
"SpriteRUID": "6d381bea1bcb4504b518a1fbfa0904ac",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
@@ -6964,8 +6970,8 @@
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "modified_snail",
|
||||
"Group": "elite"
|
||||
"EnemyId": "orange_mushroom",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6973,11 +6979,11 @@
|
||||
},
|
||||
{
|
||||
"id": "000009c8-0000-4000-8000-0000000009c8",
|
||||
"path": "/maps/map02/elite_5",
|
||||
"path": "/maps/map02/slime",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "elite_5",
|
||||
"path": "/maps/map02/elite_5",
|
||||
"name": "slime",
|
||||
"path": "/maps/map02/slime",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -6987,17 +6993,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"entry_id": "monster-slime",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000009c8-0000-4000-8000-0000000009c8",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"modelId": "monster-slime",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 5,
|
||||
"x": 6.6,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -7017,9 +7023,11 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "ed3908e24d694bb786023fc1ed073489",
|
||||
"hit": "4763c9bebc9245998c9c499b6316aa9f",
|
||||
"die": "b168793b92a844a3a3a6f4ce647a14d2"
|
||||
"stand": "50faf654ee5d479cb2958edce9feaef0",
|
||||
"move": "dc932872543f4a02bf41e977ab79e5ad",
|
||||
"hit": "61c27025a8f14c478f30ede1b49758bc",
|
||||
"jump": "8b89d86b1a9c4c4288650614c6f30e67",
|
||||
"die": "31ecb6c7cbc24599881f00cb01599f09"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -7029,7 +7037,7 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "ed3908e24d694bb786023fc1ed073489",
|
||||
"SpriteRUID": "50faf654ee5d479cb2958edce9feaef0",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
@@ -7113,6 +7121,161 @@
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "slime",
|
||||
"Group": "combat"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000009c9-0000-4000-8000-0000000009c9",
|
||||
"path": "/maps/map02/mushmom",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "mushmom",
|
||||
"path": "/maps/map02/mushmom",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": false,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "///",
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "monster-mushmom",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000009c9-0000-4000-8000-0000000009c9",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "monster-mushmom",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 4.3,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"Scale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "23c38ef3acad4a30ad59120bb939b008",
|
||||
"move": "24d8a3a75f96406ba690ed42d7250b8f",
|
||||
"hit": "c826e36ee89c48bca6aab856aa773f38",
|
||||
"attack": "4d7465e950144dc59c263aad01b14e14",
|
||||
"jump": "b7ddbda71a294141ba134249fc34c7da",
|
||||
"die": "f50664a4524147399359cb90a6f3e80c"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteRendererComponent",
|
||||
"ActionSheet": {},
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "23c38ef3acad4a30ad59120bb939b008",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.RigidbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"RealMoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.MovementComponent",
|
||||
"InputSpeed": 0,
|
||||
"JumpForce": 6,
|
||||
"Enable": false
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.StateComponent",
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 1.2,
|
||||
"y": 1.1
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.02,
|
||||
"y": 0.55
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSpawnerComponent",
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.Monster",
|
||||
"Enable": true,
|
||||
"IsDead": false
|
||||
},
|
||||
{
|
||||
"@type": "script.MonsterAttack",
|
||||
"Enable": true,
|
||||
"SpriteSize": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"PositionOffset": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.KinematicbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SideviewbodyComponent",
|
||||
"MoveVelocity": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.DamageSkinSettingComponent",
|
||||
"DamageSkinId": {
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
@@ -7124,12 +7287,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "000009c9-0000-4000-8000-0000000009c9",
|
||||
"path": "/maps/map02/boss_6",
|
||||
"id": "000009ca-0000-4000-8000-0000000009ca",
|
||||
"path": "/maps/map02/slime_boss",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "boss_6",
|
||||
"path": "/maps/map02/boss_6",
|
||||
"name": "slime_boss",
|
||||
"path": "/maps/map02/slime_boss",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -7139,17 +7302,17 @@
|
||||
"revision": 2,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "ChaseMonster",
|
||||
"entry_id": "monster-slime_boss",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": "000009c9-0000-4000-8000-0000000009c9",
|
||||
"root_entity_id": "000009ca-0000-4000-8000-0000000009ca",
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "chasemonster",
|
||||
"modelId": "monster-slime_boss",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.TransformComponent",
|
||||
"Position": {
|
||||
"x": 4,
|
||||
"x": 4.6,
|
||||
"y": 0.03499998,
|
||||
"z": 999.999
|
||||
},
|
||||
@@ -7169,9 +7332,11 @@
|
||||
{
|
||||
"@type": "MOD.Core.StateAnimationComponent",
|
||||
"ActionSheet": {
|
||||
"stand": "17b55730c26f4fd6b8fcfa288da388de",
|
||||
"hit": "eac48e84a9fc4580a4018de5cf52ddb3",
|
||||
"die": "51c2f4b59a2c413db26035aa57002fc8"
|
||||
"stand": "50faf654ee5d479cb2958edce9feaef0",
|
||||
"move": "dc932872543f4a02bf41e977ab79e5ad",
|
||||
"hit": "61c27025a8f14c478f30ede1b49758bc",
|
||||
"jump": "8b89d86b1a9c4c4288650614c6f30e67",
|
||||
"die": "31ecb6c7cbc24599881f00cb01599f09"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
@@ -7181,7 +7346,7 @@
|
||||
"EndFrameIndex": 0,
|
||||
"RenderSetting": 1,
|
||||
"SortingLayer": "MapLayer0",
|
||||
"SpriteRUID": "17b55730c26f4fd6b8fcfa288da388de",
|
||||
"SpriteRUID": "50faf654ee5d479cb2958edce9feaef0",
|
||||
"StartFrameIndex": 0,
|
||||
"Enable": true
|
||||
},
|
||||
@@ -7211,12 +7376,12 @@
|
||||
{
|
||||
"@type": "MOD.Core.HitComponent",
|
||||
"BoxSize": {
|
||||
"x": 0.63,
|
||||
"y": 0.58
|
||||
"x": 1.2,
|
||||
"y": 1.1
|
||||
},
|
||||
"ColliderOffset": {
|
||||
"x": 0.0449999869,
|
||||
"y": 0.29
|
||||
"y": 0.4
|
||||
},
|
||||
"IsLegacy": false,
|
||||
"Enable": true
|
||||
|
||||
990
map/map03.map
990
map/map03.map
File diff suppressed because it is too large
Load Diff
1256
map/map04.map
1256
map/map04.map
File diff suppressed because it is too large
Load Diff
1416
map/map05.map
1416
map/map05.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)와 동기화 유지할 것.
|
||||
// (데이터는 data/*.json 공유, 규칙 로직은 JS로 중복 재현)
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -27,6 +27,16 @@ export function shuffle(arr, rng) {
|
||||
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(취약)과 동기화.
|
||||
// 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
|
||||
@@ -44,6 +54,11 @@ export function calcAttack(base, str, weak, vulnOnTarget) {
|
||||
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 }
|
||||
export function applyDamage(hp, block, amount) {
|
||||
let dmg = amount;
|
||||
@@ -70,17 +85,64 @@ export function loadData() {
|
||||
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은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
||||
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
|
||||
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
|
||||
export function chooseAction(hand, cards, energy) {
|
||||
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id] && cards[x.id].cost <= energy && !cards[x.id].unplayable);
|
||||
export function chooseAction(hand, cards, energy, ctx = {}) {
|
||||
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 attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
|
||||
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
|
||||
const dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(cards[x.id].cost, 1);
|
||||
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(cards[x.id].cost, 1);
|
||||
const effectiveCost = (x) => {
|
||||
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];
|
||||
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 (attacks.length) return bestBy(attacks, dmgEff).i;
|
||||
if (skills.length) return bestBy(skills, blkEff).i;
|
||||
@@ -105,131 +167,577 @@ function bump(s, cost, dmg, blk) {
|
||||
// 반환: { win, turns, playerHpRemaining, draw? }
|
||||
export function simulateCombat(data, rng, stats) {
|
||||
const { cards, starterDeck, monsters } = data;
|
||||
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: PLAYER_HP };
|
||||
let drawPile = shuffle(starterDeck, rng);
|
||||
const playerMaxHp = data.playerMaxHp || PLAYER_HP;
|
||||
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 = [];
|
||||
const exhaust = [];
|
||||
let hand = [];
|
||||
let pHp = PLAYER_HP, pBlock = 0;
|
||||
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0;
|
||||
let pHp = startingPlayerHp, pBlock = data.playerStartBlock || 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 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,
|
||||
}));
|
||||
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) {
|
||||
const drawn = [];
|
||||
if (drawDisabledThisTurn === true) return drawn;
|
||||
for (let k = 0; k < n; k++) {
|
||||
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
|
||||
if (drawPile.length === 0) break;
|
||||
const card = drawPile.pop();
|
||||
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 동기화)
|
||||
if (hand.length >= 10) {
|
||||
discard.push(card);
|
||||
triggerSly(card);
|
||||
} else hand.push(card);
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
function addCardsToHand(id, n) {
|
||||
for (let k = 0; k < n; k++) {
|
||||
if (hand.length >= 10) discard.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);
|
||||
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) {
|
||||
const alive = aliveList();
|
||||
let dmg = 0;
|
||||
let blockGained = 0;
|
||||
if (c.kind === 'Attack') {
|
||||
if (alive.length && c.damage) {
|
||||
const target = chooseTarget(alive, calcAttack(c.damage || 0, pStr, pWeak, 0));
|
||||
if (c.weak) target.weak += c.weak;
|
||||
if (c.vuln) target.vuln += c.vuln;
|
||||
const hitN = c.hits || 1;
|
||||
let totalNv = 0;
|
||||
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
|
||||
dmg = totalNv;
|
||||
if (c.aoe === true) {
|
||||
for (const m2 of aliveList()) {
|
||||
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
const r2 = applyDamage(m2.hp, m2.block, d2);
|
||||
m2.hp = r2.hp; m2.block = r2.block;
|
||||
if (m2.hp <= 0) m2.alive = false;
|
||||
if (c.blockGainMultiplier && c.blockGainMultiplier > 0) blockGainMultiplier *= c.blockGainMultiplier;
|
||||
if (c.nextSkillCostZero === true) nextSkillCostZero = true;
|
||||
if (c.nextSkillRepeatCount && c.nextSkillRepeatCount > 0) nextSkillRepeatCount += c.nextSkillRepeatCount;
|
||||
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
|
||||
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
|
||||
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
|
||||
if (c.drawDamage && c.kind !== 'Power') drawDamageThisTurn += c.drawDamage;
|
||||
if (c.drawPoison && c.kind !== 'Power') drawPoisonThisTurn += c.drawPoison;
|
||||
if (c.shivAoe === true && c.kind !== 'Power') shivAoeThisCombat = true;
|
||||
if (c.skillSlyOnPlay === true && c.kind === 'Skill') skillSlyOnPlayCards.add(id);
|
||||
if (c.turnHandSlyCount && c.turnHandSlyCount > 0) {
|
||||
let picked = 0;
|
||||
for (const hid of hand) {
|
||||
if (hid === id) continue;
|
||||
const hc = cards[hid];
|
||||
if (hc?.kind === 'Skill' && !turnSkillSlyCards.has(hid) && !skillSlyOnPlayCards.has(hid) && hc.sly !== true) {
|
||||
turnSkillSlyCards.add(hid);
|
||||
picked++;
|
||||
if (picked >= c.turnHandSlyCount) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
if (c.pierce === true) {
|
||||
target.hp -= dmg;
|
||||
target.hp -= dealt;
|
||||
if (target.hp < 0) target.hp = 0;
|
||||
} else {
|
||||
const r = applyDamage(target.hp, target.block, dmg);
|
||||
const r = applyDamage(target.hp, target.block, dealt);
|
||||
target.hp = r.hp; target.block = r.block;
|
||||
}
|
||||
if (target.hp <= 0) target.alive = false;
|
||||
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;
|
||||
}
|
||||
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
|
||||
} else if (c.kind === 'Power') {
|
||||
if (c.powerEffect && recordStats) powers.push(id);
|
||||
} else {
|
||||
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
|
||||
if ((c.weak || c.vuln || c.poison) && alive.length) {
|
||||
const target = chooseTarget(alive, 0);
|
||||
if (c.weak) target.weak += c.weak;
|
||||
if (c.vuln) target.vuln += c.vuln;
|
||||
if (c.poison) target.poison += c.poison;
|
||||
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') {
|
||||
if (recordStats) powers.push(id);
|
||||
} else {
|
||||
if (c.block) blockGained = addBlock(c.block);
|
||||
const weakAmount = (c.weak || 0) + (c.xWeakPerEnergy || 0) * xEnergy;
|
||||
const vulnAmount = c.vuln || 0;
|
||||
if ((weakAmount || vulnAmount || c.poison || c.removeEnemyBlock || c.removeEnemyArtifact || c.enemyStrengthLossThisTurn) && alive.length) {
|
||||
const targets = c.affectsAllEnemies === true ? aliveList() : [chooseTarget(alive, 0)];
|
||||
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.dex) pDex += c.dex;
|
||||
if (c.thorns) pThorns += c.thorns;
|
||||
if (c.selfVuln) pVuln += c.selfVuln;
|
||||
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
||||
if (c.draw) draw(c.draw);
|
||||
if (c.heal) pHp = Math.min(pHp + c.heal, playerMaxHp);
|
||||
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.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);
|
||||
}
|
||||
function triggerSly(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);
|
||||
}
|
||||
function discardHandCard(idx, trigger = true) {
|
||||
const [id] = hand.splice(idx, 1);
|
||||
if (!id) return;
|
||||
discard.push(id);
|
||||
turnDiscardedCards++;
|
||||
if (trigger) triggerSly(id);
|
||||
}
|
||||
function applyDiscardEffects(c) {
|
||||
let discarded = 0;
|
||||
if (c.discardAll) {
|
||||
while (hand.length) discardHandCard(hand.length - 1, true);
|
||||
while (hand.length) { discardHandCard(hand.length - 1, true); discarded++; }
|
||||
} else if (c.discard) {
|
||||
const n = Math.min(c.discard, hand.length);
|
||||
for (let i = 0; i < n; i++) discardHandCard(hand.length - 1, true);
|
||||
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.addShivPerDiscard === true) addCardsToHand('Shiv', discarded);
|
||||
if (c.drawPerDiscarded) draw(discarded * c.drawPerDiscarded);
|
||||
}
|
||||
|
||||
while (turns < MAX_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 파워)
|
||||
pBlock = 0;
|
||||
if (nextTurnKeepBlock === true) nextTurnKeepBlock = false;
|
||||
else pBlock = 0;
|
||||
turnAttackMultiplier = nextTurnAttackMultiplier;
|
||||
nextTurnAttackMultiplier = 1;
|
||||
let energyBonus = 0;
|
||||
let powerTurnDraw = 0;
|
||||
let powerTurnDiscard = 0;
|
||||
for (const pid of powers) {
|
||||
const pc = cards[pid];
|
||||
if (!pc) continue;
|
||||
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
|
||||
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
|
||||
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
|
||||
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;
|
||||
}
|
||||
let energy = ENERGY + energyBonus; draw(HAND_SIZE);
|
||||
}
|
||||
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
|
||||
if (pc.turnStartDraw) powerTurnDraw += pc.turnStartDraw;
|
||||
if (pc.turnStartDiscard) powerTurnDiscard += pc.turnStartDiscard;
|
||||
}
|
||||
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) {
|
||||
const alive = aliveList();
|
||||
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;
|
||||
const id = hand[idx], c = cards[id];
|
||||
energy -= c.cost;
|
||||
resolveCardEffects(id, c, c.cost);
|
||||
let dmg = 0;
|
||||
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);
|
||||
queueSelectedReserve(c);
|
||||
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.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);
|
||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
|
||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
|
||||
}
|
||||
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
|
||||
let burn = 0;
|
||||
@@ -238,10 +746,18 @@ export function simulateCombat(data, rng, stats) {
|
||||
const kept = [];
|
||||
for (const hid of hand) {
|
||||
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);
|
||||
}
|
||||
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 };
|
||||
// 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전)
|
||||
if (pWeak > 0) pWeak--;
|
||||
@@ -249,19 +765,24 @@ export function simulateCombat(data, rng, stats) {
|
||||
for (const m of mob) {
|
||||
if (!m.alive) continue;
|
||||
// 독 틱 — 행동 시작 시 (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.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; // 매 턴 초기화 (이전 턴 블록 미이월)
|
||||
// 정의된 intent 중 랜덤 선택 (Lua EnemyActStep 동기화 — 순차→랜덤)
|
||||
const it = m.intents.length ? m.intents[Math.floor(rng() * m.intents.length)] : null;
|
||||
if (it) {
|
||||
if (it.kind === 'Attack') {
|
||||
const atk = calcAttack(it.value, m.str, m.weak, pVuln);
|
||||
const atk = calcEnemyAttack(it.value, m.str, m.weak, pVuln, enemyStrengthLossThisTurn);
|
||||
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) {
|
||||
m.hp -= pThorns;
|
||||
if (m.hp <= 0) m.alive = false;
|
||||
@@ -282,9 +803,9 @@ export function simulateCombat(data, rng, stats) {
|
||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||
}
|
||||
// 독 사망 등 적 페이즈 중 전멸 처리 (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; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, rarityForRoll,
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, calcEnemyAttack, rarityForRoll,
|
||||
} from './sim-balance.mjs';
|
||||
|
||||
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
||||
@@ -13,6 +13,85 @@ test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
||||
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', () => {
|
||||
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
||||
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
||||
@@ -42,6 +121,14 @@ test('chooseAction: 공격 없으면 스킬 선택', () => {
|
||||
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', () => {
|
||||
const idx = chooseAction(['Bash'], CARDS, 1);
|
||||
assert.equal(idx, -1);
|
||||
@@ -141,6 +228,21 @@ test('simulateCombat: 복합 카드(공격+방어) 블록이 적 공격을 흡
|
||||
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 동기화)', () => {
|
||||
assert.equal(calcAttack(6, 0, 0, 0), 6); // 기본
|
||||
assert.equal(calcAttack(6, 2, 0, 0), 8); // 힘+2
|
||||
@@ -183,6 +285,19 @@ test('simulateCombat: 카드 취약 부여가 같은 카드 피해에 선적용
|
||||
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(매턴 힘) 누적', () => {
|
||||
const data = {
|
||||
cards: {
|
||||
@@ -447,3 +562,656 @@ test("simulateCombat: thorns reflects unblocked attack damage", () => {
|
||||
assert.equal(r.turns, 1);
|
||||
assert.equal(r.playerHpRemaining, 79);
|
||||
});
|
||||
|
||||
test("simulateCombat: addShiv creates shuriken cards in hand", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
MakeShiv: { name: "MakeShiv", cost: 0, kind: "Skill", addShiv: 2 },
|
||||
Shiv: { name: "표창", cost: 0, kind: "Attack", damage: 4, exhaust: true },
|
||||
},
|
||||
starterDeck: ["MakeShiv"],
|
||||
monsters: [{ name: "Dummy", maxHp: 8, intents: [{ kind: "Attack", value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, () => 0.999999);
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
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 }
|
||||
}
|
||||
113
tools/deck/cb/boot.mjs
Normal file
113
tools/deck/cb/boot.mjs
Normal file
@@ -0,0 +1,113 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaCharsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const bootMethods = [
|
||||
method('OnBeginPlay', `${luaCardsTable(CARDS.cards)}
|
||||
${luaFramesTable()}
|
||||
${luaNodeIconsTable()}
|
||||
${luaCharsTable()}
|
||||
${luaSoulShopTable(SOUL_UNLOCKS)}
|
||||
self.SoulUnlocks = {}
|
||||
self.SoulPoints = self.SoulPoints or 0
|
||||
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
|
||||
if lp ~= nil then
|
||||
self:ReqLoadAscension(lp.PlayerComponent.UserId)
|
||||
self:ReqLoadSouls(lp.PlayerComponent.UserId)
|
||||
end
|
||||
_InputService:ConnectEvent(KeyDownEvent, function(e)
|
||||
if e.key == KeyboardKey.LeftControl then
|
||||
self.DebugCtrlDown = true
|
||||
local lp2 = _UserService.LocalPlayer
|
||||
if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then
|
||||
self:PlayerAttackMotion()
|
||||
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)`),
|
||||
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)
|
||||
local errCode, value = ds:GetAndWait("ascensionUnlocked")
|
||||
local n = 0
|
||||
if errCode == 0 and value ~= nil and value ~= "" then
|
||||
n = tonumber(value) or 0
|
||||
end
|
||||
self:RecvAscension(n, userId)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }], 5),
|
||||
method('RecvAscension', `self.AscensionUnlocked = n
|
||||
if self.AscensionLevel > self.AscensionUnlocked then
|
||||
self.AscensionLevel = self.AscensionUnlocked
|
||||
end
|
||||
self:RenderAscension()`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' },
|
||||
], 6),
|
||||
method('SaveAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||
ds:SetAndWait("ascensionUnlocked", tostring(n))`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' },
|
||||
], 5),
|
||||
method('AdjustAscension', `local v = self.AscensionLevel + delta
|
||||
if v < 0 then v = 0 end
|
||||
if v > self.AscensionUnlocked then v = self.AscensionUnlocked end
|
||||
self.AscensionLevel = v
|
||||
self:RenderAscension()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delta' }]),
|
||||
method('RenderAscension', `self:SetText("/ui/DefaultGroup/MainMenu/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))
|
||||
self:SetText("/ui/LobbyUIGroup/LobbyHud/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`),
|
||||
method('AscHpMult', `local m = 1
|
||||
if self.AscensionLevel >= 1 then m = m + 0.1 end
|
||||
if self.AscensionLevel >= 6 then m = m + 0.1 end
|
||||
return m`, [], 0, 'number'),
|
||||
method('AscAtkMult', `local m = 1
|
||||
if self.AscensionLevel >= 2 then m = m + 0.1 end
|
||||
if self.AscensionLevel >= 7 then m = m + 0.1 end
|
||||
return m`, [], 0, 'number'),
|
||||
method('AscEliteBonus', `local b = 0
|
||||
if self.AscensionLevel >= 4 then b = b + 0.2 end
|
||||
if self.AscensionLevel >= 9 then b = b + 0.2 end
|
||||
return b`, [], 0, 'number'),
|
||||
method('AscGoldMult', `local m = 1
|
||||
if self.AscensionLevel >= 5 then m = m - 0.25 end
|
||||
if self.AscensionLevel >= 10 then m = m - 0.25 end
|
||||
return m`, [], 0, 'number'),
|
||||
method('AscStartHpPenalty', `local p = 0
|
||||
if self.AscensionLevel >= 3 then p = p + 10 end
|
||||
if self.AscensionLevel >= 8 then p = p + 10 end
|
||||
return p`, [], 0, 'number'),
|
||||
];
|
||||
80
tools/deck/cb/charselect.mjs
Normal file
80
tools/deck/cb/charselect.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const charSelectMethods = [
|
||||
method('ShowCharacterSelect', `self.SelectedClass = ""
|
||||
self:ShowState("charselect")
|
||||
self:RenderCharacterSelect()`),
|
||||
method('SelectClass', `self.SelectedClass = className
|
||||
self:RenderCharacterSelect()`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' },
|
||||
]),
|
||||
method('RenderCharacterSelect', `local base = "/ui/SelectUIGroup/CharacterSelectHud"
|
||||
local arts = { { p = "/WarriorButton/Art", c = "warrior" }, { p = "/MageButton/Art", c = "magician" }, { p = "/BanditButton/Art", c = "rogue" } }
|
||||
for i = 1, #arts do
|
||||
local e = _EntityService:GetEntityByPath(base .. arts[i].p)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil and self.ClassPortraits ~= nil and self.ClassPortraits[arts[i].c] ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = self.ClassPortraits[arts[i].c]
|
||||
end
|
||||
end
|
||||
local btns = { { p = "/WarriorButton", c = "warrior" }, { p = "/MageButton", c = "magician" }, { p = "/BanditButton", c = "rogue" } }
|
||||
for i = 1, #btns do
|
||||
local e = _EntityService:GetEntityByPath(base .. btns[i].p)
|
||||
if e ~= nil then
|
||||
if e.MaskComponent == nil then
|
||||
e:AddComponent("MaskComponent")
|
||||
end
|
||||
if e.SpriteGUIRendererComponent ~= nil then
|
||||
if self.SelectedClass == btns[i].c then
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.45, 0.5, 0.58, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local nl = string.char(10)
|
||||
local name = ""
|
||||
local eng = ""
|
||||
local desc = "직업을 선택하고 시작하세요"
|
||||
local btnName = ""
|
||||
if self.SelectedClass == "warrior" then
|
||||
name = "전사"
|
||||
eng = "Warrior"
|
||||
btnName = "/WarriorButton"
|
||||
desc = "직업군 · 모험가" .. nl .. "방어를 쌓고 버티다 강하게 역공하는 단단한 탱커."
|
||||
elseif self.SelectedClass == "rogue" then
|
||||
name = "도적"
|
||||
eng = "Rogue"
|
||||
btnName = "/BanditButton"
|
||||
desc = "직업군 · 모험가" .. nl .. "표창 난사와 독으로 빠르게 몰아치는 민첩한 직업."
|
||||
elseif self.SelectedClass == "magician" then
|
||||
name = "법사"
|
||||
eng = "Magician"
|
||||
btnName = "/MageButton"
|
||||
desc = "직업군 · 모험가" .. nl .. "약하지만 게이지 운용으로 화력을 집중하는 원소 마법사."
|
||||
end
|
||||
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
|
||||
end
|
||||
self:StartRun()`),
|
||||
method('SetEntityEnabled', `local e = _EntityService:GetEntityByPath(path)
|
||||
if e ~= nil then
|
||||
e.Enable = enabled
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
|
||||
], 2),
|
||||
];
|
||||
752
tools/deck/cb/combat.mjs
Normal file
752
tools/deck/cb/combat.mjs
Normal file
@@ -0,0 +1,752 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const combatMethods = [
|
||||
method('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
|
||||
self:SelectDiscardSlot(slot)
|
||||
return
|
||||
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
|
||||
return
|
||||
end
|
||||
if self.Hand == nil then
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
return
|
||||
end
|
||||
if c.unplayable == true then
|
||||
self:Toast("사용할 수 없는 카드입니다")
|
||||
return
|
||||
end
|
||||
if self: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("에너지가 부족합니다")
|
||||
return
|
||||
end
|
||||
self.Energy = self.Energy - cost
|
||||
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)
|
||||
if c.exhaust == true then
|
||||
if self.ExhaustPile == nil then self.ExhaustPile = {} end
|
||||
table.insert(self.ExhaustPile, cardId)
|
||||
elseif c.kind ~= "Power" then
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
if self:BeginDiscardSelection(c) == true then
|
||||
return
|
||||
end
|
||||
if self:BeginReserveSelection(c) == true then
|
||||
return
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('OnCardButton', `if self:IsDiscardSelecting() == true then
|
||||
self:SelectDiscardSlot(slot)
|
||||
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' }]),
|
||||
method('FindMonsterAtTouch', `local best = 0
|
||||
local bestDist = 200
|
||||
for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
|
||||
local wp = m.entity.TransformComponent.WorldPosition
|
||||
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
|
||||
local dx = sp.x - touchPoint.x
|
||||
local dy = sp.y - touchPoint.y
|
||||
local d = math.sqrt(dx * dx + dy * dy)
|
||||
if d < bestDist then
|
||||
bestDist = d
|
||||
best = i
|
||||
end
|
||||
end
|
||||
end
|
||||
return best`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }], 0, 'number'),
|
||||
method('RenderTargetFrames', `local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
|
||||
local shownTarget = self.TargetIndex
|
||||
if dragActive == true then shownTarget = self.DragTargetIndex end
|
||||
for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
local active = false
|
||||
if m ~= nil and m.alive == true and i == shownTarget then active = true end
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) .. "/TargetMarker", active and dragActive)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
|
||||
end`),
|
||||
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||
return
|
||||
end
|
||||
if self.Hand == nil or self.Hand[slot] == nil then
|
||||
return
|
||||
end
|
||||
if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
|
||||
_TimerService:ClearTimer(self.CardHoverTweenId)
|
||||
self.CardHoverTweenId = 0
|
||||
end
|
||||
for i = 1, 10 do
|
||||
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
|
||||
end
|
||||
end
|
||||
self.DragSlot = slot
|
||||
self.DragTargetIndex = 0
|
||||
self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('OnCardDrag', `if self.DragSlot ~= slot then
|
||||
return
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
local ui = _UILogic:ScreenToUIPosition(touchPoint)
|
||||
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
local c = nil
|
||||
if cardId ~= nil then c = self.Cards[cardId] end
|
||||
if c ~= nil and c.kind == "Attack" then
|
||||
local best = self:FindMonsterAtTouch(touchPoint)
|
||||
if best ~= self.DragTargetIndex then
|
||||
self.DragTargetIndex = best
|
||||
self:RenderTargetFrames()
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||
]),
|
||||
method('OnCardDragEnd', `if self.DragSlot ~= slot then
|
||||
return
|
||||
end
|
||||
self.DragSlot = 0
|
||||
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
end
|
||||
self:ResolveCardDrop(slot, touchPoint)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||
]),
|
||||
method('ResolveCardDrop', `if self:IsDiscardSelecting() == true then
|
||||
self:SelectDiscardSlot(slot)
|
||||
return
|
||||
end
|
||||
if self: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
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
return
|
||||
end
|
||||
if c.kind == "Attack" then
|
||||
local best = self.DragTargetIndex or 0
|
||||
if best <= 0 then best = self:FindMonsterAtTouch(touchPoint) end
|
||||
self.DragTargetIndex = 0
|
||||
if best > 0 then
|
||||
self.TargetIndex = best
|
||||
self:PlayCard(slot)
|
||||
self:RenderTargetFrames()
|
||||
else
|
||||
self:RenderTargetFrames()
|
||||
end
|
||||
else
|
||||
self.DragTargetIndex = 0
|
||||
self:RenderTargetFrames()
|
||||
local ui = _UILogic:ScreenToUIPosition(touchPoint)
|
||||
if ui.y > -180 then
|
||||
self:PlayCard(slot)
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||
]),
|
||||
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
||||
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
||||
if m == nil or m.alive ~= true then
|
||||
m = nil
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end
|
||||
end
|
||||
end
|
||||
if m == nil then
|
||||
return false
|
||||
end
|
||||
local dmg = amount
|
||||
if m.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
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
|
||||
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
|
||||
local poison = self:AddPowerFieldTotal("attackPoison")
|
||||
if poison ~= nil and poison > 0 then
|
||||
self:ApplyPoisonToMonster(m, poison)
|
||||
end
|
||||
end
|
||||
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' },
|
||||
{ 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]
|
||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||
self:DealDamageToTarget(damage, pierce)
|
||||
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
return
|
||||
end
|
||||
self.FxBusy = true
|
||||
local fx = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/SkillFx")
|
||||
if fx ~= nil then
|
||||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||||
end
|
||||
if fx.UITransformComponent ~= nil and m.entity.TransformComponent ~= nil then
|
||||
local wp = m.entity.TransformComponent.WorldPosition
|
||||
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
|
||||
fx.UITransformComponent.anchoredPosition = _UILogic:ScreenToUIPosition(sp)
|
||||
end
|
||||
fx.Enable = true
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if fx ~= nil then fx.Enable = false end
|
||||
self.FxBusy = false
|
||||
local shown = damage
|
||||
local mt = self.Monsters[targetIndex]
|
||||
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
||||
shown = math.floor(damage * 1.5)
|
||||
end
|
||||
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:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
end, 0.35)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||||
]),
|
||||
method('PlayAoeFx', `self.FxBusy = true
|
||||
local fx = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/SkillFx")
|
||||
if fx ~= nil then
|
||||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||||
end
|
||||
if fx.UITransformComponent ~= nil then
|
||||
fx.UITransformComponent.anchoredPosition = Vector2(300, 60)
|
||||
end
|
||||
fx.Enable = true
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if fx ~= nil then fx.Enable = false end
|
||||
self.FxBusy = false
|
||||
local killCount = 0
|
||||
for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
if m ~= nil and m.alive == true then
|
||||
local dmg = damage
|
||||
if m.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
end
|
||||
if m.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
|
||||
dmg = math.floor(dmg * self.ActiveAttackDamageVsWeakMultiplier)
|
||||
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
|
||||
local poison = self:AddPowerFieldTotal("attackPoison")
|
||||
if poison ~= nil and poison > 0 then
|
||||
self:ApplyPoisonToMonster(m, poison)
|
||||
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.ActiveKillReward = 0
|
||||
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
end, 0.35)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||||
]),
|
||||
method('KillMonster', `local m = self.Monsters[slot]
|
||||
if m == nil then
|
||||
return
|
||||
end
|
||||
m.alive = false
|
||||
if m.entity ~= nil and isvalid(m.entity) then
|
||||
local ent = m.entity
|
||||
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
|
||||
end
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(slot), false)
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('DealDamageToPlayer', `local dmg = amount
|
||||
if self.PlayerBlock > 0 then
|
||||
local absorbed = math.min(self.PlayerBlock, dmg)
|
||||
self.PlayerBlock = self.PlayerBlock - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then
|
||||
dmg = 1
|
||||
end
|
||||
if dmg > 0 then
|
||||
self.PlayerHp = self.PlayerHp - dmg
|
||||
local reflect = self.PlayerThorns or 0
|
||||
if self:HasRelic("bronzeScales") then
|
||||
reflect = reflect + 3
|
||||
end
|
||||
if reflect > 0 and attackerSlot ~= nil and attackerSlot > 0 then
|
||||
local am = self.Monsters[attackerSlot]
|
||||
if am ~= nil and am.alive == true then
|
||||
am.hp = am.hp - reflect
|
||||
self:ShowDmgPop(am.slot, reflect)
|
||||
self:MonsterHitMotion(am.slot)
|
||||
if am.hp <= 0 then
|
||||
am.hp = 0
|
||||
self:KillMonster(am.slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
if self:HasRelic("selfFormingClay") then
|
||||
self.ClayBlockNext = self.ClayBlockNext + 3
|
||||
end
|
||||
if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then
|
||||
self.FirstHpLossDone = true
|
||||
self:DrawCards(3)
|
||||
self:RenderHand(false)
|
||||
end
|
||||
end
|
||||
if self.PlayerHp < 0 then
|
||||
self.PlayerHp = 0
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'attackerSlot' },
|
||||
]),
|
||||
method('EnemyTurn', `self.TurnBusy = true
|
||||
self:EnemyActStep(1)`),
|
||||
method('EnemyActStep', `local idx = 0
|
||||
for i = fromIndex, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then idx = i; break end
|
||||
end
|
||||
if idx == 0 or self.PlayerHp <= 0 then
|
||||
self:FinishEnemyTurn()
|
||||
return
|
||||
end
|
||||
local m = self.Monsters[idx]
|
||||
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(idx)
|
||||
self:SetEntityEnabled(base .. "/ActFrame", true)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
local poisonTicks = 1
|
||||
local bonusTicks = self:AddPowerFieldTotal("extraPoisonTicks")
|
||||
if bonusTicks ~= nil and bonusTicks > 0 then
|
||||
poisonTicks = poisonTicks + bonusTicks
|
||||
end
|
||||
for pt = 1, poisonTicks do
|
||||
if m.poison ~= nil and m.poison > 0 then
|
||||
m.hp = m.hp - m.poison
|
||||
self:ShowDmgPop(idx, m.poison)
|
||||
self:MonsterHitMotion(idx)
|
||||
m.poison = m.poison - 1
|
||||
if m.hp <= 0 then
|
||||
m.hp = 0
|
||||
self:KillMonster(m.slot)
|
||||
self:RenderCombat()
|
||||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
||||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
m.block = 0
|
||||
local intent = m.intents[m.intentIdx]
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
self:MonsterLunge(idx)
|
||||
local atk = intent.value + m.str
|
||||
if 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
|
||||
atk = math.floor(atk * 0.75)
|
||||
end
|
||||
if self.PlayerVuln > 0 then
|
||||
atk = math.floor(atk * 1.5)
|
||||
end
|
||||
local before = self.PlayerHp
|
||||
self:DealDamageToPlayer(atk, idx)
|
||||
self:ShowPlayerDmgPop(before - self.PlayerHp)
|
||||
self:PlayerHitMotion()
|
||||
elseif intent.kind == "Defend" then
|
||||
m.block = m.block + intent.value
|
||||
elseif intent.kind == "Debuff" then
|
||||
if intent.effect == "weak" then
|
||||
self.PlayerWeak = self.PlayerWeak + intent.value
|
||||
elseif intent.effect == "vuln" then
|
||||
self.PlayerVuln = self.PlayerVuln + intent.value
|
||||
end
|
||||
elseif intent.kind == "AddCard" then
|
||||
local cnt = intent.count or 1
|
||||
for ci = 1, cnt do
|
||||
table.insert(self.DiscardPile, intent.card)
|
||||
end
|
||||
self:RenderPiles()
|
||||
local cn = intent.card
|
||||
local cc = self.Cards[intent.card]
|
||||
if cc ~= nil then cn = cc.name end
|
||||
self:Toast(m.name .. ": " .. cn .. " 추가!")
|
||||
end
|
||||
end
|
||||
if #m.intents > 0 then
|
||||
m.intentIdx = math.random(1, #m.intents)
|
||||
end
|
||||
if m.weak > 0 then m.weak = m.weak - 1 end
|
||||
if m.vuln > 0 then m.vuln = m.vuln - 1 end
|
||||
self:RenderCombat()
|
||||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
||||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
||||
end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]),
|
||||
method('FinishEnemyTurn', `self.TurnBusy = false
|
||||
self:CheckCombatEnd()
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`),
|
||||
method('ClearCombatCards', `self.DrawPile = {}
|
||||
self.DiscardPile = {}
|
||||
self.ExhaustPile = {}
|
||||
self.Hand = {}
|
||||
self.DiscardSelectRemaining = 0
|
||||
self.DiscardSelectTotal = 0
|
||||
self.DiscardPostShiv = 0
|
||||
self.DiscardShivPerPick = 0
|
||||
self.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:RenderHand(false)
|
||||
self:RenderPiles()`),
|
||||
method('CheckCombatEnd', `if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
local anyAlive = false
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then anyAlive = true; break end
|
||||
end
|
||||
if anyAlive == false then
|
||||
self.CombatOver = true
|
||||
self:ClearCombatCards()
|
||||
self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult())
|
||||
self:ApplyRelics("combatEnd")
|
||||
self:ApplyRelics("combatReward")
|
||||
self:MaybeDropPotion()
|
||||
self:RenderRun()
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil and node.type == "elite" then
|
||||
self.Gold = self.Gold + 15
|
||||
local nid = self:PickNewRelic()
|
||||
if nid ~= "" then
|
||||
self:AddRelic(nid)
|
||||
local nr = self.Relics[nid]
|
||||
if nr ~= nil then
|
||||
self:Toast("유물 획득: " .. nr.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
if node ~= nil and node.type == "boss" then
|
||||
if self:CanAdvanceJob() == true and self.Floor < self.RunLength then
|
||||
self:ShowJobChoice()
|
||||
else
|
||||
if self.PlayerJob ~= "" then self:AwardSouls(1) end
|
||||
local bid = self:PickNewRelic()
|
||||
if bid ~= "" then
|
||||
self:AddRelic(bid)
|
||||
local br = self.Relics[bid]
|
||||
if br ~= nil then
|
||||
self:Toast("유물 획득: " .. br.name)
|
||||
end
|
||||
end
|
||||
self:ContinueAfterBoss()
|
||||
end
|
||||
else
|
||||
self:OfferReward()
|
||||
end
|
||||
elseif self.PlayerHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:EndRun("패배...")
|
||||
end`),
|
||||
method('ContinueAfterBoss', `if self.Floor < self.RunLength then
|
||||
self.Floor = self.Floor + 1
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self:GenerateMap()
|
||||
self:RenderRun()
|
||||
self:TeleportToActMap()
|
||||
self:ShowMap()
|
||||
else
|
||||
self:EndRun("런 클리어!")
|
||||
end`),
|
||||
];
|
||||
538
tools/deck/cb/deckturn.mjs
Normal file
538
tools/deck/cb/deckturn.mjs
Normal file
@@ -0,0 +1,538 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const deckTurnMethods = [
|
||||
method('Shuffle', `if list == nil then
|
||||
\treturn
|
||||
end
|
||||
for i = #list, 2, -1 do
|
||||
\tlocal j = math.random(1, i)
|
||||
\tlist[i], list[j] = list[j], list[i]
|
||||
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]),
|
||||
method('BindButtons', `if self.ButtonsBound == true 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
|
||||
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
|
||||
self.EndTurnHandler = nil
|
||||
end
|
||||
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
|
||||
end
|
||||
local drawPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/DrawPile")
|
||||
if drawPile ~= nil and (drawPile.ButtonComponent ~= nil or drawPile:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.DrawPileHandler ~= nil then
|
||||
drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)
|
||||
self.DrawPileHandler = nil
|
||||
end
|
||||
self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end)
|
||||
end
|
||||
local discardPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/DiscardPile")
|
||||
if discardPile ~= nil and (discardPile.ButtonComponent ~= nil or discardPile:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.DiscardPileHandler ~= nil then
|
||||
discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)
|
||||
self.DiscardPileHandler = nil
|
||||
end
|
||||
self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end)
|
||||
end
|
||||
local exhaustPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/ExhaustPile")
|
||||
if exhaustPile ~= nil and (exhaustPile.ButtonComponent ~= nil or exhaustPile:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.ExhaustPileHandler ~= nil then
|
||||
exhaustPile:DisconnectEvent(ButtonClickEvent, self.ExhaustPileHandler)
|
||||
self.ExhaustPileHandler = nil
|
||||
end
|
||||
self.ExhaustPileHandler = exhaustPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("exhaust") end)
|
||||
end
|
||||
local inspectClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud/Close")
|
||||
if inspectClose ~= nil and (inspectClose.ButtonComponent ~= nil or inspectClose:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.DeckInspectCloseHandler ~= nil then
|
||||
inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)
|
||||
self.DeckInspectCloseHandler = nil
|
||||
end
|
||||
self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)
|
||||
end
|
||||
local allDeckButton = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/AllDeckButton")
|
||||
if allDeckButton ~= nil and (allDeckButton.ButtonComponent ~= nil or allDeckButton:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.AllDeckHandler ~= nil then
|
||||
allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)
|
||||
self.AllDeckHandler = nil
|
||||
end
|
||||
self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)
|
||||
end
|
||||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
|
||||
if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.AllDeckCloseHandler ~= nil then
|
||||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||
self.AllDeckCloseHandler = nil
|
||||
end
|
||||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||
end
|
||||
self:BindClassDeckTabs()
|
||||
for i = 1, 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
|
||||
local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i))
|
||||
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
|
||||
local cardPath = "/ui/RunUIGroup/CardHand/Card" .. tostring(i)
|
||||
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
|
||||
cardEntity:ConnectEvent(UITouchDragEvent, function(ev) self:OnCardDrag(i, ev.TouchPoint) end)
|
||||
cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(i, ev.TouchPoint) end)
|
||||
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:HoverCard(i) end)
|
||||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end)
|
||||
if (cardEntity.ButtonComponent ~= nil or cardEntity:AddComponent("ButtonComponent") ~= nil) then
|
||||
cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local rc = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Reward" .. tostring(i))
|
||||
if rc ~= nil and (rc.ButtonComponent ~= nil or rc:AddComponent("ButtonComponent") ~= nil) then
|
||||
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
|
||||
if rc.UITouchReceiveComponent ~= nil then
|
||||
local cardPath = "/ui/RunUIGroup/RewardHud/Reward" .. tostring(i)
|
||||
rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||
rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
local skip = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Skip")
|
||||
if skip ~= nil and (skip.ButtonComponent ~= nil or skip:AddComponent("ButtonComponent") ~= nil) then
|
||||
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
|
||||
end
|
||||
local mapNodeIds = {}
|
||||
for r = 1, ${MAP_ROWS} do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
table.insert(mapNodeIds, "r" .. tostring(r) .. "c" .. tostring(c))
|
||||
end
|
||||
end
|
||||
table.insert(mapNodeIds, "boss")
|
||||
for i = 1, #mapNodeIds do
|
||||
local nid = mapNodeIds[i]
|
||||
local mn = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud/Node_" .. nid)
|
||||
if mn ~= nil and (mn.ButtonComponent ~= nil or mn:AddComponent("ButtonComponent") ~= nil) then
|
||||
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
|
||||
end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local sc = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Card" .. tostring(i))
|
||||
if sc ~= nil and (sc.ButtonComponent ~= nil or sc:AddComponent("ButtonComponent") ~= nil) then
|
||||
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
|
||||
if sc.UITouchReceiveComponent ~= nil then
|
||||
local cardPath = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i)
|
||||
sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||||
sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
local shopLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Leave")
|
||||
if shopLeave ~= nil and (shopLeave.ButtonComponent ~= nil or shopLeave:AddComponent("ButtonComponent") ~= nil) then
|
||||
shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||
end
|
||||
local shopRelic = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Relic")
|
||||
if shopRelic ~= nil and (shopRelic.ButtonComponent ~= nil or shopRelic:AddComponent("ButtonComponent") ~= nil) then
|
||||
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)
|
||||
end
|
||||
local restLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/RestHud/Leave")
|
||||
if restLeave ~= nil and (restLeave.ButtonComponent ~= nil or restLeave:AddComponent("ButtonComponent") ~= nil) then
|
||||
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||
end
|
||||
for i = 1, ${MAX_MONSTERS} do
|
||||
local ms = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i))
|
||||
if ms ~= nil and (ms.ButtonComponent ~= nil or ms:AddComponent("ButtonComponent") ~= nil) then
|
||||
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
|
||||
end
|
||||
end
|
||||
for i = 1, 10 do
|
||||
local rs = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
|
||||
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
|
||||
local idx = i
|
||||
rs:ConnectEvent(UITouchEnterEvent, function()
|
||||
local rid = nil
|
||||
if self.RunRelics ~= nil then rid = self.RunRelics[idx] end
|
||||
if rid ~= nil and self.Relics[rid] ~= nil then
|
||||
self:ShowTooltip(self.Relics[rid].name, self.Relics[rid].desc, -240 + (idx - 1) * 48)
|
||||
end
|
||||
end)
|
||||
rs:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
|
||||
end
|
||||
end
|
||||
for i = 1, 5 do
|
||||
local ps = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
|
||||
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
|
||||
local idx = i
|
||||
ps:ConnectEvent(UITouchEnterEvent, function()
|
||||
local pid = nil
|
||||
if self.RunPotions ~= nil then pid = self.RunPotions[idx] end
|
||||
if pid ~= nil and self.Potions[pid] ~= nil then
|
||||
self:ShowTooltip(self.Potions[pid].name, self.Potions[pid].desc, 240 + (idx - 1) * 44)
|
||||
end
|
||||
end)
|
||||
ps:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
|
||||
ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end)
|
||||
end
|
||||
end
|
||||
local pmUse = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Use")
|
||||
if pmUse ~= nil and (pmUse.ButtonComponent ~= nil or pmUse:AddComponent("ButtonComponent") ~= nil) then
|
||||
pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end)
|
||||
end
|
||||
local pmToss = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Toss")
|
||||
if pmToss ~= nil and (pmToss.ButtonComponent ~= nil or pmToss:AddComponent("ButtonComponent") ~= nil) then
|
||||
pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end)
|
||||
end
|
||||
local pmClose = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Close")
|
||||
if pmClose ~= nil and (pmClose.ButtonComponent ~= nil or pmClose:AddComponent("ButtonComponent") ~= nil) then
|
||||
pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end)
|
||||
end
|
||||
local shopPotion = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Potion")
|
||||
if shopPotion ~= nil and (shopPotion.ButtonComponent ~= nil or shopPotion:AddComponent("ButtonComponent") ~= nil) then
|
||||
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
|
||||
end
|
||||
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
|
||||
if chest ~= nil and (chest.ButtonComponent ~= nil or chest:AddComponent("ButtonComponent") ~= nil) then
|
||||
chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end)
|
||||
end
|
||||
local treasureLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Leave")
|
||||
if treasureLeave ~= nil and (treasureLeave.ButtonComponent ~= nil or treasureLeave:AddComponent("ButtonComponent") ~= nil) then
|
||||
treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||
end
|
||||
local jcRelic = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobChoiceHud/RelicButton")
|
||||
if jcRelic ~= nil and (jcRelic.ButtonComponent ~= nil or jcRelic:AddComponent("ButtonComponent") ~= nil) then
|
||||
jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end)
|
||||
end
|
||||
local jcJob = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobChoiceHud/JobButton")
|
||||
if jcJob ~= nil and (jcJob.ButtonComponent ~= nil or jcJob:AddComponent("ButtonComponent") ~= nil) then
|
||||
jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end)
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local slotIdx = i
|
||||
local jb = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i))
|
||||
if jb ~= nil and (jb.ButtonComponent ~= nil or jb:AddComponent("ButtonComponent") ~= nil) then
|
||||
jb:ConnectEvent(ButtonClickEvent, function()
|
||||
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
|
||||
self:SetJob(self.JobOpts[slotIdx].id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end`),
|
||||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||||
self.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.BlockGainMultiplier = 1
|
||||
self:ApplyRelics("turnStart")
|
||||
if self.NextTurnKeepBlock == true then
|
||||
self.NextTurnKeepBlock = false
|
||||
else
|
||||
self.PlayerBlock = 0
|
||||
end
|
||||
if self.ClayBlockNext > 0 then
|
||||
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
|
||||
self.ClayBlockNext = 0
|
||||
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
|
||||
for i = 1, #self.PlayerPowers do
|
||||
local pc = self.Cards[self.PlayerPowers[i]]
|
||||
if pc ~= nil then
|
||||
if pc.powerEffect == "strengthPerTurn" then
|
||||
self.PlayerStr = self.PlayerStr + pc.value
|
||||
elseif pc.powerEffect == "energyPerTurn" then
|
||||
self.Energy = self.Energy + pc.value
|
||||
elseif pc.powerEffect == "blockPerTurn" then
|
||||
self.PlayerBlock = self.PlayerBlock + pc.value
|
||||
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
|
||||
if pc.turnStartShiv ~= nil then
|
||||
self:AddCardsToHand("Shiv", pc.turnStartShiv)
|
||||
end
|
||||
if pc.turnStartDraw ~= nil then
|
||||
powerTurnDraw = powerTurnDraw + pc.turnStartDraw
|
||||
end
|
||||
if pc.turnStartDiscard ~= nil then
|
||||
powerTurnDiscard = powerTurnDiscard + pc.turnStartDiscard
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
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:RenderCombat()
|
||||
if powerTurnDiscard > 0 then
|
||||
self:BeginDiscardSelection({ discard = math.min(powerTurnDiscard, #self.Hand) })
|
||||
return
|
||||
end
|
||||
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
|
||||
return
|
||||
end
|
||||
if self:IsDiscardSelecting() == true then
|
||||
self:Toast("버릴 카드를 먼저 선택하세요")
|
||||
return
|
||||
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
|
||||
for bi = 1, #self.Hand do
|
||||
\tlocal hc = self.Cards[self.Hand[bi]]
|
||||
\tif hc ~= nil and hc.endTurnDamage ~= nil then burn = burn + hc.endTurnDamage end
|
||||
end
|
||||
if burn > 0 then
|
||||
\tself.PlayerHp = self.PlayerHp - burn
|
||||
\tif self.PlayerHp < 0 then self.PlayerHp = 0 end
|
||||
\tself:ShowPlayerDmgPop(burn)
|
||||
\tself:RenderCombat()
|
||||
end
|
||||
local kept = {}
|
||||
for i = 1, #self.Hand do
|
||||
\tlocal cardId = self.Hand[i]
|
||||
\tlocal c = self.Cards[cardId]
|
||||
\tif c ~= nil and (c.retain == true or (c.class == "shiv" and self:HasPowerField("shivRetain") == true) or i == retainSlot) then
|
||||
\t\ttable.insert(kept, cardId)
|
||||
\telse
|
||||
\t\ttable.insert(self.DiscardPile, cardId)
|
||||
\tend
|
||||
end
|
||||
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.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self.TurnSkillSlyCards = {}
|
||||
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
|
||||
\tif #self.DrawPile <= 0 then
|
||||
\t\tself:RecycleDiscardIntoDraw()
|
||||
\tend
|
||||
\tif #self.DrawPile <= 0 then
|
||||
\t\tbreak
|
||||
\tend
|
||||
\tlocal cardId = table.remove(self.DrawPile)
|
||||
\ttable.insert(drawnCards, cardId)
|
||||
\tself.CardsDrawnThisCombat = (self.CardsDrawnThisCombat or 0) + 1
|
||||
\tself:ApplyDrawTrigger()
|
||||
\tif #self.Hand >= 10 then
|
||||
\t\ttable.insert(self.DiscardPile, cardId)
|
||||
\t\tself:TriggerSly(cardId)
|
||||
\telse
|
||||
\t\ttable.insert(self.Hand, cardId)
|
||||
\t\tdrewAny = true
|
||||
\t\ttable.insert(drawnSlots, #self.Hand)
|
||||
\tend
|
||||
end
|
||||
self:RenderPiles()
|
||||
if drewAny == true then
|
||||
\tself:RenderHand(false)
|
||||
end
|
||||
if animate == true and #drawnSlots > 0 then
|
||||
\tlocal drawStart = Vector2(-590, 8)
|
||||
\tfor i = 1, #drawnSlots do
|
||||
\t\tlocal slot = drawnSlots[i]
|
||||
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
|
||||
\tend
|
||||
return drawnCards
|
||||
end`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
|
||||
], 0, 'any'),
|
||||
method('AddCardsToHand', `if self.Hand == nil then
|
||||
self.Hand = {}
|
||||
end
|
||||
if self.DiscardPile == nil then
|
||||
self.DiscardPile = {}
|
||||
end
|
||||
for i = 1, amount do
|
||||
if #self.Hand >= 10 then
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
else
|
||||
table.insert(self.Hand, cardId)
|
||||
end
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
]),
|
||||
method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then
|
||||
\treturn
|
||||
end
|
||||
self.DrawPile = {}
|
||||
for i = 1, #self.DiscardPile do
|
||||
\tself.DrawPile[i] = self.DiscardPile[i]
|
||||
end
|
||||
self.DiscardPile = {}
|
||||
self:Shuffle(self.DrawPile)`),
|
||||
method('RenderPiles', `self:SetText("/ui/RunUIGroup/DeckHud/DrawPile/Count", self:FormatNumber(#self.DrawPile))
|
||||
self:SetText("/ui/RunUIGroup/DeckHud/DiscardPile/Count", self:FormatNumber(#self.DiscardPile))
|
||||
self:SetText("/ui/RunUIGroup/DeckHud/ExhaustPile/Count", self:FormatNumber(#(self.ExhaustPile or {})))
|
||||
self:SetText("/ui/RunUIGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
|
||||
local inspect = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||
if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then
|
||||
self:OpenDeckInspect(self.DeckInspectKind)
|
||||
end`),
|
||||
];
|
||||
256
tools/deck/cb/deckview.mjs
Normal file
256
tools/deck/cb/deckview.mjs
Normal file
@@ -0,0 +1,256 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const deckViewMethods = [
|
||||
method('OpenDeckInspect', `self.DeckInspectKind = kind
|
||||
if self.DeckAllOpen == true then
|
||||
self.DeckAllOpen = false
|
||||
local allHud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||
if allHud ~= nil then
|
||||
allHud.Enable = false
|
||||
end
|
||||
end
|
||||
local pile = {}
|
||||
local title = ""
|
||||
if kind == "discard" then
|
||||
pile = self.DiscardPile or {}
|
||||
title = "버린 덱"
|
||||
elseif kind == "exhaust" then
|
||||
pile = self.ExhaustPile or {}
|
||||
title = "소멸 덱"
|
||||
else
|
||||
pile = self.DrawPile or {}
|
||||
title = "뽑을 덱"
|
||||
end
|
||||
self:RenderDeckInspect(pile, title)
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||||
method('CloseDeckInspect', `self.DeckInspectKind = ""
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end`),
|
||||
method('RenderDeckInspect', `local count = 0
|
||||
if pile ~= nil then
|
||||
count = #pile
|
||||
end
|
||||
local suffix = " (" .. tostring(count) .. ")"
|
||||
if count > 60 then
|
||||
suffix = suffix .. " - 60장까지 표시"
|
||||
end
|
||||
self:SetText("/ui/DeckUIGroup/DeckInspectHud/Title", title .. suffix)
|
||||
self:SetEntityEnabled("/ui/DeckUIGroup/DeckInspectHud/Empty", count <= 0)
|
||||
for i = 1, 60 do
|
||||
local path = "/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. tostring(i)
|
||||
local cardId = nil
|
||||
if pile ~= nil then
|
||||
cardId = pile[i]
|
||||
end
|
||||
if cardId == nil then
|
||||
self:SetEntityEnabled(path, false)
|
||||
else
|
||||
self:SetEntityEnabled(path, true)
|
||||
self:ApplyInspectCardVisual(i, cardId)
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
|
||||
]),
|
||||
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/WarriorTab")
|
||||
if warriorTab ~= nil and (warriorTab.ButtonComponent ~= nil or warriorTab:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.WarriorDeckTabHandler ~= nil then
|
||||
warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler)
|
||||
self.WarriorDeckTabHandler = nil
|
||||
end
|
||||
self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end)
|
||||
end
|
||||
local thiefTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/ThiefTab")
|
||||
if thiefTab ~= nil and (thiefTab.ButtonComponent ~= nil or thiefTab:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.ThiefDeckTabHandler ~= nil then
|
||||
thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler)
|
||||
self.ThiefDeckTabHandler = nil
|
||||
end
|
||||
self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("rogue") end)
|
||||
end
|
||||
local mageTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/MageTab")
|
||||
if mageTab ~= nil and (mageTab.ButtonComponent ~= nil or mageTab:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.MageDeckTabHandler ~= nil then
|
||||
mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler)
|
||||
self.MageDeckTabHandler = nil
|
||||
end
|
||||
self.MageDeckTabHandler = mageTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("magician") end)
|
||||
end`),
|
||||
method('OpenClassDeck', `self.CodexMode = false
|
||||
self.ClassDeckMode = true
|
||||
self.DebugCardPickerMode = false
|
||||
self.DeckAllOpen = true
|
||||
self:SetClassDeckTab(className)
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
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
|
||||
return
|
||||
end
|
||||
self.ClassDeckCards = {}
|
||||
self.ClassDeckTitle = "직업 덱"
|
||||
if className ~= "warrior" and className ~= "magician" and className ~= "rogue" then
|
||||
className = "rogue"
|
||||
end
|
||||
self.ClassDeckClass = className
|
||||
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
|
||||
self.ClassDeckTitle = "전사 전체 덱"
|
||||
elseif className == "magician" then
|
||||
self.ClassDeckTitle = "마법사 전체 덱"
|
||||
else
|
||||
self.ClassDeckTitle = "도적 전체 덱"
|
||||
end
|
||||
for id, c in pairs(self.Cards) do
|
||||
if c ~= nil and c.curse ~= true and c.token ~= true and allowed[c.class] == true then
|
||||
table.insert(self.ClassDeckCards, id)
|
||||
end
|
||||
end
|
||||
table.sort(self.ClassDeckCards, function(a, b)
|
||||
local ca = self.Cards[a]
|
||||
local cb = self.Cards[b]
|
||||
local na = a
|
||||
local nb = b
|
||||
if ca ~= nil and ca.name ~= nil then na = ca.name end
|
||||
if cb ~= nil and cb.name ~= nil then nb = cb.name end
|
||||
if na == nb then return a < b end
|
||||
return na < nb
|
||||
end)
|
||||
self:RenderAllDeck()
|
||||
self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
||||
method('RenderClassDeckTabs', `local tabs = {
|
||||
{ path = "/ui/DeckUIGroup/DeckAllHud/WarriorTab", cls = "warrior" },
|
||||
{ path = "/ui/DeckUIGroup/DeckAllHud/ThiefTab", cls = "rogue" },
|
||||
{ path = "/ui/DeckUIGroup/DeckAllHud/MageTab", cls = "magician" },
|
||||
}
|
||||
for i = 1, #tabs do
|
||||
self:SetEntityEnabled(tabs[i].path, self.ClassDeckMode == true)
|
||||
local e = _EntityService:GetEntityByPath(tabs[i].path)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ClassDeckClass == tabs[i].cls then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1)
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
|
||||
if inspectHud ~= nil then
|
||||
inspectHud.Enable = false
|
||||
end
|
||||
self.DeckInspectKind = ""
|
||||
self.ClassDeckMode = false
|
||||
self.ClassDeckClass = ""
|
||||
self.DebugCardPickerMode = false
|
||||
self:RenderClassDeckTabs()
|
||||
self.DeckAllOpen = true
|
||||
self:RenderAllDeck()
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('CloseAllDeck', `self.DeckAllOpen = false
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
if self.ClassDeckMode == true then
|
||||
self.ClassDeckMode = false
|
||||
self.ClassDeckCards = {}
|
||||
self.ClassDeckTitle = ""
|
||||
self.ClassDeckClass = ""
|
||||
end
|
||||
self.DebugCardPickerMode = false
|
||||
self:RenderClassDeckTabs()
|
||||
if self.CodexMode == true then
|
||||
self.CodexMode = false
|
||||
self:ShowLobby()
|
||||
end`),
|
||||
method('RenderAllDeck', `local pile = self.RunDeck or {}
|
||||
local title = "모든 덱"
|
||||
if self.ClassDeckMode == true then
|
||||
pile = self.ClassDeckCards or {}
|
||||
title = self.ClassDeckTitle
|
||||
if self.DebugCardPickerMode == true then
|
||||
title = title .. " - 테스트 카드 추가"
|
||||
end
|
||||
elseif self.CodexMode == true then
|
||||
pile = self.CodexCards or {}
|
||||
title = "카드 도감"
|
||||
end
|
||||
local count = #pile
|
||||
self:SetText("/ui/DeckUIGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")")
|
||||
self:RenderClassDeckTabs()
|
||||
self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud/Empty", count <= 0)
|
||||
for i = 1, 120 do
|
||||
local path = "/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(i)
|
||||
local cardId = pile[i]
|
||||
if cardId == nil then
|
||||
self:SetEntityEnabled(path, false)
|
||||
else
|
||||
self:SetEntityEnabled(path, true)
|
||||
self:ApplyAllDeckCardVisual(i, cardId)
|
||||
end
|
||||
end`),
|
||||
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ 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' },
|
||||
]),
|
||||
];
|
||||
959
tools/deck/cb/hand.mjs
Normal file
959
tools/deck/cb/hand.mjs
Normal file
@@ -0,0 +1,959 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const handMethods = [
|
||||
method('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
|
||||
if self.Hand ~= nil then
|
||||
n = #self.Hand
|
||||
end
|
||||
if n <= 0 then
|
||||
return 0
|
||||
end
|
||||
local spacing = 175
|
||||
if n > 8 then spacing = math.floor(1400 / n) end
|
||||
local startX = -((n - 1) * spacing) / 2
|
||||
return startX + (slot - 1) * spacing`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'),
|
||||
method('RenderHand', `local n = #self.Hand
|
||||
local spacing = 175
|
||||
if n > 8 then spacing = math.floor(1400 / n) end
|
||||
local startX = -((n - 1) * spacing) / 2
|
||||
local drawStart = Vector2(-590, 8)
|
||||
for i = 1, 10 do
|
||||
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i))
|
||||
\tif cardEntity ~= nil then
|
||||
\t\tlocal cardId = self.Hand[i]
|
||||
\t\tif cardId == nil then
|
||||
\t\t\tcardEntity.Enable = false
|
||||
\t\telse
|
||||
\t\t\tcardEntity.Enable = true
|
||||
\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end
|
||||
\t\t\tself:ApplyCardVisual(i, cardId)
|
||||
\t\t\tlocal tx = self:GetHandSlotX(i)
|
||||
\t\t\tif animate == true then
|
||||
\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03)
|
||||
\t\t\telse
|
||||
\t\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.anchoredPosition = Vector2(tx, 0) end
|
||||
\t\t\tend
|
||||
\t\tend
|
||||
\tend
|
||||
end
|
||||
self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]),
|
||||
method('ApplyCardFace', `local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill", class = "warrior", rarity = "normal" }
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
end
|
||||
local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"]
|
||||
local ruid = nil
|
||||
if frames ~= nil then
|
||||
ruid = frames[c.rarity or "normal"]
|
||||
end
|
||||
if ruid ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ruid
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||||
self:SetText(base .. "/Name", c.name)
|
||||
self:SetText(base .. "/Desc", self:FormatCardDescription(c.desc))
|
||||
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
||||
if art ~= nil then
|
||||
if c.image ~= nil and c.image ~= "" then
|
||||
art.Enable = true
|
||||
if art.SpriteGUIRendererComponent ~= nil then
|
||||
art.SpriteGUIRendererComponent.ImageRUID = c.image
|
||||
end
|
||||
else
|
||||
art.Enable = false
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('SetCardHover', `local prefix = ""
|
||||
local count = 0
|
||||
local xs = {}
|
||||
local baseY = 0
|
||||
local hoverIndex = 0
|
||||
local push = 110
|
||||
if string.find(path, "/ui/RunUIGroup/CardHand/Card") == 1 then
|
||||
if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||||
return
|
||||
end
|
||||
prefix = "/ui/RunUIGroup/CardHand/Card"
|
||||
count = 0
|
||||
if self.Hand ~= nil then count = #self.Hand end
|
||||
for i = 1, count do
|
||||
xs[i] = self:GetHandSlotX(i)
|
||||
end
|
||||
baseY = 0
|
||||
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
||||
elseif string.find(path, "/ui/RunUIGroup/RewardHud/Reward") == 1 then
|
||||
prefix = "/ui/RunUIGroup/RewardHud/Reward"
|
||||
count = 3
|
||||
xs = { -300, 0, 300 }
|
||||
baseY = 0
|
||||
hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0
|
||||
elseif string.find(path, "/ui/RunUIGroup/ShopHud/Card") == 1 then
|
||||
prefix = "/ui/RunUIGroup/ShopHud/Card"
|
||||
count = 3
|
||||
xs = { -300, 0, 300 }
|
||||
baseY = 20
|
||||
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
||||
end
|
||||
if count <= 0 then
|
||||
return
|
||||
end
|
||||
if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
|
||||
_TimerService:ClearTimer(self.CardHoverTweenId)
|
||||
self.CardHoverTweenId = 0
|
||||
end
|
||||
local items = {}
|
||||
for i = 1, count do
|
||||
local e = _EntityService:GetEntityByPath(prefix .. tostring(i))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
local tr = e.UITransformComponent
|
||||
local tx = xs[i]
|
||||
local ty = baseY
|
||||
local sc = 1
|
||||
if hover == true and hoverIndex > 0 then
|
||||
if i == hoverIndex and e.Enable == true then
|
||||
sc = 1.5
|
||||
elseif i < hoverIndex then
|
||||
tx = tx - push
|
||||
elseif i > hoverIndex then
|
||||
tx = tx + push
|
||||
end
|
||||
end
|
||||
table.insert(items, { tr = tr, sx = tr.anchoredPosition.x, sy = tr.anchoredPosition.y, ss = tr.UIScale.x, tx = tx, ty = ty, ts = sc })
|
||||
end
|
||||
end
|
||||
local elapsed = 0
|
||||
local duration = 0.12
|
||||
local eventId = 0
|
||||
eventId = _TimerService:SetTimerRepeat(function()
|
||||
elapsed = elapsed + 1 / 60
|
||||
local t = math.min(elapsed / duration, 1)
|
||||
local eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)
|
||||
for i = 1, #items do
|
||||
local it = items[i]
|
||||
local x = it.sx + (it.tx - it.sx) * eased
|
||||
local y = it.sy + (it.ty - it.sy) * eased
|
||||
local s = it.ss + (it.ts - it.ss) * eased
|
||||
it.tr.anchoredPosition = Vector2(x, y)
|
||||
it.tr.UIScale = Vector3(s, s, 1)
|
||||
end
|
||||
if t >= 1 then
|
||||
_TimerService:ClearTimer(eventId)
|
||||
if self.CardHoverTweenId == eventId then
|
||||
self.CardHoverTweenId = 0
|
||||
end
|
||||
end
|
||||
end, 1 / 60)
|
||||
self.CardHoverTweenId = eventId`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' },
|
||||
]),
|
||||
method('ApplyCardVisual', `self:ApplyCardFace("/ui/RunUIGroup/CardHand/Card" .. tostring(slot), cardId)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('SetText', `local entity = _EntityService:GetEntityByPath(path)
|
||||
if entity ~= nil and entity.TextComponent ~= nil then
|
||||
\tentity.TextComponent.Text = value
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' },
|
||||
]),
|
||||
method('FormatNumber', `if value == nil then
|
||||
return ""
|
||||
end
|
||||
local n = tonumber(value)
|
||||
if n == nil then
|
||||
return tostring(value)
|
||||
end
|
||||
if math.abs(n - math.floor(n)) < 0.00001 then
|
||||
return string.format("%d", math.floor(n))
|
||||
end
|
||||
return tostring(n)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }], 0, 'string'),
|
||||
method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||
if cardEntity == nil or cardEntity.UITransformComponent == nil then
|
||||
\treturn
|
||||
end
|
||||
local tr = cardEntity.UITransformComponent
|
||||
tr.anchoredPosition = fromPos
|
||||
local elapsed = 0
|
||||
local eventId = 0
|
||||
eventId = _TimerService:SetTimerRepeat(function()
|
||||
\telapsed = elapsed + 1 / 60
|
||||
\tlocal t = math.min(elapsed / duration, 1)
|
||||
\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)
|
||||
\ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased)
|
||||
\tif t >= 1 then
|
||||
\t\t_TimerService:ClearTimer(eventId)
|
||||
\tend
|
||||
end, 1 / 60)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
|
||||
]),
|
||||
method('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
|
||||
if amount > 0 and self.PlayerDex ~= nil then
|
||||
amount = amount + self.PlayerDex
|
||||
end
|
||||
if self.BlockGainMultiplier ~= nil and self.BlockGainMultiplier > 1 then
|
||||
amount = amount * self.BlockGainMultiplier
|
||||
end
|
||||
if amount < 0 then
|
||||
amount = 0
|
||||
end
|
||||
self.PlayerBlock = self.PlayerBlock + amount
|
||||
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||
method('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
|
||||
self.FightAttackCount = self.FightAttackCount + 1
|
||||
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
|
||||
base2 = base2 + 8
|
||||
end
|
||||
local dmg = base2 + self.PlayerStr
|
||||
if self:HasRelic("penNib") and self.FightAttackCount % 10 == 0 then
|
||||
dmg = dmg * 2
|
||||
end
|
||||
if self.PlayerWeak > 0 then
|
||||
dmg = math.floor(dmg * 0.75)
|
||||
end
|
||||
if self.TurnAttackMultiplier ~= nil and self.TurnAttackMultiplier > 1 then
|
||||
dmg = dmg * self.TurnAttackMultiplier
|
||||
end
|
||||
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
|
||||
dmg = 5
|
||||
end
|
||||
if dmg < 0 then
|
||||
dmg = 0
|
||||
end
|
||||
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||
method('QueueNextTurnAddCard', `if cardId == nil or cardId == "" or amount == nil or amount <= 0 then
|
||||
return
|
||||
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.damage ~= nil or c.xDamagePerEnergy ~= nil then
|
||||
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 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
|
||||
total = total + self:CalcPlayerAttack(baseDmg)
|
||||
end
|
||||
local useAoe = c.aoe == true
|
||||
if c.class == "shiv" and (self.ShivAoeThisCombat == true or self:HasPowerField("shivAoe") == true) then
|
||||
useAoe = true
|
||||
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
|
||||
if c.block ~= nil then
|
||||
self:AddCardBlock(c.block)
|
||||
end
|
||||
if free ~= true then
|
||||
self:ApplyRelics("cardPlayed")
|
||||
end
|
||||
elseif c.kind == "Skill" then
|
||||
if c.block ~= nil then
|
||||
self:AddCardBlock(c.block)
|
||||
end
|
||||
elseif c.kind == "Power" then
|
||||
if free ~= true then
|
||||
table.insert(self.PlayerPowers, cardId)
|
||||
end
|
||||
end
|
||||
if c.strength ~= nil then
|
||||
self.PlayerStr = self.PlayerStr + c.strength
|
||||
end
|
||||
if c.dex ~= nil then
|
||||
self.PlayerDex = self.PlayerDex + c.dex
|
||||
end
|
||||
if c.thorns ~= nil then
|
||||
self.PlayerThorns = self.PlayerThorns + c.thorns
|
||||
end
|
||||
if c.selfVuln ~= nil then
|
||||
self.PlayerVuln = self.PlayerVuln + c.selfVuln
|
||||
end
|
||||
if c.heal ~= nil then
|
||||
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
||||
end
|
||||
if c.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]
|
||||
if tm == nil or tm.alive ~= true then
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
|
||||
end
|
||||
end
|
||||
local targets = {}
|
||||
if c.affectsAllEnemies == true and 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(targets, om)
|
||||
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
|
||||
local drawnCards = {}
|
||||
if c.draw ~= nil then
|
||||
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
|
||||
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
||||
self:AddCardsToHand("Shiv", c.addShiv)
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
||||
{ 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]
|
||||
if c == nil then
|
||||
return
|
||||
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:ResolveCardEffects(cardId, 0, c, true, 0)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
|
||||
method('DiscardHandCard', `if self.Hand == nil then
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
local startX = self:GetHandSlotX(slot)
|
||||
table.remove(self.Hand, slot)
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
self.TurnDiscardedCards = (self.TurnDiscardedCards or 0) + 1
|
||||
if triggerSly == true then
|
||||
self:TriggerSly(cardId)
|
||||
end
|
||||
if animate == true then
|
||||
self:AnimateDiscardCards({ cardId }, { startX }, { slot })
|
||||
end`, [
|
||||
{ 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: 'animate' },
|
||||
]),
|
||||
method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'),
|
||||
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
|
||||
return
|
||||
end
|
||||
if self:IsDiscardSelecting() == true then
|
||||
local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining
|
||||
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
|
||||
else
|
||||
e.Enable = false
|
||||
end`),
|
||||
method('BeginDiscardSelection', `if c == nil or self.Hand == nil then
|
||||
return false
|
||||
end
|
||||
if c.discardAll == true then
|
||||
return self:AutoDiscardHand(c)
|
||||
end
|
||||
local n = 0
|
||||
if c.discard ~= nil then
|
||||
n = math.min(c.discard, #self.Hand)
|
||||
end
|
||||
if n <= 0 then
|
||||
return false
|
||||
end
|
||||
self.DiscardSelectRemaining = n
|
||||
self.DiscardSelectTotal = n
|
||||
self.DiscardPostShiv = 0
|
||||
self.DiscardShivPerPick = 0
|
||||
self.DiscardPostDraw = 0
|
||||
self.DiscardDrawPerPick = 0
|
||||
if c.addShiv ~= nil then
|
||||
self.DiscardPostShiv = c.addShiv
|
||||
end
|
||||
if c.addShivPerDiscard == true then
|
||||
self.DiscardShivPerPick = 1
|
||||
end
|
||||
if c.drawPerDiscarded ~= nil and c.drawPerDiscarded > 0 then
|
||||
self.DiscardDrawPerPick = c.drawPerDiscarded
|
||||
end
|
||||
self:UpdateDiscardPrompt()
|
||||
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'),
|
||||
method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0
|
||||
self.DiscardSelectTotal = 0
|
||||
local shivCount = self.DiscardPostShiv or 0
|
||||
local drawCount = self.DiscardPostDraw or 0
|
||||
self.DiscardPostShiv = 0
|
||||
self.DiscardPostDraw = 0
|
||||
self.DiscardShivPerPick = 0
|
||||
self.DiscardDrawPerPick = 0
|
||||
self:UpdateDiscardPrompt()
|
||||
local finish = function()
|
||||
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
|
||||
if delayRender == true then
|
||||
_TimerService:SetTimerOnce(finish, 0.22)
|
||||
else
|
||||
finish()
|
||||
end`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delayRender' }]),
|
||||
method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then
|
||||
return false
|
||||
end
|
||||
if self.Hand == nil or self.Hand[slot] == nil then
|
||||
return true
|
||||
end
|
||||
local discarded = self.Hand[slot]
|
||||
self:DiscardHandCard(slot, true, true)
|
||||
if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then
|
||||
self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick
|
||||
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
|
||||
if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then
|
||||
self:FinishDiscardSelection(true)
|
||||
else
|
||||
self:UpdateDiscardPrompt()
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
_TimerService:SetTimerOnce(function() self:RenderHand(false) end, 0.22)
|
||||
end
|
||||
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
|
||||
];
|
||||
213
tools/deck/cb/items.mjs
Normal file
213
tools/deck/cb/items.mjs
Normal file
@@ -0,0 +1,213 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const itemMethods = [
|
||||
method('HasRelic', `if self.RunRelics == nil then
|
||||
return false
|
||||
end
|
||||
for i = 1, #self.RunRelics do
|
||||
if self.RunRelics[i] == id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
||||
method('ApplyRelics', `if self.RunRelics == nil then
|
||||
return
|
||||
end
|
||||
for i = 1, #self.RunRelics do
|
||||
local r = self.Relics[self.RunRelics[i]]
|
||||
if r ~= nil and r.hook == hook then
|
||||
if r.effect == "block" then
|
||||
self.PlayerBlock = self.PlayerBlock + r.value
|
||||
elseif r.effect == "energy" then
|
||||
self.Energy = self.Energy + r.value
|
||||
elseif r.effect == "strength" then
|
||||
self.PlayerStr = self.PlayerStr + r.value
|
||||
elseif r.effect == "draw" then
|
||||
self:DrawCards(r.value)
|
||||
self:RenderHand(false)
|
||||
elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then
|
||||
self.PlayerHp = self.PlayerHp + r.value
|
||||
if self.PlayerHp > self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
elseif r.effect == "healIfLow" then
|
||||
if self.PlayerHp * 2 <= self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerHp + r.value
|
||||
if self.PlayerHp > self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
end
|
||||
elseif r.effect == "gold" then
|
||||
self.Gold = self.Gold + r.value
|
||||
end
|
||||
end
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hook' }]),
|
||||
method('AddRelic', `if self.RunRelics == nil then
|
||||
self.RunRelics = {}
|
||||
end
|
||||
table.insert(self.RunRelics, id)
|
||||
local r = self.Relics[id]
|
||||
if r ~= nil and r.hook == "passive" then
|
||||
if r.effect == "potionSlots" then
|
||||
self.PotionSlots = r.value
|
||||
self:RenderPotions()
|
||||
elseif r.effect == "maxHp" then
|
||||
self.PlayerMaxHp = self.PlayerMaxHp + r.value
|
||||
self.PlayerHp = self.PlayerHp + r.value
|
||||
end
|
||||
end
|
||||
self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
method('PickNewRelic', `local pool = {}
|
||||
for i = 1, #self.RelicPool do
|
||||
if self:HasRelic(self.RelicPool[i]) == false then
|
||||
table.insert(pool, self.RelicPool[i])
|
||||
end
|
||||
end
|
||||
if #pool == 0 then
|
||||
self.Gold = self.Gold + 25
|
||||
self:Toast("유물을 모두 모았습니다! 메소 +25")
|
||||
return ""
|
||||
end
|
||||
return pool[math.random(1, #pool)]`, [], 0, 'string'),
|
||||
method('AddPotion', `if self.RunPotions == nil then
|
||||
self.RunPotions = {}
|
||||
end
|
||||
if #self.RunPotions >= self.PotionSlots then
|
||||
self:Toast("물약 슬롯이 가득 찼습니다")
|
||||
return false
|
||||
end
|
||||
table.insert(self.RunPotions, pid)
|
||||
self:RenderPotions()
|
||||
return true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pid' }], 0, 'boolean'),
|
||||
method('MaybeDropPotion', `if math.random() > ${POTIONS.dropChance} then
|
||||
return
|
||||
end
|
||||
local keys = {}
|
||||
for pid, _ in pairs(self.Potions) do
|
||||
table.insert(keys, pid)
|
||||
end
|
||||
table.sort(keys)
|
||||
local pid = keys[math.random(1, #keys)]
|
||||
if self:AddPotion(pid) == true then
|
||||
local p = self.Potions[pid]
|
||||
self:Toast("물약 획득: " .. p.name)
|
||||
end`),
|
||||
method('RenderPotions', `for i = 1, 5 do
|
||||
local base = "/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
local pid = nil
|
||||
if self.RunPotions ~= nil then
|
||||
pid = self.RunPotions[i]
|
||||
end
|
||||
if pid ~= nil and self.Potions[pid] ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = self.Potions[pid].icon
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
elseif i > self.PotionSlots then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.1, 0.1, 0.12, 0.85)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.25, 0.3, 0.9)
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('OpenPotionMenu', `if self.RunPotions == nil or self.RunPotions[slot] == nil then
|
||||
return
|
||||
end
|
||||
self.PotionMenuSlot = slot
|
||||
local pid = self.RunPotions[slot]
|
||||
local p = self.Potions[pid]
|
||||
if p ~= nil then
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc)
|
||||
end
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('ClosePotionMenu', `self.PotionMenuSlot = 0
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", false)`),
|
||||
method('UsePotion', `if self.PotionMenuSlot <= 0 then
|
||||
return
|
||||
end
|
||||
if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then
|
||||
self:Toast("지금은 사용할 수 없습니다")
|
||||
return
|
||||
end
|
||||
local combat = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud")
|
||||
local hand = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand")
|
||||
if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then
|
||||
self:Toast("전투 중에만 사용할 수 있습니다")
|
||||
return
|
||||
end
|
||||
local pid = self.RunPotions[self.PotionMenuSlot]
|
||||
if pid == nil then
|
||||
return
|
||||
end
|
||||
local p = self.Potions[pid]
|
||||
if p == nil then
|
||||
return
|
||||
end
|
||||
if p.effect == "heal" then
|
||||
self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp)
|
||||
elseif p.effect == "damage" then
|
||||
self:DealDamageToTarget(p.value, false)
|
||||
self:ShowDmgPop(self.TargetIndex, p.value)
|
||||
elseif p.effect == "strength" then
|
||||
self.PlayerStr = self.PlayerStr + p.value
|
||||
elseif p.effect == "block" then
|
||||
self.PlayerBlock = self.PlayerBlock + p.value
|
||||
elseif p.effect == "energy" then
|
||||
self.Energy = self.Energy + p.value
|
||||
elseif p.effect == "weak" then
|
||||
local tm = self.Monsters[self.TargetIndex]
|
||||
if tm ~= nil and tm.alive == true then
|
||||
tm.weak = tm.weak + p.value
|
||||
end
|
||||
end
|
||||
table.remove(self.RunPotions, self.PotionMenuSlot)
|
||||
self:Toast("물약 사용: " .. p.name)
|
||||
self:ClosePotionMenu()
|
||||
self:RenderPotions()
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()`),
|
||||
method('TossPotion', `if self.PotionMenuSlot <= 0 then
|
||||
return
|
||||
end
|
||||
local pid = self.RunPotions[self.PotionMenuSlot]
|
||||
if pid ~= nil then
|
||||
local p = self.Potions[pid]
|
||||
table.remove(self.RunPotions, self.PotionMenuSlot)
|
||||
if p ~= nil then
|
||||
self:Toast("물약 버림: " .. p.name)
|
||||
end
|
||||
end
|
||||
self:ClosePotionMenu()
|
||||
self:RenderPotions()`),
|
||||
method('RenderRelics', `local count = 0
|
||||
if self.RunRelics ~= nil then
|
||||
count = #self.RunRelics
|
||||
end
|
||||
for i = 1, 10 do
|
||||
local base = "/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
local rid = nil
|
||||
if self.RunRelics ~= nil then
|
||||
rid = self.RunRelics[i]
|
||||
end
|
||||
if rid ~= nil and self.Relics[rid] ~= nil and (i < 10 or count <= 10) then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = self.Relics[rid].icon
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.15, 0.16, 0.2, 0.6)
|
||||
end
|
||||
end
|
||||
end
|
||||
local of = ""
|
||||
if count > 10 then
|
||||
of = "+" .. tostring(count - 9)
|
||||
end
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/RelicOverflow", of)`),
|
||||
];
|
||||
125
tools/deck/cb/jobs.mjs
Normal file
125
tools/deck/cb/jobs.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
import { method } from '../lib/codeblock.mjs';
|
||||
|
||||
export const jobMethods = [
|
||||
method('BaseClassLabel', `if classId == "warrior" then
|
||||
return "전사"
|
||||
elseif classId == "rogue" then
|
||||
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
|
||||
local bid = self:PickNewRelic()
|
||||
if bid ~= "" then
|
||||
self:AddRelic(bid)
|
||||
local br = self.Relics[bid]
|
||||
if br ~= nil then
|
||||
self:Toast("유물 획득: " .. br.name)
|
||||
end
|
||||
end
|
||||
self:ContinueAfterBoss()
|
||||
else
|
||||
self:ShowJobSelect()
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||||
method('ShowJobSelect', `local current = self:CurrentClassId()
|
||||
local opts = nil
|
||||
if self.Jobs ~= nil then
|
||||
opts = self.Jobs[current]
|
||||
end
|
||||
if opts == nil then
|
||||
opts = {}
|
||||
end
|
||||
self.JobOpts = opts
|
||||
for i = 1, 3 do
|
||||
local base = "/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i)
|
||||
local o = opts[i]
|
||||
if o ~= nil then
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", o.name)
|
||||
self:SetText(base .. "/Desc", o.desc)
|
||||
local sc = nil
|
||||
if o.starter ~= nil and o.starter ~= "" then
|
||||
sc = self.Cards[o.starter]
|
||||
end
|
||||
if sc ~= nil then
|
||||
self:SetText(base .. "/Starter", "대표 카드: " .. sc.name)
|
||||
else
|
||||
self:SetText(base .. "/Starter", "대표 카드: 계보 유지")
|
||||
end
|
||||
else
|
||||
self:SetEntityEnabled(base, false)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", true)`),
|
||||
method('JobLabel', `if self.PlayerJob ~= "" and self.JobMeta ~= nil and self.JobMeta[self.PlayerJob] ~= nil then
|
||||
return self.JobMeta[self.PlayerJob].name
|
||||
end
|
||||
return self:BaseClassLabel(self.SelectedClass)`, [], 0, 'string'),
|
||||
method('SetJob', `local current = self:CurrentClassId()
|
||||
local starter = ""
|
||||
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
|
||||
if opts[i].id == jobId then
|
||||
starter = opts[i].starter or ""
|
||||
tier = opts[i].tier or 2
|
||||
break
|
||||
end
|
||||
end
|
||||
self.PlayerJob = jobId
|
||||
if starter ~= "" then
|
||||
table.insert(self.RunDeck, starter)
|
||||
local sc = self.Cards[starter]
|
||||
if sc ~= nil then
|
||||
self:Toast(tostring(tier) .. "차 전직: " .. self:JobLabel() .. "! 신규 카드 - " .. sc.name)
|
||||
end
|
||||
else
|
||||
self:Toast(tostring(tier) .. "차 전직: " .. self:JobLabel() .. "!")
|
||||
end
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||||
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", false)
|
||||
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' }]),
|
||||
];
|
||||
230
tools/deck/cb/map.mjs
Normal file
230
tools/deck/cb/map.mjs
Normal file
@@ -0,0 +1,230 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const mapMethods = [
|
||||
method('ShowMap', `self:ShowState("map")
|
||||
self:RenderMap()`),
|
||||
method('GenerateMap', `-- 절차 생성 — tools/map/rogue-map.mjs(JS 미러)와 로직 동기화 유지
|
||||
self.MapNodes = {}
|
||||
self.MapStart = {}
|
||||
self.VisitedNodes = {}
|
||||
self.Depth = 0
|
||||
self.MapNodes["boss"] = { type = "boss", row = ${MAP_ROWS} + 1, col = 0, next = {} }
|
||||
local cols = { 1, 2, 3, 4 }
|
||||
for i = #cols, 2, -1 do
|
||||
local j = math.random(1, i)
|
||||
cols[i], cols[j] = cols[j], cols[i]
|
||||
end
|
||||
local starts = { cols[1], cols[2], math.random(1, ${MAP_COLS}), math.random(1, ${MAP_COLS}) }
|
||||
for p = 1, 4 do
|
||||
local c = starts[p]
|
||||
local sid = "r1c" .. tostring(c)
|
||||
if self.MapNodes[sid] == nil then
|
||||
self.MapNodes[sid] = { type = "combat", row = 1, col = c, next = {} }
|
||||
end
|
||||
local found = false
|
||||
for i = 1, #self.MapStart do
|
||||
if self.MapStart[i] == sid then found = true end
|
||||
end
|
||||
if found == false then
|
||||
table.insert(self.MapStart, sid)
|
||||
end
|
||||
for r = 1, ${MAP_ROWS} - 1 do
|
||||
local nc = c + math.random(-1, 1)
|
||||
if nc < 1 then nc = 1 end
|
||||
if nc > ${MAP_COLS} then nc = ${MAP_COLS} end
|
||||
local nid = "r" .. tostring(r + 1) .. "c" .. tostring(nc)
|
||||
if self.MapNodes[nid] == nil then
|
||||
self.MapNodes[nid] = { type = "combat", row = r + 1, col = nc, next = {} }
|
||||
end
|
||||
local fid = "r" .. tostring(r) .. "c" .. tostring(c)
|
||||
local dup = false
|
||||
for i = 1, #self.MapNodes[fid].next do
|
||||
if self.MapNodes[fid].next[i] == nid then dup = true end
|
||||
end
|
||||
if dup == false then
|
||||
table.insert(self.MapNodes[fid].next, nid)
|
||||
end
|
||||
c = nc
|
||||
end
|
||||
local lid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c)
|
||||
local bdup = false
|
||||
for i = 1, #self.MapNodes[lid].next do
|
||||
if self.MapNodes[lid].next[i] == "boss" then bdup = true end
|
||||
end
|
||||
if bdup == false then
|
||||
table.insert(self.MapNodes[lid].next, "boss")
|
||||
end
|
||||
end
|
||||
for r = 3, ${MAP_ROWS} do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
local id = "r" .. tostring(r) .. "c" .. tostring(c)
|
||||
local node = self.MapNodes[id]
|
||||
if node ~= nil then
|
||||
-- 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지)
|
||||
local parentTypes = {}
|
||||
for pid, pn in pairs(self.MapNodes) do
|
||||
if pn.row == r - 1 then
|
||||
for i = 1, #pn.next do
|
||||
if pn.next[i] == id then parentTypes[pn.type] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
local w
|
||||
if r == ${MAP_ROWS} then
|
||||
w = { { "rest", 50 }, { "combat", 25 }, { "shop", 10 }, { "elite", 8 }, { "treasure", 7 } }
|
||||
elseif r >= 4 then
|
||||
w = { { "combat", 45 }, { "elite", 16 }, { "shop", 12 }, { "rest", 12 }, { "treasure", 15 } }
|
||||
else
|
||||
w = { { "combat", 45 }, { "shop", 12 }, { "rest", 12 } }
|
||||
end
|
||||
local total = 0
|
||||
for i = 1, #w do
|
||||
local t = w[i][1]
|
||||
if (t == "elite" or t == "rest" or t == "shop") and parentTypes[t] == true then
|
||||
w[i][2] = 0
|
||||
end
|
||||
total = total + w[i][2]
|
||||
end
|
||||
local roll = math.random() * total
|
||||
local acc = 0
|
||||
for i = 1, #w do
|
||||
acc = acc + w[i][2]
|
||||
if roll <= acc then
|
||||
node.type = w[i][1]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('IsReachable', `local list
|
||||
if self.CurrentNodeId == "" then
|
||||
list = self.MapStart
|
||||
else
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node == nil then
|
||||
return false
|
||||
end
|
||||
list = node.next
|
||||
end
|
||||
for i = 1, #list do
|
||||
if list[i] == id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
||||
method('RenderMapNode', `local base = "/ui/RunUIGroup/MapHud/Node_" .. id
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e == nil then
|
||||
return
|
||||
end
|
||||
local node = self.MapNodes[id]
|
||||
if node == nil then
|
||||
e.Enable = false
|
||||
return
|
||||
end
|
||||
e.Enable = true
|
||||
local ruid = self.NodeIcons[node.type]
|
||||
if ruid == nil then
|
||||
ruid = self.NodeIcons["combat"]
|
||||
end
|
||||
if e.SpriteGUIRendererComponent ~= nil and ruid ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ruid
|
||||
end
|
||||
local reachable = self:IsReachable(id)
|
||||
local visited = false
|
||||
if self.VisitedNodes ~= nil then
|
||||
for i = 1, #self.VisitedNodes do
|
||||
if self.VisitedNodes[i] == id then visited = true end
|
||||
end
|
||||
end
|
||||
if e.SpriteGUIRendererComponent ~= nil then
|
||||
if id == self.CurrentNodeId then
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
|
||||
elseif visited == true then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.9)
|
||||
elseif reachable == true then
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85)
|
||||
end
|
||||
end
|
||||
if (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then
|
||||
e.ButtonComponent.Enable = reachable
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
method('RenderMapDots', `local node = self.MapNodes[fromId]
|
||||
local has = false
|
||||
if node ~= nil then
|
||||
for i = 1, #node.next do
|
||||
if node.next[i] == toId then has = true end
|
||||
end
|
||||
end
|
||||
for k = 1, 3 do
|
||||
local d = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
|
||||
if d ~= nil then
|
||||
d.Enable = has
|
||||
if has == true and d.SpriteGUIRendererComponent ~= nil then
|
||||
if fromId == self.CurrentNodeId then
|
||||
d.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1)
|
||||
else
|
||||
d.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.8)
|
||||
end
|
||||
end
|
||||
end
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'dotId' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromId' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toId' },
|
||||
]),
|
||||
method('RenderMap', `for r = 1, ${MAP_ROWS} do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
self:RenderMapNode("r" .. tostring(r) .. "c" .. tostring(c))
|
||||
end
|
||||
end
|
||||
self:RenderMapNode("boss")
|
||||
for r = 1, ${MAP_ROWS} - 1 do
|
||||
for c = 1, ${MAP_COLS} do
|
||||
local fid = "r" .. tostring(r) .. "c" .. tostring(c)
|
||||
for c2 = c - 1, c + 1 do
|
||||
if c2 >= 1 and c2 <= ${MAP_COLS} then
|
||||
self:RenderMapDots(fid .. "_" .. tostring(c2), fid, "r" .. tostring(r + 1) .. "c" .. tostring(c2))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for c = 1, ${MAP_COLS} do
|
||||
local fid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c)
|
||||
self:RenderMapDots(fid .. "_b", fid, "boss")
|
||||
end
|
||||
`),
|
||||
method('PickNode', `if self.RunActive ~= true then
|
||||
return
|
||||
end
|
||||
if self:IsReachable(id) ~= true then
|
||||
return
|
||||
end
|
||||
self.CurrentNodeId = id
|
||||
if self.VisitedNodes == nil then
|
||||
self.VisitedNodes = {}
|
||||
end
|
||||
table.insert(self.VisitedNodes, id)
|
||||
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = false
|
||||
end
|
||||
local node = self.MapNodes[id]
|
||||
self.Depth = node.row
|
||||
self:RenderRun()
|
||||
if node.type == "shop" then
|
||||
self:ShowShop()
|
||||
elseif node.type == "rest" then
|
||||
self:ShowRest()
|
||||
elseif node.type == "treasure" then
|
||||
self:ShowTreasure()
|
||||
else
|
||||
self.CurrentEnemyId = ""
|
||||
self:StartCombat()
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||||
];
|
||||
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' }]),
|
||||
];
|
||||
296
tools/deck/cb/render.mjs
Normal file
296
tools/deck/cb/render.mjs
Normal file
@@ -0,0 +1,296 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const renderMethods = [
|
||||
method('BuffsLabel', `local parts = {}
|
||||
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
|
||||
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
|
||||
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
|
||||
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
|
||||
return table.concat(parts, " ")`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
|
||||
], 0, 'string'),
|
||||
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
|
||||
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i)
|
||||
local m = self.Monsters[i]
|
||||
if m ~= nil and m.alive == true then
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", m.name)
|
||||
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
|
||||
local intent = m.intents[m.intentIdx]
|
||||
local t = ""
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
local atk = intent.value + m.str
|
||||
if m.weak > 0 then atk = math.floor(atk * 0.75) end
|
||||
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
|
||||
t = "공격 " .. tostring(atk)
|
||||
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
|
||||
elseif intent.kind == "Debuff" then
|
||||
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
|
||||
else t = "취약 " .. tostring(intent.value) .. " 부여" end
|
||||
elseif intent.kind == "AddCard" then
|
||||
t = "저주 카드 추가"
|
||||
end
|
||||
end
|
||||
self:SetText(base .. "/Intent", t)
|
||||
local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
|
||||
local shownTarget = self.TargetIndex
|
||||
if dragActive == true then shownTarget = self.DragTargetIndex end
|
||||
self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive)
|
||||
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
|
||||
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
|
||||
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1)
|
||||
elseif intent.kind == "Debuff" then
|
||||
intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1)
|
||||
elseif intent.kind == "AddCard" then
|
||||
intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1)
|
||||
else
|
||||
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
|
||||
end
|
||||
end
|
||||
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
|
||||
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
|
||||
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
|
||||
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0))
|
||||
else
|
||||
self:SetEntityEnabled(base, false)
|
||||
end
|
||||
end
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
|
||||
self:SetHpBar("/ui/RunUIGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
|
||||
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 pb ~= "" then pb = pb .. " " end
|
||||
pb = pb .. "민첩+" .. tostring(self.PlayerDex)
|
||||
end
|
||||
if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then
|
||||
if pb ~= "" then pb = pb .. " " end
|
||||
pb = pb .. "가시" .. tostring(self.PlayerThorns)
|
||||
end
|
||||
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
|
||||
local names = {}
|
||||
for i = 1, #self.PlayerPowers do
|
||||
local pc = self.Cards[self.PlayerPowers[i]]
|
||||
if pc ~= nil then table.insert(names, pc.name) end
|
||||
end
|
||||
if pb ~= "" then pb = pb .. " · " end
|
||||
pb = pb .. table.concat(names, " ")
|
||||
end
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb)
|
||||
self:RenderRun()`),
|
||||
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
|
||||
local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey
|
||||
local pop = _EntityService:GetEntityByPath(base)
|
||||
if pop == nil then
|
||||
return
|
||||
end
|
||||
self.DmgPopSeq = (self.DmgPopSeq or 0) + 1
|
||||
local popSeq = self.DmgPopSeq
|
||||
self:SetText(base, "")
|
||||
local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
|
||||
local shown = tostring(math.max(0, math.floor(amount)))
|
||||
if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
|
||||
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
|
||||
end
|
||||
local digits = {}
|
||||
for i = 1, string.len(shown) do
|
||||
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
|
||||
end
|
||||
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
|
||||
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
|
||||
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
|
||||
self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false)
|
||||
end
|
||||
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
|
||||
local digitPath = base .. "/Digit" .. tostring(i)
|
||||
local digitEntity = _EntityService:GetEntityByPath(digitPath)
|
||||
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
|
||||
if digits[i] ~= nil then
|
||||
digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
|
||||
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
if digitEntity.UITransformComponent ~= nil then
|
||||
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
|
||||
end
|
||||
self:SetEntityEnabled(digitPath, true)
|
||||
else
|
||||
self:SetEntityEnabled(digitPath, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
local popPos = nil
|
||||
local m = self.Monsters[slot]
|
||||
if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
|
||||
local wp = m.entity.TransformComponent.WorldPosition
|
||||
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45}))
|
||||
popPos = _UILogic:ScreenToUIPosition(screen)
|
||||
else
|
||||
local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey)
|
||||
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
|
||||
local sp = slotEntity.UITransformComponent.anchoredPosition
|
||||
popPos = Vector2(sp.x, sp.y + 76)
|
||||
end
|
||||
end
|
||||
if pop ~= nil and pop.UITransformComponent ~= nil then
|
||||
if popPos ~= nil then
|
||||
pop.UITransformComponent.anchoredPosition = popPos
|
||||
else
|
||||
pop.UITransformComponent.anchoredPosition = Vector2(0, 120)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled(base, true)
|
||||
for i = 1, 6 do
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if self.DmgPopSeq ~= popSeq then
|
||||
return
|
||||
end
|
||||
local p = _EntityService:GetEntityByPath(base)
|
||||
if p ~= nil and p.UITransformComponent ~= nil then
|
||||
local cur = p.UITransformComponent.anchoredPosition
|
||||
p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
|
||||
end
|
||||
end, 0.045 * i)
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if self.DmgPopSeq ~= popSeq then
|
||||
return
|
||||
end
|
||||
self:SetEntityEnabled(base, false)
|
||||
end, 0.48)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||
]),
|
||||
method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop"
|
||||
if amount > 0 then
|
||||
self:SetText(base, "-" .. string.format("%d", amount))
|
||||
else
|
||||
self:SetText(base, "막음")
|
||||
end
|
||||
self:SetEntityEnabled(base, true)
|
||||
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||||
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
|
||||
if lp == nil then
|
||||
return
|
||||
end
|
||||
if lp.StateComponent == nil then
|
||||
return
|
||||
end
|
||||
pcall(function() lp.StateComponent:ChangeState("ATTACK") end)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then
|
||||
pcall(function() lp.StateComponent:ChangeState("IDLE") end)
|
||||
end
|
||||
end, 0.5)`),
|
||||
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer
|
||||
if lp == nil then
|
||||
return
|
||||
end
|
||||
if lp.StateComponent ~= nil then
|
||||
pcall(function() lp.StateComponent:ChangeState("HIT") end)
|
||||
end
|
||||
local tr = lp.TransformComponent
|
||||
if tr == nil then
|
||||
return
|
||||
end
|
||||
local p = tr.Position
|
||||
tr.Position = Vector3(p.x - 0.15, p.y, p.z)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then
|
||||
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z)
|
||||
end
|
||||
end, 0.15)`),
|
||||
method('MonsterLunge', `local m = self.Monsters[idx]
|
||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||
return
|
||||
end
|
||||
if m.motionBusy == true then
|
||||
return
|
||||
end
|
||||
m.motionBusy = true
|
||||
local e = m.entity
|
||||
local tr = e.TransformComponent
|
||||
if tr == nil then
|
||||
m.motionBusy = false
|
||||
return
|
||||
end
|
||||
local p = tr.Position
|
||||
tr.Position = Vector3(p.x - 0.35, p.y, p.z)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if isvalid(e) and e.TransformComponent ~= nil then
|
||||
e.TransformComponent.Position = Vector3(p.x, p.y, p.z)
|
||||
end
|
||||
m.motionBusy = false
|
||||
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]),
|
||||
method('MonsterHitMotion', `local m = self.Monsters[slot]
|
||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||
return
|
||||
end
|
||||
local e = m.entity
|
||||
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then
|
||||
e.SpriteRendererComponent.SpriteRUID = m.hitClip
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then
|
||||
e.SpriteRendererComponent.SpriteRUID = m.standClip
|
||||
end
|
||||
end, 0.5)
|
||||
else
|
||||
if m.motionBusy == true then
|
||||
return
|
||||
end
|
||||
m.motionBusy = true
|
||||
local tr = e.TransformComponent
|
||||
if tr == nil then
|
||||
m.motionBusy = false
|
||||
return
|
||||
end
|
||||
local p = tr.Position
|
||||
local seq = { 0.12, -0.12, 0 }
|
||||
for i = 1, #seq do
|
||||
local dx = seq[i]
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if isvalid(e) and e.TransformComponent ~= nil then
|
||||
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z)
|
||||
end
|
||||
if i == #seq then
|
||||
m.motionBusy = false
|
||||
end
|
||||
end, 0.06 * i)
|
||||
end
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
|
||||
if e == nil or e.UITransformComponent == nil then
|
||||
return
|
||||
end
|
||||
local ratio = 0
|
||||
if maxHp > 0 then ratio = hp / maxHp end
|
||||
if ratio < 0 then ratio = 0 end
|
||||
local w = width * ratio
|
||||
e.UITransformComponent.RectSize = Vector2(w, 14)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
|
||||
]),
|
||||
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
|
||||
self.TargetIndex = slot
|
||||
self:RenderCombat()
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층"
|
||||
if self.AscensionLevel > 0 then
|
||||
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
|
||||
end
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Floor", floorText)
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
|
||||
|
||||
60
tools/deck/cb/reward.mjs
Normal file
60
tools/deck/cb/reward.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const rewardMethods = [
|
||||
method('CardPool', `local pool = {}
|
||||
for id, c in pairs(self.Cards) do
|
||||
if c.token ~= true and self:CanUseClassCard(c.class) == true then
|
||||
table.insert(pool, id)
|
||||
end
|
||||
end
|
||||
table.sort(pool)
|
||||
return pool`, [], 0, 'any'),
|
||||
method('OfferReward', `self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", false)
|
||||
local pool = self:CardPool()
|
||||
local byRarity = {}
|
||||
for _, id in ipairs(pool) do
|
||||
local r = self.Cards[id].rarity or "normal"
|
||||
if byRarity[r] == nil then byRarity[r] = {} end
|
||||
table.insert(byRarity[r], id)
|
||||
end
|
||||
self.RewardChoices = {}
|
||||
for i = 1, 3 do
|
||||
local roll = math.random(1, 100)
|
||||
local want = "normal"
|
||||
if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end
|
||||
local bucket = byRarity[want]
|
||||
if bucket == nil or #bucket == 0 then bucket = pool end
|
||||
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
|
||||
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
||||
end
|
||||
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end`),
|
||||
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/RunUIGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||
]),
|
||||
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
||||
return
|
||||
end
|
||||
if slot ~= 0 and self.RewardChoices ~= nil then
|
||||
local id = self.RewardChoices[slot]
|
||||
if id ~= nil then
|
||||
table.insert(self.RunDeck, id)
|
||||
end
|
||||
end
|
||||
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
|
||||
hud.Enable = false
|
||||
end
|
||||
self:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
];
|
||||
259
tools/deck/cb/run.mjs
Normal file
259
tools/deck/cb/run.mjs
Normal file
@@ -0,0 +1,259 @@
|
||||
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, 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';
|
||||
|
||||
export const runMethods = [
|
||||
method('StartRun', `if self.SelectedClass == "magician" then
|
||||
self.PlayerMaxHp = ${CLASSES.magician.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} }
|
||||
elseif self.SelectedClass == "rogue" then
|
||||
self.PlayerMaxHp = ${CLASSES.rogue.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.rogue.map(luaStr).join(', ')} }
|
||||
else
|
||||
self.PlayerMaxHp = ${CLASSES.warrior.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} }
|
||||
end
|
||||
self.PlayerMaxHp = self.PlayerMaxHp - self:AscStartHpPenalty()
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
self.Gold = 0
|
||||
self.Floor = 1
|
||||
self.RunLength = ${ACT_COUNT}
|
||||
self.RunActive = true
|
||||
self.RunRelics = {}
|
||||
self.RunPotions = {}
|
||||
self.PotionSlots = ${POTIONS.baseSlots}
|
||||
${luaPotionsTable(POTIONS.potions)}
|
||||
${luaRelicsTable(RELICS.relics)}
|
||||
self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} }
|
||||
${luaEnemiesTable(ENEMIES.enemies)}
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self.PlayerJob = ""
|
||||
${luaJobsTable(JOBS)}
|
||||
${luaJobMetaTable(JOB_META)}
|
||||
${luaClassGroupsTable(CLASS_GROUPS)}
|
||||
${luaClassLineagesTable(CLASS_LINEAGES)}
|
||||
${luaFramesTable()}
|
||||
${luaNodeIconsTable()}
|
||||
${luaCharsTable()}
|
||||
self:GenerateMap()
|
||||
self:BindButtons()
|
||||
self:AddRelic("${RELICS.startingRelic}")
|
||||
self:ApplySoulUnlocks()
|
||||
self:RenderPotions()
|
||||
self:TeleportToActMap()
|
||||
self:ShowMap()`),
|
||||
method('KickCombatCamera', `local cam = nil
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then cam = lp.CameraComponent end
|
||||
if cam == nil then cam = _CameraService:GetCurrentCameraComponent() end
|
||||
if cam ~= nil then cam.ConfineCameraArea = false end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
local cc = nil
|
||||
local lp2 = _UserService.LocalPlayer
|
||||
if lp2 ~= nil then cc = lp2.CameraComponent end
|
||||
if cc == nil then cc = _CameraService:GetCurrentCameraComponent() end
|
||||
if cc ~= nil then
|
||||
cc.ZoomRatio = ${CAM.zoomRatio}
|
||||
cc.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY})
|
||||
cc.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY})
|
||||
cc.ConfineCameraArea = true
|
||||
end
|
||||
end, 0.2)`),
|
||||
method('StartCombat', `self:ShowState("combat")
|
||||
self:KickCombatCamera()
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/Result", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/TooltipBox", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/DiscardPrompt", false)
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||||
self.MaxEnergy = 3
|
||||
self.Turn = 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.PlayerDex = 0
|
||||
self.PlayerThorns = 0
|
||||
self.PlayerWeak = 0
|
||||
self.PlayerVuln = 0
|
||||
self.PlayerIntangible = 0
|
||||
self.BonusRewardScreens = 0
|
||||
self.ActiveKillReward = 0
|
||||
self.PlayerPowers = {}
|
||||
self.FightAttackCount = 0
|
||||
self.TurnAttackCardsPlayed = 0
|
||||
self.TurnDiscardedCards = 0
|
||||
self.TurnCardsPlayedThisTurn = 0
|
||||
self.DamageDealtThisTurn = 0
|
||||
self.DmgPopSeq = 0
|
||||
self.FirstHpLossDone = false
|
||||
self.ClayBlockNext = 0
|
||||
self.DiscardSelectRemaining = 0
|
||||
self.DiscardSelectTotal = 0
|
||||
self.DiscardPostShiv = 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.DiscardPile = {}
|
||||
self.ExhaustPile = {}
|
||||
self.Hand = {}
|
||||
${luaCardsTable(CARDS.cards)}
|
||||
self.DrawPile = {}
|
||||
for i = 1, #self.RunDeck do
|
||||
self.DrawPile[i] = self.RunDeck[i]
|
||||
end
|
||||
self:Shuffle(self.DrawPile)
|
||||
self:PrepareCombatDrawPile()
|
||||
self:BuildMonsters()
|
||||
self:RenderCombat()
|
||||
self:StartPlayerTurn()
|
||||
self:ApplyRelics("combatStart")
|
||||
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
|
||||
self.Registered = {}
|
||||
end
|
||||
local g = group
|
||||
if g == nil or g == "" then g = "combat" end
|
||||
local mp = mapName
|
||||
if mp == nil then mp = "" end
|
||||
table.insert(self.Registered, { entity = monster, enemyId = enemyId, group = g, map = mp })`, [
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'group' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'mapName' },
|
||||
]),
|
||||
method('BuildMonsters', `self.Monsters = {}
|
||||
local g = "combat"
|
||||
local node = self.MapNodes[self.CurrentNodeId]
|
||||
if node ~= nil and node.type ~= nil then g = node.type end
|
||||
local pmap = ""
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil and lp.CurrentMapName ~= nil then pmap = lp.CurrentMapName end
|
||||
local reg = self.Registered or {}
|
||||
for i = 1, #reg do
|
||||
if reg[i].entity ~= nil and isvalid(reg[i].entity) then
|
||||
reg[i].entity:SetVisible(false)
|
||||
end
|
||||
end
|
||||
local byGroup = {}
|
||||
for i = 1, #reg do
|
||||
local r = reg[i]
|
||||
if r.entity ~= nil and isvalid(r.entity) and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then
|
||||
local gg = r.group
|
||||
if gg == nil or gg == "" then gg = "combat" end
|
||||
if byGroup[gg] == nil then byGroup[gg] = {} end
|
||||
local x = 0
|
||||
if r.entity.TransformComponent ~= nil then
|
||||
x = r.entity.TransformComponent.WorldPosition.x
|
||||
end
|
||||
table.insert(byGroup[gg], { entity = r.entity, enemyId = r.enemyId, x = x })
|
||||
end
|
||||
end
|
||||
-- 노드 타입별 랜덤 구성: 일반 1~3 / 엘리트 1+일반0~2 / 보스 1
|
||||
local chosen = {}
|
||||
local function takeFrom(key, k)
|
||||
local src = byGroup[key] or {}
|
||||
local pool = {}
|
||||
for i = 1, #src do pool[i] = src[i] end
|
||||
self:Shuffle(pool)
|
||||
local taken = 0
|
||||
for i = 1, #pool do
|
||||
if taken >= k then break end
|
||||
table.insert(chosen, pool[i])
|
||||
taken = taken + 1
|
||||
end
|
||||
end
|
||||
if g == "boss" then
|
||||
takeFrom("boss", 1)
|
||||
elseif g == "elite" then
|
||||
takeFrom("elite", 1)
|
||||
takeFrom("combat", math.random(0, 2))
|
||||
else
|
||||
takeFrom("combat", math.random(1, 3))
|
||||
end
|
||||
if #chosen == 0 then takeFrom(g, 1) end
|
||||
if #chosen == 0 then takeFrom("combat", 1) end
|
||||
table.sort(chosen, function(a, b) return a.x < b.x end)
|
||||
local actMultipliers = { ${ACT_DIFFICULTY_MULTIPLIERS.join(', ')} }
|
||||
local mult = actMultipliers[self.Floor] or actMultipliers[#actMultipliers]
|
||||
if g == "elite" or g == "boss" then
|
||||
mult = mult + self:AscEliteBonus()
|
||||
end
|
||||
local n = #chosen
|
||||
if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end
|
||||
for i = 1, n do
|
||||
local item = chosen[i]
|
||||
local e = self.Enemies[item.enemyId]
|
||||
if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end
|
||||
local intents = {}
|
||||
for k = 1, #e.intents do
|
||||
local v = e.intents[k].value or 0
|
||||
if e.intents[k].kind == "Attack" then
|
||||
v = math.floor(v * mult * self:AscAtkMult())
|
||||
elseif e.intents[k].kind ~= "Debuff" then
|
||||
v = math.floor(v * mult)
|
||||
end
|
||||
intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect, card = e.intents[k].card, count = e.intents[k].count }
|
||||
end
|
||||
local maxHp = math.floor(e.maxHp * mult * self:AscHpMult())
|
||||
local hitClip = nil
|
||||
local standClip = nil
|
||||
if item.entity.StateAnimationComponent ~= nil then
|
||||
pcall(function()
|
||||
hitClip = item.entity.StateAnimationComponent.ActionSheet["hit"]
|
||||
standClip = item.entity.StateAnimationComponent.ActionSheet["stand"]
|
||||
end)
|
||||
end
|
||||
local startIdx = 1
|
||||
if #intents > 0 then startIdx = math.random(1, #intents) end
|
||||
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
||||
hp = maxHp, maxHp = maxHp, block = 0, str = e.str or 0, weak = 0, vuln = 0, poison = 0, artifact = e.artifact or 0,
|
||||
hitClip = hitClip, standClip = standClip, motionBusy = false,
|
||||
intents = intents, intentIdx = startIdx, alive = true, slot = i }
|
||||
self:ReviveMonsterEntity(item.entity)
|
||||
self:PositionMonsterSlot(i)
|
||||
end
|
||||
self.TargetIndex = 1`),
|
||||
method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then
|
||||
return
|
||||
end
|
||||
monster:SetEnable(true)
|
||||
monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]),
|
||||
];
|
||||
24
tools/deck/cb/runend.mjs
Normal file
24
tools/deck/cb/runend.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const runEndMethods = [
|
||||
method('ShowResult', `self:SetText("/ui/RunUIGroup/CombatHud/Result", text)
|
||||
local entity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/Result")
|
||||
if entity ~= nil then
|
||||
entity.Enable = true
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||||
method('EndRun', `local msg = text
|
||||
if text == "런 클리어!" and self.AscensionLevel >= self.AscensionUnlocked and self.AscensionUnlocked < 10 then
|
||||
self.AscensionUnlocked = self.AscensionUnlocked + 1
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
self:SaveAscension(self.AscensionUnlocked, lp.PlayerComponent.UserId)
|
||||
end
|
||||
self:RenderAscension()
|
||||
msg = "런 클리어! 승천 " .. string.format("%d", self.AscensionUnlocked) .. " 해금!"
|
||||
end
|
||||
self:ShowResult(msg)
|
||||
self.RunActive = false
|
||||
_TimerService:SetTimerOnce(function() self:ShowLobby() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||||
|
||||
174
tools/deck/cb/screens.mjs
Normal file
174
tools/deck/cb/screens.mjs
Normal file
@@ -0,0 +1,174 @@
|
||||
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 screensMethods = [
|
||||
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/RewardHud", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/MapHud", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/RestHud", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud", false)
|
||||
self:SetEntityEnabled("/ui/SelectUIGroup/JobChoiceHud", false)
|
||||
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", false)
|
||||
self:SetEntityEnabled("/ui/DeckUIGroup/DeckInspectHud", false)
|
||||
self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud", false)
|
||||
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", false)
|
||||
self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", 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()
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
|
||||
self:SetEntityEnabled("/ui/SelectUIGroup/CharacterSelectHud", state == "charselect")
|
||||
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", state == "lobby")
|
||||
if state == "map" then
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/MapHud", true)
|
||||
elseif state == "combat" then
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud", true)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", true)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/CardHand", true)
|
||||
elseif state == "shop" then
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud", true)
|
||||
elseif state == "rest" then
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/RestHud", true)
|
||||
elseif state == "treasure" then
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud", true)
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
|
||||
method('ShowMainMenu', `self.SelectedClass = ""
|
||||
self:RenderAscension()
|
||||
self:ShowState("menu")
|
||||
self:SetText("/ui/DefaultGroup/MainMenu/Title", "메이플 덱 어드벤처")
|
||||
self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱을 만들어 모험을 시작하세요")
|
||||
self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임")
|
||||
self:BindMenuButtons()`),
|
||||
method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton")
|
||||
if buttonEntity ~= nil and (buttonEntity.ButtonComponent ~= nil or buttonEntity:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.NewGameHandler ~= nil then
|
||||
buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)
|
||||
self.NewGameHandler = nil
|
||||
end
|
||||
self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
|
||||
end
|
||||
local warrior = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/WarriorButton")
|
||||
if warrior ~= nil and (warrior.ButtonComponent ~= nil or warrior:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.WarriorSelectHandler ~= nil then
|
||||
warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler)
|
||||
self.WarriorSelectHandler = nil
|
||||
end
|
||||
self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end)
|
||||
end
|
||||
local thief = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/BanditButton")
|
||||
if thief ~= nil and (thief.ButtonComponent ~= nil or thief:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.ThiefSelectHandler ~= nil then
|
||||
thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler)
|
||||
self.ThiefSelectHandler = nil
|
||||
end
|
||||
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("rogue") end)
|
||||
end
|
||||
local mage = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/MageButton")
|
||||
if mage ~= nil and (mage.ButtonComponent ~= nil or mage:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.MageSelectHandler ~= nil then
|
||||
mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler)
|
||||
self.MageSelectHandler = nil
|
||||
end
|
||||
self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end)
|
||||
end
|
||||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
|
||||
if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.AllDeckCloseHandler ~= nil then
|
||||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||
self.AllDeckCloseHandler = nil
|
||||
end
|
||||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||
end
|
||||
self:BindClassDeckTabs()
|
||||
local start = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/StartButton")
|
||||
if start ~= nil and (start.ButtonComponent ~= nil or start:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.StartGameHandler ~= nil then
|
||||
start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler)
|
||||
self.StartGameHandler = nil
|
||||
end
|
||||
self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)
|
||||
end
|
||||
local charBack = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/BackButton")
|
||||
if charBack ~= nil and (charBack.ButtonComponent ~= nil or charBack:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.CharBackHandler ~= nil then
|
||||
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
|
||||
self.CharBackHandler = nil
|
||||
end
|
||||
self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
|
||||
end
|
||||
local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus")
|
||||
if ascMinus ~= nil and (ascMinus.ButtonComponent ~= nil or ascMinus:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.AscMinusHandler ~= nil then
|
||||
ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler)
|
||||
self.AscMinusHandler = nil
|
||||
end
|
||||
self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end)
|
||||
end
|
||||
local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus")
|
||||
if ascPlus ~= nil and (ascPlus.ButtonComponent ~= nil or ascPlus:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.AscPlusHandler ~= nil then
|
||||
ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler)
|
||||
self.AscPlusHandler = nil
|
||||
end
|
||||
self.AscPlusHandler = ascPlus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(1) end)
|
||||
end`),
|
||||
method('ShowLobby', `self.SelectedClass = ""
|
||||
self:RenderAscension()
|
||||
self:RenderSoulLabel()
|
||||
self:ShowState("lobby")
|
||||
self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false)
|
||||
self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)
|
||||
self:BindLobbyButtons()
|
||||
self:BindMenuButtons()
|
||||
self:GoLobbyMap()`),
|
||||
method('RenderSoulLabel', `local soulPoints = self.SoulPoints or 0
|
||||
self:SetText("/ui/LobbyUIGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", soulPoints))
|
||||
self:SetText("/ui/LobbyUIGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", soulPoints))`),
|
||||
method('BindLobbyButtons', `if self.LobbyBound == true then
|
||||
return
|
||||
end
|
||||
self.LobbyBound = true
|
||||
local function bindClick(path, handler)
|
||||
local entity = _EntityService:GetEntityByPath(path)
|
||||
if entity ~= nil and (entity.ButtonComponent ~= nil or entity:AddComponent("ButtonComponent") ~= nil) then
|
||||
entity:ConnectEvent(ButtonClickEvent, handler)
|
||||
end
|
||||
end
|
||||
bindClick("/ui/LobbyUIGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end)
|
||||
bindClick("/ui/LobbyUIGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end)
|
||||
bindClick("/ui/LobbyUIGroup/BoardHud/Close", function() self:CloseBoard() end)
|
||||
bindClick("/ui/LobbyUIGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`),
|
||||
method('ShowCodex', `self.CodexMode = true
|
||||
self.ClassDeckMode = true
|
||||
local close = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
|
||||
if close ~= nil and (close.ButtonComponent ~= nil or close:AddComponent("ButtonComponent") ~= nil) then
|
||||
if self.AllDeckCloseHandler ~= nil then
|
||||
close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||||
end
|
||||
self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||||
end
|
||||
self:BindClassDeckTabs()
|
||||
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", false)
|
||||
self:SetClassDeckTab("warrior")
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
|
||||
if hud ~= nil then
|
||||
hud.Enable = true
|
||||
end
|
||||
self:RenderAllDeck()`),
|
||||
method('ShowBoard', `self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", true)`),
|
||||
method('CloseBoard', `self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false)`),
|
||||
];
|
||||
173
tools/deck/cb/shop.mjs
Normal file
173
tools/deck/cb/shop.mjs
Normal file
@@ -0,0 +1,173 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const shopMethods = [
|
||||
method('ShowShop', `local pool = self:CardPool()
|
||||
self.ShopChoices = {}
|
||||
self.ShopBought = { false, false, false }
|
||||
for i = 1, 3 do
|
||||
self.ShopChoices[i] = pool[math.random(1, #pool)]
|
||||
end
|
||||
self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)]
|
||||
self.ShopRelicBought = false
|
||||
local pkeys = {}
|
||||
for pid, _ in pairs(self.Potions) do
|
||||
table.insert(pkeys, pid)
|
||||
end
|
||||
table.sort(pkeys)
|
||||
self.ShopPotion = pkeys[math.random(1, #pkeys)]
|
||||
self.ShopPotionBought = false
|
||||
self:RenderShop()
|
||||
self:ShowState("shop")`),
|
||||
method('RenderShop', `self:SetText("/ui/RunUIGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold))
|
||||
for i = 1, 3 do
|
||||
local cid = self.ShopChoices[i]
|
||||
local c = self.Cards[cid]
|
||||
local base = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i)
|
||||
if c ~= nil then
|
||||
self:ApplyCardFace(base, cid)
|
||||
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopBought[i] == true then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local rr = self.Relics[self.ShopRelic]
|
||||
if rr ~= nil then
|
||||
self:SetText("/ui/RunUIGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
|
||||
self:SetText("/ui/RunUIGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
|
||||
local re = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Relic")
|
||||
if re ~= nil and re.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopRelicBought == true then
|
||||
re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
else
|
||||
re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
local pp = self.Potions[self.ShopPotion]
|
||||
if pp ~= nil then
|
||||
self:SetText("/ui/RunUIGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc)
|
||||
self:SetText("/ui/RunUIGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소")
|
||||
local pe = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Potion")
|
||||
if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopPotionBought == true then
|
||||
pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||
else
|
||||
pe.SpriteGUIRendererComponent.Color = Color(0.45, 0.7, 0.55, 1)
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('BuyRelic', `if self.ShopRelicBought == true then
|
||||
return
|
||||
end
|
||||
if self.Gold < ${RELIC_PRICE} then
|
||||
return
|
||||
end
|
||||
self.Gold = self.Gold - ${RELIC_PRICE}
|
||||
self:AddRelic(self.ShopRelic)
|
||||
self.ShopRelicBought = true
|
||||
self:RenderShop()
|
||||
self:RenderRun()`),
|
||||
method('BuyPotion', `if self.ShopPotionBought == true then
|
||||
return
|
||||
end
|
||||
if self.Gold < ${POTIONS.shopPrice} then
|
||||
return
|
||||
end
|
||||
if self.RunPotions ~= nil and #self.RunPotions >= self.PotionSlots then
|
||||
self:Toast("물약 슬롯이 가득 찼습니다")
|
||||
return
|
||||
end
|
||||
if self:AddPotion(self.ShopPotion) == true then
|
||||
self.Gold = self.Gold - ${POTIONS.shopPrice}
|
||||
self.ShopPotionBought = true
|
||||
end
|
||||
self:RenderShop()
|
||||
self:RenderRun()`),
|
||||
method('BuyCard', `if self.ShopBought == nil or self.ShopBought[slot] == true then
|
||||
return
|
||||
end
|
||||
if self.Gold < ${CARD_PRICE} then
|
||||
return
|
||||
end
|
||||
self.Gold = self.Gold - ${CARD_PRICE}
|
||||
table.insert(self.RunDeck, self.ShopChoices[slot])
|
||||
self.ShopBought[slot] = true
|
||||
self:RenderShop()
|
||||
self:RenderRun()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('ShowRest', `local old = self.PlayerHp
|
||||
self.PlayerHp = self.PlayerHp + ${REST_HEAL}
|
||||
if self.PlayerHp > self.PlayerMaxHp then
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
local healed = self.PlayerHp - old
|
||||
self:SetText("/ui/RunUIGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")")
|
||||
self:RenderCombat()
|
||||
self:ShowState("rest")`),
|
||||
method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud")
|
||||
if s ~= nil then
|
||||
s.Enable = false
|
||||
end
|
||||
local r = _EntityService:GetEntityByPath("/ui/RunUIGroup/RestHud")
|
||||
if r ~= nil then
|
||||
r.Enable = false
|
||||
end
|
||||
local t = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud")
|
||||
if t ~= nil then
|
||||
t.Enable = false
|
||||
end
|
||||
self:ShowMap()`),
|
||||
method('ShowTreasure', `self.ChestOpened = false
|
||||
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
|
||||
if chest ~= nil then
|
||||
if chest.SpriteGUIRendererComponent ~= nil then
|
||||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}"
|
||||
end
|
||||
if chest.UITransformComponent ~= nil then
|
||||
chest.UITransformComponent.anchoredPosition = Vector2(0, 40)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Reward", false)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Hint", true)
|
||||
self:ShowState("treasure")`),
|
||||
method('OpenChest', `if self.ChestOpened == true then
|
||||
return
|
||||
end
|
||||
self.ChestOpened = true
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Hint", false)
|
||||
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
|
||||
local steps = { 10, -10, 8, -8, 5, 0 }
|
||||
for i = 1, #steps do
|
||||
local dx = steps[i]
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if chest ~= nil and isvalid(chest) and chest.UITransformComponent ~= nil then
|
||||
chest.UITransformComponent.anchoredPosition = Vector2(dx, 40)
|
||||
end
|
||||
end, 0.08 * i)
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if chest ~= nil and isvalid(chest) and chest.SpriteGUIRendererComponent ~= nil then
|
||||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_OPEN_RUID}"
|
||||
end
|
||||
local g = 40 + math.random(0, 20)
|
||||
local nid = self:PickNewRelic()
|
||||
local msg = ""
|
||||
if nid ~= "" then
|
||||
self:AddRelic(nid)
|
||||
local nr = self.Relics[nid]
|
||||
msg = "유물 획득: " .. nr.name .. " · 메소 +" .. tostring(g)
|
||||
else
|
||||
g = g + 30
|
||||
msg = "메소 +" .. tostring(g)
|
||||
end
|
||||
self.Gold = self.Gold + g
|
||||
self:RenderRun()
|
||||
self:SetText("/ui/RunUIGroup/TreasureHud/Reward", msg)
|
||||
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Reward", true)
|
||||
end, 0.55)`),
|
||||
];
|
||||
114
tools/deck/cb/soul.mjs
Normal file
114
tools/deck/cb/soul.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const soulMethods = [
|
||||
method('ShowSoulShop', `self:RenderSoulLabel()
|
||||
self:RenderSoulShop()
|
||||
self:BindSoulShopButtons()
|
||||
self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", true)`),
|
||||
method('CloseSoulShop', `self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)`),
|
||||
method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||
local e1, pts = ds:GetAndWait("soulPoints")
|
||||
local e2, unl = ds:GetAndWait("soulUnlocks")
|
||||
local p = 0
|
||||
if e1 == 0 and pts ~= nil and pts ~= "" then p = tonumber(pts) or 0 end
|
||||
local u = ""
|
||||
if e2 == 0 and unl ~= nil then u = unl end
|
||||
self:RecvSouls(p, u, userId)`, [{ Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
|
||||
method('RecvSouls', `self.SoulPoints = p
|
||||
self.SoulUnlocks = {}
|
||||
if u ~= nil and u ~= "" then
|
||||
for key in string.gmatch(u, "([^,]+)") do
|
||||
self.SoulUnlocks[key] = true
|
||||
end
|
||||
end
|
||||
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 6),
|
||||
method('SaveSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||||
ds:SetAndWait("soulPoints", tostring(p))
|
||||
ds:SetAndWait("soulUnlocks", u)`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
|
||||
method('SerializeUnlocks', `local parts = {}
|
||||
if self.SoulUnlocks ~= nil then
|
||||
for k, v in pairs(self.SoulUnlocks) do
|
||||
if v == true then table.insert(parts, k) end
|
||||
end
|
||||
end
|
||||
return table.concat(parts, ",")`, [], 0, 'string'),
|
||||
method('AwardSouls', `self.SoulPoints = (self.SoulPoints or 0) + n
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
|
||||
end
|
||||
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]),
|
||||
method('BuySoulUnlock', `local d = nil
|
||||
if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end
|
||||
if d == nil then return end
|
||||
if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then
|
||||
self:Toast("이미 보유 중입니다")
|
||||
return
|
||||
end
|
||||
if (self.SoulPoints or 0) < d.cost then
|
||||
self:Toast("영혼이 부족합니다")
|
||||
return
|
||||
end
|
||||
self.SoulPoints = self.SoulPoints - d.cost
|
||||
if self.SoulUnlocks == nil then self.SoulUnlocks = {} end
|
||||
self.SoulUnlocks[d.key] = true
|
||||
local lp = _UserService.LocalPlayer
|
||||
if lp ~= nil then
|
||||
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
|
||||
end
|
||||
self:Toast(d.name .. " 해금!")
|
||||
self:RenderSoulLabel()
|
||||
self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]),
|
||||
method('RenderSoulShop', `local defs = self.SoulShopDef or {}
|
||||
for i = 1, 4 do
|
||||
local base = "/ui/LobbyUIGroup/SoulShopHud/Item" .. tostring(i)
|
||||
local d = defs[i]
|
||||
if d == nil then
|
||||
self:SetEntityEnabled(base, false)
|
||||
else
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", d.name)
|
||||
self:SetText(base .. "/Desc", d.desc)
|
||||
local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true
|
||||
if owned then
|
||||
self:SetText(base .. "/Status", "보유 중")
|
||||
elseif (self.SoulPoints or 0) >= d.cost then
|
||||
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매")
|
||||
else
|
||||
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족")
|
||||
end
|
||||
end
|
||||
end`),
|
||||
method('BindSoulShopButtons', `if self.SoulShopBound == true then
|
||||
return
|
||||
end
|
||||
self.SoulShopBound = true
|
||||
for i = 1, 4 do
|
||||
local idx = i
|
||||
local e = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/SoulShopHud/Item" .. tostring(i))
|
||||
if e ~= nil and (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then
|
||||
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
|
||||
end
|
||||
end`),
|
||||
method('ApplySoulUnlocks', `if self.SoulUnlocks == nil then return end
|
||||
if self.SoulUnlocks["meso"] == true then self.Gold = self.Gold + 60 end
|
||||
if self.SoulUnlocks["hp"] == true then
|
||||
self.PlayerMaxHp = self.PlayerMaxHp + 15
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
end
|
||||
if self.SoulUnlocks["trim"] == true then
|
||||
for i = 1, #self.RunDeck do
|
||||
local cid = self.RunDeck[i]
|
||||
if cid == "Defend" or cid == "MagicGuard" or cid == "DarkSight" then
|
||||
table.remove(self.RunDeck, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.SoulUnlocks["relic"] == true then
|
||||
local nid = self:PickNewRelic()
|
||||
if nid ~= "" then self:AddRelic(nid) end
|
||||
end`),
|
||||
];
|
||||
159
tools/deck/cb/tooltip.mjs
Normal file
159
tools/deck/cb/tooltip.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||
|
||||
export const tooltipMethods = [
|
||||
method('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
|
||||
return ""
|
||||
end
|
||||
local lines = {}
|
||||
local function add(name, desc)
|
||||
for i = 1, #lines do
|
||||
if string.find(lines[i], name .. ":", 1, true) == 1 then
|
||||
return
|
||||
end
|
||||
end
|
||||
table.insert(lines, name .. ": " .. desc)
|
||||
end
|
||||
local cardDesc = c.desc or ""
|
||||
if c.sly == true or string.find(cardDesc, "교활", 1, true) ~= nil then
|
||||
add("교활", "버려지면 비용 없이 사용됩니다.")
|
||||
end
|
||||
if c.retain == true or string.find(cardDesc, "보존", 1, true) ~= nil then
|
||||
add("보존", "턴 종료 시 버려지지 않고 손에 남습니다.")
|
||||
end
|
||||
if c.dex ~= nil and c.dex > 0 or string.find(cardDesc, "민첩", 1, true) ~= nil then
|
||||
add("민첩", "카드로 얻는 방어도가 증가합니다.")
|
||||
end
|
||||
if c.thorns ~= nil and c.thorns > 0 or string.find(cardDesc, "가시", 1, true) ~= nil then
|
||||
add("가시", "피해를 받으면 공격자에게 반사 피해를 줍니다.")
|
||||
end
|
||||
if c.exhaust == true or string.find(cardDesc, "소멸.", 1, true) ~= nil then
|
||||
add("소멸", "사용 후 소멸 덱으로 이동해 이번 전투 동안 다시 나오지 않습니다.")
|
||||
end
|
||||
if string.find(cardDesc, "선천성", 1, true) ~= nil then
|
||||
add("선천성", "전투 시작 시 손패에 들어옵니다.")
|
||||
end
|
||||
if c.vuln ~= nil and c.vuln > 0 then
|
||||
add("취약", "받는 공격 피해가 50% 증가합니다.")
|
||||
end
|
||||
if c.weak ~= nil and c.weak > 0 then
|
||||
add("약화", "주는 공격 피해가 25% 감소합니다.")
|
||||
end
|
||||
if c.poison ~= nil and c.poison > 0 then
|
||||
add("중독", "턴 시작 시 체력을 잃고 수치가 1 감소합니다.")
|
||||
end
|
||||
if c.pierce == true then
|
||||
add("관통", "방어도를 무시하고 피해를 줍니다.")
|
||||
end
|
||||
if c.aoe == true then
|
||||
add("전체", "모든 적에게 적용됩니다.")
|
||||
end
|
||||
if c.kind == "Power" then
|
||||
add("파워", "사용하면 전투 동안 지속 효과로 남습니다.")
|
||||
end
|
||||
if c.unplayable == true then
|
||||
add("저주", "사용할 수 없고 손패를 방해합니다.")
|
||||
end
|
||||
local out = ""
|
||||
for i = 1, #lines do
|
||||
if i > 1 then out = out .. "\\n" end
|
||||
out = out .. lines[i]
|
||||
end
|
||||
return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'string'),
|
||||
method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||||
return
|
||||
end
|
||||
if self.Hand == nil then
|
||||
return
|
||||
end
|
||||
local cardId = self.Hand[slot]
|
||||
if cardId == nil then
|
||||
return
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||
local tx = 0
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
tx = e.UITransformComponent.anchoredPosition.x
|
||||
e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1)
|
||||
end
|
||||
local c = self.Cards[cardId]
|
||||
if c ~= nil then
|
||||
local tip = self:BuildCardKeywordTooltip(c)
|
||||
if tip ~= "" then
|
||||
local tipX = tx + 270
|
||||
if tx > 180 then tipX = tx - 270 end
|
||||
if tipX > 760 then tipX = tx - 270 end
|
||||
if tipX < -760 then tipX = tx + 270 end
|
||||
self:ShowTooltipAt("키워드", tip, tipX, 90)
|
||||
else
|
||||
self:HideTooltip()
|
||||
end
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||||
end
|
||||
self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('ShowTooltip', `self:ShowTooltipAt(name, desc, x, 400)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
|
||||
]),
|
||||
method('ShowTooltipAt', `self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Name", name)
|
||||
self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Desc", desc)
|
||||
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TooltipBox")
|
||||
if e ~= nil then
|
||||
if e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.anchoredPosition = Vector2(x, y)
|
||||
end
|
||||
e.Enable = true
|
||||
end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' },
|
||||
]),
|
||||
method('HideTooltip', `self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/TooltipBox", false)`),
|
||||
];
|
||||
File diff suppressed because it is too large
Load Diff
58
tools/deck/legacy/hud/board.mjs
Normal file
58
tools/deck/legacy/hud/board.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
|
||||
|
||||
export function buildBoard() {
|
||||
const board = [];
|
||||
let brdId = 0;
|
||||
const boardRoot = entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 14,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.95 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
boardRoot.jsonString.enable = false;
|
||||
board.push(boardRoot);
|
||||
board.push(entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud/Title',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '게시판', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
board.push(entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud/Body',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1100, y: 520 }, pos: { x: 0, y: 20 } }),
|
||||
sprite({ color: { r: 0.1, g: 0.12, b: 0.16, a: 0.9 }, type: 1 }),
|
||||
text({ value: '· 카드는 직업/등급에 따라 보상 확률이 다릅니다.\n· 몬스터는 매 턴 정해진 행동 중 하나를 무작위로 합니다.\n· 일부 몬스터는 덱에 저주 카드(상처/화상)를 넣습니다.\n· 손패는 최대 10장, 초과분은 자동으로 버려집니다.\n· 2차 전직 후 보스를 잡으면 영혼이 쌓입니다.\n· 영혼은 상인 NPC에서 덱빌딩 해금에 사용합니다.', fontSize: 24, bold: false, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
board.push(entity({
|
||||
id: guid('brd', brdId++),
|
||||
path: '/ui/DefaultGroup/BoardHud/Close',
|
||||
modelId: 'uibutton', entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
return board;
|
||||
}
|
||||
494
tools/deck/legacy/hud/combat.mjs
Normal file
494
tools/deck/legacy/hud/combat.mjs
Normal file
@@ -0,0 +1,494 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
|
||||
|
||||
export function buildCombat() {
|
||||
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
|
||||
const combat = [];
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 0),
|
||||
path: '/ui/DefaultGroup/CombatHud',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
],
|
||||
}));
|
||||
const SLOT_W = 140, SLOT_H = 96;
|
||||
for (let i = 1; i <= MAX_MONSTERS; i++) {
|
||||
const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`;
|
||||
const slot = entity({
|
||||
id: guid('cmb', 40 + i),
|
||||
path: base,
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: 20 + i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }),
|
||||
sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
});
|
||||
slot.jsonString.enable = false;
|
||||
combat.push(slot);
|
||||
const targetFrame = entity({
|
||||
id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
targetFrame.jsonString.enable = false;
|
||||
combat.push(targetFrame);
|
||||
const targetMarker = entity({
|
||||
id: guid('cmb', 360 + i), path: `${base}/TargetMarker`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 116, y: 116 }, pos: { x: 0, y: 2 } }),
|
||||
sprite({ color: { r: 0.95, g: 0.08, b: 0.05, a: 0.92 }, type: 1 }),
|
||||
text({ value: '+', fontSize: 72, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 4 }),
|
||||
],
|
||||
});
|
||||
targetMarker.jsonString.enable = false;
|
||||
combat.push(targetMarker);
|
||||
const targetLabel = entity({
|
||||
id: guid('cmb', 370 + i), path: `${base}/TargetMarker/Label`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 10,
|
||||
components: [
|
||||
transform({ parentW: 116, parentH: 116, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 112, y: 28 }, pos: { x: 0, y: -52 } }),
|
||||
sprite({ color: { r: 0.08, g: 0.02, b: 0.02, a: 0.86 }, type: 1 }),
|
||||
text({ value: 'TARGET', fontSize: 18, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 3 }),
|
||||
],
|
||||
});
|
||||
targetLabel.jsonString.enable = false;
|
||||
combat.push(targetLabel);
|
||||
const actFrame = entity({
|
||||
id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: { r: 0.95, g: 0.3, b: 0.25, a: 0.3 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
actFrame.jsonString.enable = false;
|
||||
combat.push(actFrame);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 30 }, pos: { x: 0, y: 34 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 26 }, pos: { x: 0, y: 6 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }),
|
||||
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }),
|
||||
sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 40, y: 24 }, pos: { x: 0, y: -36 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const dmgPopBase = `/ui/DefaultGroup/CombatHud/DmgPop${i}`;
|
||||
const dmgPop = entity({
|
||||
id: guid('cmb', 250 + i), path: dmgPopBase, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 80 + i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 192, y: 56 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1 }),
|
||||
],
|
||||
});
|
||||
dmgPop.jsonString.enable = false;
|
||||
combat.push(dmgPop);
|
||||
for (let d = 0; d < DAMAGE_POP_MAX_DIGITS; d++) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 380 + i * 10 + d), path: `${dmgPopBase}/Digit${d + 1}`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 90 + i,
|
||||
components: [
|
||||
transform({ parentW: 192, parentH: 56, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: DAMAGE_POP_DIGIT_W, y: DAMAGE_POP_DIGIT_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ dataId: DAMAGE_DIGIT_RUIDS[0], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
const mBlockBadge = entity({
|
||||
id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 6,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 36 }, pos: { x: -HP_BAR_W / 2 - 30, y: -14 } }),
|
||||
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
mBlockBadge.jsonString.enable = false;
|
||||
combat.push(mBlockBadge);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 280 + i), path: `${base}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 40, parentH: 36, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 32 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '0', fontSize: 17, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 290 + i), path: `${base}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 7,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 60, y: 22 }, pos: { x: 0, y: -58 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 15, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel';
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 210), path: PP, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 96 }, pos: { x: -760, y: -494 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: PANEL_BG, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }),
|
||||
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 14 }, pos: { x: -94, y: -6 } }),
|
||||
sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const blockBadge = entity({
|
||||
id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }),
|
||||
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
blockBadge.jsonString.enable = false;
|
||||
combat.push(blockBadge);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 217), path: `${PP}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 6,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 22 }, pos: { x: 0, y: -44 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 14, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const playerDmgPop = entity({
|
||||
id: guid('cmb', 260), path: `${PP}/DmgPop`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 30 }, pos: { x: 16, y: 40 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 22, bold: true, color: { r: 1, g: 0.4, b: 0.35, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
playerDmgPop.jsonString.enable = false;
|
||||
combat.push(playerDmgPop);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 200),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
const topTexts = [
|
||||
['Floor', -520, 160, '막 1/3', GOLD],
|
||||
['Gold', -360, 160, '메소 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
|
||||
];
|
||||
topTexts.forEach(([suffix, x, w, value, color], ti) => {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 201 + ti),
|
||||
path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`,
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: ti,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize: 22, bold: true, color, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 209),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar/MesoIcon',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 26, y: 26 }, pos: { x: -432, y: 0 } }),
|
||||
sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 300 + i),
|
||||
path: `/ui/DefaultGroup/CombatHud/TopBar/RelicSlot${i}`,
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent',
|
||||
displayOrder: 3 + i,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: -240 + (i - 1) * 48, y: 0 } }),
|
||||
sprite({ color: { r: 0.15, g: 0.16, b: 0.2, a: 0.6 }, type: 0, raycast: true }),
|
||||
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
|
||||
],
|
||||
}));
|
||||
}
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 311),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 14,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 60, y: 30 }, pos: { x: 192, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 18, bold: true, color: { r: 0.8, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 320 + i),
|
||||
path: `/ui/DefaultGroup/CombatHud/TopBar/PotionSlot${i}`,
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent',
|
||||
displayOrder: 14 + i,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: 240 + (i - 1) * 44, y: 0 } }),
|
||||
sprite({ color: { r: 0.22, g: 0.25, b: 0.3, a: 0.9 }, type: 0, raycast: true }),
|
||||
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
|
||||
],
|
||||
}));
|
||||
}
|
||||
const tooltipBox = entity({
|
||||
id: guid('cmb', 330),
|
||||
path: '/ui/DefaultGroup/CombatHud/TooltipBox',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 20,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 150 }, pos: { x: 0, y: 400 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.04, g: 0.05, b: 0.08, a: 0.96 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
tooltipBox.jsonString.enable = false;
|
||||
combat.push(tooltipBox);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 331),
|
||||
path: '/ui/DefaultGroup/CombatHud/TooltipBox/Name',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 28 }, pos: { x: 0, y: 52 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 19, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 332),
|
||||
path: '/ui/DefaultGroup/CombatHud/TooltipBox/Desc',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 102 }, pos: { x: 0, y: -18 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
const discardPrompt = entity({
|
||||
id: guid('cmb', 333),
|
||||
path: '/ui/DefaultGroup/CombatHud/DiscardPrompt',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 22,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 520, y: 48 }, pos: { x: 0, y: -260 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.86 }, type: 1 }),
|
||||
text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
discardPrompt.jsonString.enable = false;
|
||||
combat.push(discardPrompt);
|
||||
const potionMenu = entity({
|
||||
id: guid('cmb', 340),
|
||||
path: '/ui/DefaultGroup/CombatHud/PotionMenu',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 21,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 180 }, pos: { x: 0, y: 120 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.07, g: 0.08, b: 0.12, a: 0.97 }, type: 1 }),
|
||||
],
|
||||
});
|
||||
potionMenu.jsonString.enable = false;
|
||||
combat.push(potionMenu);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 341),
|
||||
path: '/ui/DefaultGroup/CombatHud/PotionMenu/Title',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 36 }, pos: { x: 0, y: 52 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 19, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const pmButtons = [
|
||||
['Use', '사용', -120, { r: 0.32, g: 0.55, b: 0.36, a: 1 }],
|
||||
['Toss', '버리기', 0, { r: 0.6, g: 0.32, b: 0.3, a: 1 }],
|
||||
['Close', '닫기', 120, { r: 0.25, g: 0.28, b: 0.35, a: 1 }],
|
||||
];
|
||||
pmButtons.forEach(([suffix, label, x, color], bi) => {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 342 + bi),
|
||||
path: `/ui/DefaultGroup/CombatHud/PotionMenu/${suffix}`,
|
||||
modelId: 'uibutton', entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1 + bi,
|
||||
components: [
|
||||
transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 104, y: 46 }, pos: { x, y: -40 } }),
|
||||
sprite({ color, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: label, fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 205),
|
||||
path: '/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton',
|
||||
modelId: 'uibutton', entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 140, y: 40 }, pos: { x: 528, y: 0 } }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '모든덱보기', fontSize: 18, bold: true, color: GOLD, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
const skillFx = entity({
|
||||
id: guid('cmb', 230), path: '/ui/DefaultGroup/CombatHud/SkillFx',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 30,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 110, y: 110 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }),
|
||||
],
|
||||
});
|
||||
skillFx.jsonString.enable = false;
|
||||
combat.push(skillFx);
|
||||
const result = entity({
|
||||
id: guid('cmb', 2),
|
||||
path: '/ui/DefaultGroup/CombatHud/Result',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 8,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
result.jsonString.enable = false;
|
||||
combat.push(result);
|
||||
return combat;
|
||||
}
|
||||
160
tools/deck/legacy/hud/deckall.mjs
Normal file
160
tools/deck/legacy/hud/deckall.mjs
Normal file
@@ -0,0 +1,160 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
|
||||
|
||||
export function buildDeckAll() {
|
||||
const allDeck = [];
|
||||
const allHud = entity({
|
||||
id: guid('all', 0),
|
||||
path: '/ui/DefaultGroup/DeckAllHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 16,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
allHud.jsonString.enable = false;
|
||||
allDeck.push(allHud);
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 1),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Panel',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1080, y: 800 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 2),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Title',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 54 }, pos: { x: 0, y: 380 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '모든 덱', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 3),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Close',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 486, y: 380 } }),
|
||||
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const deckTabs = [
|
||||
{ key: 'Warrior', label: '전사', x: -210 },
|
||||
{ key: 'Thief', label: '도적', x: 0 },
|
||||
{ key: 'Mage', label: '마법사', x: 210 },
|
||||
];
|
||||
for (let i = 0; i < deckTabs.length; i++) {
|
||||
const tab = deckTabs[i];
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 10 + i),
|
||||
path: `/ui/DefaultGroup/DeckAllHud/${tab.key}Tab`,
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3 + i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 170, y: 46 }, pos: { x: tab.x, y: 318 } }),
|
||||
sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: tab.label, fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 4),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Empty',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 40 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '덱이 없습니다', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
allDeck.push(entity({
|
||||
id: guid('all', 5),
|
||||
path: '/ui/DefaultGroup/DeckAllHud/Grid',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 980, y: 620 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
|
||||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||||
],
|
||||
}));
|
||||
const ALL_DECK_CARD_COUNT = 120;
|
||||
const ALL_DECK_CARD_W = 158;
|
||||
const ALL_DECK_CARD_H = 214;
|
||||
// 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조
|
||||
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
|
||||
const allBase = 6 + (i - 1) * 7;
|
||||
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
|
||||
const card = entity({
|
||||
id: guid('all2', allBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
});
|
||||
card.jsonString.enable = false;
|
||||
allDeck.push(card);
|
||||
const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W);
|
||||
for (const [tIdx, [suffix, cfg]] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
allDeck.push(entity({
|
||||
id: guid('all2', allBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: dOrder,
|
||||
components: [
|
||||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
allDeck.push(entity({
|
||||
id: guid('all2', allBase + 6),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: allDeckLayout.art.size, pos: allDeckLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
return allDeck;
|
||||
}
|
||||
122
tools/deck/legacy/hud/deckhud.mjs
Normal file
122
tools/deck/legacy/hud/deckhud.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
|
||||
|
||||
export function buildDeckHud() {
|
||||
const hud = [];
|
||||
const add = (e) => hud.push(e);
|
||||
|
||||
add(entity({
|
||||
id: guid('hud', 0),
|
||||
path: '/ui/DefaultGroup/DeckHud',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
],
|
||||
}));
|
||||
|
||||
for (const pile of [
|
||||
{ key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.20, b: 0.25, a: 1 } },
|
||||
{ key: 'ExhaustPile', x: 430, label: '소멸 덱', count: '0', color: { r: 0.13, g: 0.13, b: 0.18, a: 1 } },
|
||||
{ key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } },
|
||||
]) {
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: `/ui/DefaultGroup/DeckHud/${pile.key}`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: pile.key === 'DrawPile' ? 0 : pile.key === 'ExhaustPile' ? 1 : 2,
|
||||
components: [
|
||||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: pile.color, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: pile.count, fontSize: 42, bold: true }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EndTurnButton',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 160 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '턴 종료', fontSize: 28, bold: true, color: GOLD, alignment: 0 }),
|
||||
],
|
||||
}));
|
||||
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 160 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
add(entity({
|
||||
id: guid('hud', hud.length),
|
||||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
|
||||
return hud;
|
||||
}
|
||||
138
tools/deck/legacy/hud/deckinspect.mjs
Normal file
138
tools/deck/legacy/hud/deckinspect.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
|
||||
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
|
||||
|
||||
export function buildDeckInspect() {
|
||||
const inspect = [];
|
||||
const inspectHud = entity({
|
||||
id: guid('ins', 0),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 15,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
inspectHud.jsonString.enable = false;
|
||||
inspect.push(inspectHud);
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 1),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Panel',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1040, y: 760 }, pos: { x: 0, y: 10 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 2),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Title',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 54 }, pos: { x: 0, y: 350 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '\uB371 \uBCF4\uAE30', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 3),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Close',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 466, y: 350 } }),
|
||||
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 4),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Empty',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '\uCE74\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
inspect.push(entity({
|
||||
id: guid('ins', 5),
|
||||
path: '/ui/DefaultGroup/DeckInspectHud/Grid',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 950, y: 610 }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
|
||||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||||
],
|
||||
}));
|
||||
const INSPECT_CARD_COUNT = 60;
|
||||
const INSPECT_CARD_W = 158;
|
||||
const INSPECT_CARD_H = 214;
|
||||
// 카드 단위 엔티티는 v2 네임스페이스(ins2/all2/rwd2/shp2) — 자식 구성이 바뀌면 id를 통째로 새로 발급해야 함.
|
||||
// 구 id를 다른 path에 재사용하면 메이커 refresh의 id 기준 in-place 병합이 꼬여 자식이 소실됨 (P13 실측).
|
||||
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
|
||||
const insBase = 6 + (i - 1) * 7;
|
||||
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
|
||||
const card = entity({
|
||||
id: guid('ins2', insBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
|
||||
],
|
||||
});
|
||||
card.jsonString.enable = false;
|
||||
inspect.push(card);
|
||||
const inspectLayout = cardFaceLayout(INSPECT_CARD_W);
|
||||
for (const [tIdx, [suffix, cfg]] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
inspect.push(entity({
|
||||
id: guid('ins2', insBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: dOrder,
|
||||
components: [
|
||||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
inspect.push(entity({
|
||||
id: guid('ins2', insBase + 6),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: inspectLayout.art.size, pos: inspectLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
return inspect;
|
||||
}
|
||||
51
tools/deck/legacy/hud/jobchoice.mjs
Normal file
51
tools/deck/legacy/hud/jobchoice.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
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 buildJobChoice() {
|
||||
const jobChoice = [];
|
||||
const jobChoiceHud = entity({
|
||||
id: guid('job', 0),
|
||||
path: '/ui/DefaultGroup/JobChoiceHud',
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 9,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }),
|
||||
],
|
||||
});
|
||||
jobChoiceHud.jsonString.enable = false;
|
||||
jobChoice.push(jobChoiceHud);
|
||||
jobChoice.push(entity({
|
||||
id: guid('job', 1),
|
||||
path: '/ui/DefaultGroup/JobChoiceHud/Title',
|
||||
modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 220 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '보스 처치 보상을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const jcButtons = [
|
||||
['RelicButton', '유물 획득', -240, { r: 0.7, g: 0.55, b: 0.85, a: 1 }],
|
||||
['JobButton', '2차 전직', 240, { r: 0.86, g: 0.6, b: 0.3, a: 1 }],
|
||||
];
|
||||
jcButtons.forEach(([suffix, label, x, color], bi) => {
|
||||
jobChoice.push(entity({
|
||||
id: guid('job', 2 + bi),
|
||||
path: `/ui/DefaultGroup/JobChoiceHud/${suffix}`,
|
||||
modelId: 'uibutton', entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1 + bi,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 140 }, pos: { x, y: 0 } }),
|
||||
sprite({ color, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: label, fontSize: 32, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
return jobChoice;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user