1 Commits

90 changed files with 250371 additions and 257106 deletions

4
.gitignore vendored
View File

@@ -7,8 +7,6 @@
# Claude Code 로컬 설정 — 단, 팀 공유 하네스 설정(settings.json)은 커밋 (RULES.md 참조)
.claude/*
!.claude/settings.json
# 개인 스킬(superpowers) 브레인스토밍/계획 산출물 — 로컬 전용, 협업 공유 X (프로젝트 설계 문서 docs/*.md 는 추적 유지)
docs/superpowers/
# === OS / 에디터 잡파일 ===
Thumbs.db
@@ -25,5 +23,3 @@ AGENTS.md
Environment/
McpScreenshots/
*.log
# 메이커가 재편(reorg) 중 부모를 잃은 엔티티를 모아두는 임시 폴더 (잡파일)
Mislocated/

View File

@@ -1,142 +0,0 @@
{
"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": []
}
}
}

View File

@@ -32,10 +32,10 @@
{
"@type": "script.SlayDeckController",
"Enable": true,
"Energy": 0,
"MaxEnergy": 3,
"Turn": 0,
"TweenEventId": 0
"Energy": 0.0,
"MaxEnergy": 3.0,
"Turn": 0.0,
"TweenEventId": 0.0
}
],
"@version": 1

View File

@@ -44,8 +44,8 @@ git pull
```
slaymaple/
├── data/ # 게임 데이터 단일 소스 (생성기가 읽어 주입). 맵은 정적 데이터 없음(절차 생성)
│ ├── cards.json # 카드 121장(클래스·2차전직별 + 저주) + 클래스별 시작 덱
│ ├── enemies.json # 적 18종(일반/정예/보스, 디버프 인텐트 포함)
│ ├── cards.json # 카드 122장(클래스·2차전직별 + 저주) + 클래스별 시작 덱
│ ├── enemies.json # 적 12종(일반/정예/보스, 디버프 인텐트 포함)
│ ├── potions.json # 물약 6종 + 드랍률·슬롯·상점가
│ ├── relics.json # 유물 19종(StS 효과 × 메이플 장비) + 시작 유물 + 풀
│ ├── cardframes.json # 커스텀 카드 프레임 3종(전사/마법사/도적 × normal/unique/legend) + 보상 등급 가중치
@@ -77,9 +77,9 @@ slaymaple/
│ ├── player/ # gen-player-lock.mjs(전투맵 입력 잠금) · freeze-turn-player.mjs(모델 이동 정지) · gen-lobby-npc.mjs(LobbyNpc·LobbyMobility codeblock)
│ ├── monster/ # gen-combat-monster.mjs(EnemyId 마커) · freeze-turn-monsters.mjs(필드 AI 정지)
│ ├── balance/ # sim-balance.mjs(전투 밸런스 몬테카를로 시뮬) · sim-balance.test.mjs
│ ├── verify/ # count.mjs·uimap.mjs·cbgap.mjs(산출물 카운트/UIGroup 매핑/재연결 GAP 검증 — 경로 내장)
│ ├── verify/ # count.mjs(산출물 카운트 검증 헬퍼 — 경로 내장)
│ └── git/ # gitea-pr.mjs(UTF-8 안전 PR 생성/수정/머지 — RULES.md 참조)
├── ui/ # UIGroup 7종 — 메이커 저작(Default/Select/Lobby/Run/Deck/Popup/Toast)
├── ui/ # UI 그룹 (DefaultGroup 8.3MB 산출물 / PopupGroup / ToastGroup)
├── docs/
│ ├── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서
│ ├── ui-generation-structure.md # UI 생성 구조 문서
@@ -89,22 +89,14 @@ slaymaple/
└── README.md
```
> ⚠️ **`map/*.map` · `SlayDeckController.codeblock` · `Global/common.gamelogic`는 생성 산출물**입니다 — 직접 편집하면 재생성 때 사라집니다. 게임 로직 변경은 `data/*.json`·`tools/`의 생성기를 고 재생성하세요. **`ui/*.ui`는 메이커 저작**(생성기 미생성)이라 메이커에서만 편집합니다(자세한 규칙은 [`RULES.md`](RULES.md)).
> ⚠️ **`map/*.map` · `ui/DefaultGroup.ui` · `*.codeblock` · `Global/*.gamelogic`는 생성 산출물**입니다 — 직접 편집하면 다음 재생성 때 사라집니다. 게임 변경은 `data/*.json` 또는 `tools/`의 생성기를 고친 뒤 재생성하세요(자세한 규칙은 [`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 차용)** — **파이터**: 공격을 *연속*으로 내면 콤보가 쌓이고(방어·파워 등 비공격 카드를 쓰면 콤보 리셋) 콤보로 데미지 증가 버프 = 브루저. **페이지**: 위협 디버프로 버티며 방어도 축적 → **바디 슬램(방어 비례 피해)** 카운터. **스피어맨**: 하이퍼바디·아이언월 유지/리치형.
- **🗡️ 도적 (단검·독, Silent 차용)** — 표창 난사 / 독 / 교활·버림. **어쌔신**(표창·크리·흡혈) / **시프**(단검 난타·독). *형 구현 완료(Silent 86장)*.
- **🔮 법사 (약체·게이지, Defect 차용)** — **위자드(불/독)**: 독을 묻히고 *독 걸린 적에 불 카드 → 추가 데미지*(독뎀 시너지). **위자드(썬/콜)**: 오브로 썬더(다중 공격)·콜드(빙결=취약+피해), 오브 획득·다중 소모 운용. **클레릭**: 오브 없이 회복·버프 + 언데드엔 힐로 공격하는 보조 힐러.
## 게임 프레임워크 현황
**StS2풍 덱빌더 로그라이크가 end-to-end로 완성**됐고, 이제 **로비 마을을 기점으로 반복 런**이 돕니다 (게임 시작 시 MainMenu 없이 바로 로비로 진입):
**StS2풍 덱빌더 로그라이크가 end-to-end로 완성**됐고, 이제 **로비 마을을 기점으로 반복 런**이 돕니다:
```
로비 맵(NPC 4종) → 모험가 NPC → 캐릭터 선택(전사/도적/마법사) → 절차 생성 맵(5막)
@@ -112,16 +104,15 @@ slaymaple/
→ 런 클리어(승천 해금) → 로비 복귀(영혼 정산) → 다음 런 …
```
게임 전체는 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작합니다. **UI는 메이커 저작**(7개 UIGroup: Default/Select/Lobby/Run/Deck/Popup/Toast)이고, 컨트롤러가 엔티티 경로(`/ui/<UIGroup>/<Hud>/...`)로 내용을 런타임 주입합니다. 생성기 `tools/deck/gen-slaydeck.mjs`**`SlayDeckController.codeblock` + `common.gamelogic`만 생성**(`.ui` 미접근, 결정적 출력`RULES.md` 참조). 게임 데이터는 **`data/*.json`**, 맵 구조는 **런타임 절차 생성**(`GenerateMap` Lua ↔ `tools/map/rogue-map.mjs` JS 미러).
게임 전체는 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작하며, 모든 산출물(`ui/DefaultGroup.ui` · `SlayDeckController.codeblock` · `common.gamelogic`)은 **`tools/deck/gen-slaydeck.mjs` 단일 소스에서 생성**됩니다(결정적 출력, 직접 편집 금지`RULES.md` 참조). 게임 데이터는 **`data/*.json`** 가 단일 소스, 맵 구조는 **런타임 절차 생성**(`GenerateMap` Lua ↔ `tools/map/rogue-map.mjs` JS 미러).
### 구현된 기능 (배포 퀄리티 P1~P15+, PR #34~#79)
### 구현된 기능 (배포 퀄리티 P1~P15, PR #34~#57)
| 영역 | 내용 |
|---|---|
| **로비 마을** | 전용 물리 맵 `lobby.map`(마을 배경). **NPC 4종 월드 엔티티** — 모험가(런 시작)·사서(카드 도감)·상인(영혼 상점)·안내원(게시판). 근접 시 머리 위 마크 + `↑`**또는 직접 클릭**으로 상호작용. **이동·공격 모션은 로비 맵에서만** 풀림(전투맵은 잠금), 카메라는 로비에서 **플레이어 추종**(전투맵은 고정) |
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택(**초상화·직업 설명·선택 테두리 강조** 캐릭터 선택 UI), 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**2차 전직**] — 각 클래스 3종(전사→파이터/페이지/스피어맨, 법사→위자드불독/위자드썬콜/클레릭, 도적→Shiv/Poison/Trickster). 전용 카드는 해당 클래스 풀만 획득 |
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬=위로 스윕). 카드 **121** — kind **Attack/Skill/Power/Status**. 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
| **도적 카드 공용 효과** | 카드 효과를 **카드명 하드코딩 대신 `data/cards.json` 공용 필드**로 표현(재사용). **불가침**·**x-cost**(에너지 비례 피해/약화)·드로우 수 비례 데미지·**다음 스킬 반복**·**처치 보상/반복**·카드 설명 **키워드 하이라이트**·드로우 연동(`drawSkillBlock`·`drawPoison`)·독 버스트·랜덤 타깃 등. **Lua + JS 미러 양쪽 구현**. 필드 사전 [`docs/card-effect-fields.md`](docs/card-effect-fields.md) |
| **캐릭터·전직** | 시작 시 **전사(HP80)/도적(HP70)/마법사(HP70)** 3종 선택, 클래스별 시작 덱. 보스 클리어 시 [유물] vs [**2차 전직**] — 각 클래스 3종(전사→파이터/페이지/스피어맨, 법사→위자드불독/위자드썬콜/클레릭, 도적→Shiv/Poison/Trickster). 전용 카드는 해당 클래스 풀만 획득 |
| **카드 전투** | 에너지 3·드로우·**드래그 사용**(공격=적에 드롭, 스킬=위로 스윕). 카드 **122** — kind **Attack/Skill/Power/Status**. 메커니즘: 다단히트·방어 무시·자가 디버프·드로·회복·**전체 공격(AoE)**·**독(DoT)**·**retain**(턴 종료 손패 유지)·**sly discard**(버림 트리거) |
| **버프/디버프** | StS 표준 — **힘**(+N 영구)·**약화**(주는 피해 25%)·**취약**(받는 피해 +50%)·**독**(매 행동 틱). 양방향(적 디버프 인텐트 포함), 인텐트는 최종 예상치 표시 |
| **전투 연출** | 공격 이펙트·**몬스터 데미지 팝업(자릿수 스킨)**·드래그 타깃 마커·적 개별 차례·**공격/피격/독뎀 모션**(아바타 상태 전이·몬스터 hit 클립·런지/넉백) |
| **절차 생성 맵** | 막 시작마다 **경로 생성**(런마다 다름, **가로 진행**). 층 규칙: 1~2층 전투만 → 3층~ 상점/휴식 → 4층~ 엘리트/**유물 방** → 보스 수렴. 점선 경로·상태 4단·층 카운터. 노드 타입별 **몬스터 랜덤 구성**(일반 1~3 / 엘리트 / 보스) + intent 랜덤 행동 |
@@ -131,10 +122,10 @@ slaymaple/
| **승천(Ascension)** | A1~A10 누적 모디파이어(적 강화·시작 HP 감소·보상 감소). UserDataStorage 유저별 영구 저장, 런 클리어 시 다음 단계 해금 |
| **멀티 act** | **5막** 진행(보스 클리어→다음 막 텔레포트, 맵·인카운터 변경, 적 스케일 `1+(막-1)*0.45`), 5막 클리어 시 런 종료 |
| **경제** | 화폐 표기 **메소**(코인 아이콘), 카드/유물/물약 메소 가격. 내부 식별자는 Gold 유지 |
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트(현 84종) |
| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 전투 규칙 JS 미러(몬테카를로) + `tools/map/rogue-map.mjs`(맵 생성 미러) + node 단위테스트 |
> ⚠️ 수치(적 스탯·경제·승천 배율)는 1차 조정 상태입니다. 정밀 밸런싱은 `sim-balance.mjs`로 검증하며 진행합니다.
> 도적(Silent) 카드 86장은 STS Silent 완역 포트 + **공식 스킬 아이콘 적용 완료**. 남은 작업은 카드명 메이플 재서사(어쌔신/시프)·멀티플레이어 전제 카드 싱글 정리 — [`docs/deck-concept.md`](docs/deck-concept.md) 참조.
> 도적(Silent) 카드 88장은 효과·프레임은 적용됐으나 **카드 아이콘(image/fx) 미할당** 상태입니다(전사·마법사 카드는 실 스킬 아이콘 적용 완료).
### 유용한 스크립트 호출
`/common` 엔티티(또는 Play Test 컨텍스트)에서:
@@ -158,20 +149,9 @@ 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 # 컨트롤러+common (UI는 메이커 저작 — 미생성)
node tools/deck/gen-slaydeck.mjs # 게임 전체(UI·컨트롤러·common·맵 인카운터)
node tools/map/gen-maps.mjs # map01~05 배경/타일
node tools/map/gen-lobby-map.mjs # 로비 맵 + NPC 배치
node tools/player/gen-lobby-npc.mjs # 로비 codeblock(LobbyNpc·LobbyMobility)
@@ -185,7 +165,7 @@ node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
## 아키텍처 메모
현재 게임 전체 로직이 `SlayDeckController` 단일 codeblock에 모여 있습니다. 초기 설계의 3분할(`SlayCardCatalog`/`SlayRunState`/`SlayCombatManager`)은 **기능적으로 모두 구현**됐으나 아직 한 컴포넌트 안에 있습니다. 맵 NPC·카메라·입력 잠금 등 **맵 단위 동작은 별도 codeblock**(LobbyNpc/LobbyMobility/MapCamera/PlayerLock/CombatMonster)으로 분리해 각 맵 루트/엔티티에 부착합니다. 카드/적/맵/유물/프레임/카메라 데이터는 `data/*.json`로 외부화돼 있습니다. **2026-06-17**: UI를 단일 `DefaultGroup`에서 7개 UIGroup(Select/Lobby/Run/Deck 등)으로 분리해 **메이커 저작으로 전환** — 생성기는 더 이상 `.ui`를 만들지 않고, 컨트롤러가 새 UIGroup 경로로 재연결됨(옛 UI emit `hud/*`·`gen-cardhand``tools/deck/legacy/` 휴면). 재연결 무결성은 `tools/verify/cbgap.mjs`(GAP 0)로 검증.
현재 게임 전체 로직이 `SlayDeckController` 단일 codeblock에 모여 있습니다. 초기 설계의 3분할(`SlayCardCatalog`/`SlayRunState`/`SlayCombatManager`)은 **기능적으로 모두 구현**됐으나 아직 한 컴포넌트 안에 있습니다. 맵 NPC·카메라·입력 잠금 등 **맵 단위 동작은 별도 codeblock**(LobbyNpc/LobbyMobility/MapCamera/PlayerLock/CombatMonster)으로 분리해 각 맵 루트/엔티티에 부착합니다. 카드/적/맵/유물/프레임/카메라 데이터는 `data/*.json`로 외부화돼 있습니다.
> ⚠️ **전투 규칙과 맵 생성은 Lua(gen-slaydeck 내장)와 JS 미러(sim-balance/rogue-map)로 이중 구현**입니다. 한쪽을 고치면 반드시 다른 쪽도 동기화하고 테스트하세요(`RULES.md` §6).
@@ -193,9 +173,7 @@ node tools/monster/gen-combat-monster.mjs # 몬스터 EnemyId 마커
## 향후 개선 계획 (후속 후보)
- [x] 전투 루프 · 런 루프 · 절차 생성 맵 · 상점/휴식/유물 방 · 유물 19종 · 물약 · 버프/디버프 · Power · 전직(전사/법사/도적 2차) · 승천+개인 저장 · 전투 모션 · 커스텀 프레임 · **반복 런·로비 맵·NPC·영혼·메소·카메라 추종 (P1~P15 완료)**
- [x] **UI 메이커-저작 전환** — 단일 DefaultGroup → 7개 UIGroup 분리, 생성기 UI 저작 폐기(`tools/deck/legacy/`), 컨트롤러 경로 재연결(cbgap GAP 0) (2026-06-17)
- [x] **시작 로비 직행 · 캐릭터 선택 UI · 디버그 치트 · map01 로스터 (2026-06-18)** — 게임 시작 시 MainMenu 없이 곧장 로비 진입(MainMenu는 추후 싱글/멀티/종료 메뉴로 재지정); 캐릭터 선택 화면 초상화·직업 설명·선택 테두리·Art 클리핑(MaskComponent) 배선; 디버그 단축키 Ctrl+Shift+C(카드 picker)·Ctrl+Shift+E(체력+에너지 전체 회복); map01 몬스터 18종 로스터(랜덤 행동)
- [ ] **도적 카드명 재서사·설명 한글화** — Silent 직역 카드명을 어쌔신/시프 메이플 스킬명으로 재서사(아이콘은 적용 완료), 2차 전직 설명 한글화
- [ ] **도적 카드 아이콘** — Silent 88장에 실 스킬 아이콘(image/fx) 할당, 2차 전직 설명 한글화
- [ ] **런 이어하기** — 진행 중 런 직렬화 저장(UserDataStorage 확장, 메뉴 "이어하기" 활성화)
- [ ] **카드 제거/업그레이드** — 상점 카드 제거 슬롯, 휴식 노드에서 카드 강화
- [ ] **이벤트 노드(?)** — 랜덤 텍스트 이벤트(선택지·리스크/리워드)

View File

@@ -11,8 +11,8 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
| 산출물 (절대 Read/Edit 금지) | 크기 | 단일 소스 (여기만 편집) | 재생성 명령 |
|---|---|---|---|
| `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` |
| `ui/DefaultGroup.ui` | **~7.1MB** | `data/*.json` + `tools/deck/`(`gen-slaydeck.mjs`+`lib/`+`hud/`) | `node tools/deck/gen-slaydeck.mjs` |
| `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | 〃 | 〃 |
| `Global/common.gamelogic` | ~1KB | 〃 | 〃 |
| `map/map01.map`~`map05.map`, `map/lobby.map` | 각 ~210KB | `tools/map/`·`tools/monster/`·`tools/camera/`·`tools/player/` (↓ 보조 생성기) | 해당 생성기 |
| `RootDesk/MyDesk/CombatMonster.codeblock` | ~2KB | `tools/monster/gen-combat-monster.mjs` | `node tools/monster/gen-combat-monster.mjs` |
@@ -21,11 +21,10 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
| `RootDesk/MyDesk/LobbyNpc.codeblock`·`LobbyMobility.codeblock` | 각 ~2-3KB | `tools/player/gen-lobby-npc.mjs` | `node tools/player/gen-lobby-npc.mjs` |
| `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/*.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`를 재생성.)
- `.claude/settings.json`의 permissions.deny가 위 파일의 Read/Edit/Write 도구 사용을 차단한다 (이 저장소를 열면 자동 적용). deny는 **glob**`ui/*.ui`·`map/*.map`·`RootDesk/MyDesk/*.codeblock`·`Global/common.gamelogic`·`Global/SectorConfig.config`. 따라서 **메이커 저작 codeblock/UI**(`Monster`·`MonsterAttack`·`PlayerAttack`·`PlayerHit`·`UIPopup`·`UIToast`.codeblock, `ui/PopupGroup.ui`·`ui/ToastGroup.ui`)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발.
- 게임 로직·UI 수정 = **`tools/deck/gen-slaydeck.mjs`(오케스트레이터 + codeblock Lua) 또는 `data/*.json`(데이터) 수정** → 재생성 → 산출물은 통째로 커밋.
- **UI emit은 HUD별 모듈** `tools/deck/hud/*.mjs`(charselect·shop·combat·map·deckall·soulshop 등 16종), **codeblock 메서드(Lua)는 기능별 모듈** `tools/deck/cb/*.mjs`(boot·state·combat·hand·deckview·items·map·shop 등 17종, 메서드 161개를 연속런으로). **공유분**: UI 헬퍼·상수·데이터·lua 테이블 = `tools/deck/lib/ui-helpers.mjs`·`tools/deck/lib/data.mjs`, method/prop/codeblock 헬퍼·writeCodeblocks 상수 = `tools/deck/lib/codeblock.mjs`. 특정 화면 UI 수정은 `hud/<name>.mjs`, 특정 메서드 수정은 `cb/<name>.mjs`만(의존: orchestrator→{hud,cb}→lib 단방향). prop 103개는 오케스트레이터 writeCodeblocks에 유지. **cb 모듈은 원본 메서드 순서 보존이 바이트동일 조건** — 새 메서드는 해당 기능 모듈의 알맞은 위치에 추가하고 writeCodeblocks의 spread 순서 유지.
- 리팩터 시 **출력 바이트-동일 검증**: `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`)
@@ -37,7 +36,7 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
- `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`·`hud/*.mjs``tools/deck/legacy/`로 이관 — 휴면, UI 메이커 저작 전환)
- `tools/deck/gen-cardhand.mjs``DefaultGroup.ui` 카드핸드 보조 패처
## 2. 산출물 검증은 카운트로, 내용 출력 금지
@@ -65,7 +64,6 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
- 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create <spec.json>` / `merge <번호>`.
- PR 제목과 본문은 한국어로 작성한다.
- 산출물 재생성 커밋은 소스 변경 커밋과 분리하거나, 메시지에 "산출물 재생성"을 명시.
- **PR 머지 후 브랜치 삭제**: 머지된 `feature/*`·`docs/*` 브랜치는 로컬·원격 모두 삭제한다. 삭제 전 `git merge-base --is-ancestor origin/<브랜치> origin/main`로 완전 머지 확인(종료코드 0=완전 머지 → 삭제 가능). main에 없는 커밋이 남은 브랜치와 `codex/*` 등 작업 중 브랜치는 보존한다.
## 5. 메이커(MSW) 연동 주의
@@ -87,9 +85,3 @@ 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 메서드를 작성하거나 기존 메서드를 손댈 때 이 규칙을 적용한다(대규모 일괄 개명은 별도 작업으로).

View File

@@ -1,23 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,23 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,36 +0,0 @@
{
"Id": "",
"GameId": "",
"EntryKey": "codeblock://4a220aa8-e014-4c7b-8234-fff8c5c66686",
"ContentType": "x-mod/codeblock",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"CoreVersion": {
"Major": 0,
"Minor": 2
},
"ScriptVersion": {
"Major": 1,
"Minor": 1
},
"Description": "",
"Id": "4a220aa8-e014-4c7b-8234-fff8c5c66686",
"Language": 1,
"Name": "MapleTree",
"Type": 1,
"Source": 0,
"Target": null,
"Properties": [],
"Methods": [],
"EntityEventHandlers": []
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,23 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,23 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,23 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,7 +0,0 @@
@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

View File

@@ -14,7 +14,7 @@
"Defend": {
"name": "아이언 바디",
"cost": 1,
"kind": "Attack",
"kind": "Skill",
"block": 5,
"desc": "방어도 5",
"image": "7648c3b8e1ca44fc8ec353561207a670",
@@ -59,7 +59,6 @@
"cost": 2,
"kind": "Attack",
"damage": 8,
"firstCardDamageBonus": 2,
"vuln": 2,
"desc": "피해 8, 취약 2",
"image": "fe83c7635b0e49ed83d75a2833adb53e",
@@ -90,8 +89,8 @@
"name": "분노",
"cost": 1,
"kind": "Power",
"aoe": true,
"damage": 4,
"powerEffect": "strengthPerTurn",
"value": 1,
"desc": "매 턴 시작 시 힘 +1",
"image": "379d86e3de064959aa4612f71e84ccfb",
"class": "warrior",
@@ -238,8 +237,7 @@
"kind": "Skill",
"class": "magician",
"block": 3,
"discardAll": true,
"drawPerDiscarded": 1,
"draw": 1,
"desc": "방어도 3, 드로 1",
"image": "7f70a9dc7e304433bb8121dd9c4df98b",
"rarity": "normal"
@@ -380,8 +378,7 @@
"rarity": "normal",
"desc": "피해를 3 줍니다. 약화를 1 부여합니다.",
"weak": 1,
"damage": 3,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"damage": 3
},
"SilentStrike": {
"name": "타격",
@@ -390,8 +387,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "피해를 6 줍니다.",
"damage": 6,
"image": "92a5020c978c46bdabab910598118b86"
"damage": 6
},
"Survivor": {
"name": "생존자",
@@ -401,8 +397,7 @@
"rarity": "normal",
"desc": "방어도를 8 얻습니다. 카드를 1장 버립니다.",
"block": 8,
"discard": 1,
"image": "49c8f279bfa64bf3954037f17da0052d"
"discard": 1
},
"SilentDefend": {
"name": "수비",
@@ -411,8 +406,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "방어도를 5 얻습니다.",
"block": 5,
"image": "0946f69d84464df29b24b94c744c868d"
"block": 5
},
"Slice": {
"name": "칼질",
@@ -421,8 +415,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "피해를 6 줍니다.",
"damage": 6,
"image": "92a5020c978c46bdabab910598118b86"
"damage": 6
},
"Shiv": {
"name": "표창",
@@ -433,8 +426,7 @@
"desc": "피해를 4 줍니다. 소멸.",
"damage": 4,
"exhaust": true,
"token": true,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"token": true
},
"DaggerSpray": {
"name": "단검 분사",
@@ -445,8 +437,7 @@
"desc": "모든 적에게 피해를 4만큼 2번 줍니다.",
"aoe": true,
"damage": 4,
"hits": 2,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"hits": 2
},
"DaggerThrow": {
"name": "단검 투척",
@@ -455,10 +446,9 @@
"class": "bandit",
"rarity": "normal",
"desc": "피해를 9 줍니다. 카드를 1장 뽑습니다. 카드를 1장 버립니다.",
"drawUntilHandSize": 6,
"draw": 1,
"damage": 9,
"discard": 1,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"discard": 1
},
"PoisonedStab": {
"name": "독 찌르기",
@@ -468,8 +458,7 @@
"rarity": "normal",
"desc": "피해를 6 줍니다. 중독을 3 부여합니다.",
"poison": 3,
"damage": 6,
"image": "19361e72087946b1888684185b40d935"
"damage": 6
},
"SuckerPunch": {
"name": "불의의 일격",
@@ -479,9 +468,7 @@
"rarity": "normal",
"desc": "피해를 8 줍니다. 약화를 1 부여합니다.",
"weak": 1,
"damage": 8,
"cardPlayedDamage": 2,
"image": "92a5020c978c46bdabab910598118b86"
"damage": 8
},
"LeadingStrike": {
"name": "선제 타격",
@@ -491,8 +478,7 @@
"rarity": "normal",
"desc": "피해를 3 줍니다. 표창을 2장 손으로 가져옵니다.",
"damage": 3,
"addShiv": 2,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"addShiv": 2
},
"FollowThrough": {
"name": "완수",
@@ -501,10 +487,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "피해를 7 줍니다. 손에 다른 카드가 5장 이상 있다면, 1번 추가로 적중합니다.",
"damage": 7,
"otherHandAtLeast": 5,
"bonusHitsWhenOtherHandAtLeast": 1,
"image": "92a5020c978c46bdabab910598118b86"
"damage": 7
},
"FlickFlack": {
"name": "재주넘기",
@@ -515,8 +498,7 @@
"desc": "교활. 모든 적에게 피해를 6 줍니다.",
"aoe": true,
"damage": 6,
"sly": true,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"sly": true
},
"Ricochet": {
"name": "도탄",
@@ -527,9 +509,7 @@
"desc": "교활. 무작위 적에게 피해를 3만큼 4번 줍니다.",
"damage": 3,
"hits": 4,
"sly": true,
"image": "1b0f2dc8abd0434990eee1befefcbe0d",
"randomTargetEachHit": true
"sly": true
},
"Prepared": {
"name": "예비",
@@ -537,10 +517,9 @@
"kind": "Skill",
"class": "bandit",
"rarity": "normal",
"desc": "카드를 1장 버리고, 이번 턴에 준 피해만큼 방어를 얻습니다.",
"blockPerDamageDealtThisTurn": 1,
"discard": 1,
"image": "c1e19219745e44c39ae6ac2f77e347d9"
"desc": "카드를 1장 뽑습니다. 카드를 1장 버립니다.",
"draw": 1,
"discard": 1
},
"Anticipate": {
"name": "예측",
@@ -549,8 +528,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "이번 턴 동안 민첩을 2 얻습니다.",
"dex": 2,
"image": "49c8f279bfa64bf3954037f17da0052d"
"dex": 2
},
"Deflect": {
"name": "튕겨내기",
@@ -559,8 +537,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "방어도를 4 얻습니다.",
"block": 4,
"image": "0946f69d84464df29b24b94c744c868d"
"block": 4
},
"BladeDance": {
"name": "검무",
@@ -570,8 +547,7 @@
"rarity": "normal",
"desc": "표창을 3장 손으로 가져옵니다. 소멸.",
"addShiv": 3,
"exhaust": true,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"exhaust": true
},
"Backflip": {
"name": "공중제비",
@@ -581,8 +557,7 @@
"rarity": "normal",
"desc": "방어도를 5 얻습니다. 카드를 2장 뽑습니다.",
"block": 5,
"draw": 2,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"draw": 2
},
"DodgeAndRoll": {
"name": "구르기",
@@ -591,9 +566,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "방어도를 4 얻습니다. 다음 턴에, 방어도를 4 얻습니다",
"block": 4,
"nextTurnBlock": 4,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"block": 4
},
"PiercingWail": {
"name": "귀를 찢는 비명",
@@ -602,10 +575,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "이번 턴 동안 모든 적이 힘을 6 잃습니다. 소멸.",
"draw": 1,
"image": "0946f69d84464df29b24b94c744c868d",
"affectsAllEnemies": true,
"enemyStrengthLossThisTurn": 6
"draw": 1
},
"CloakAndDagger": {
"name": "망토와 단검",
@@ -615,8 +585,7 @@
"rarity": "normal",
"desc": "방어도를 6 얻습니다. 표창을 1장 손으로 가져옵니다.",
"block": 6,
"addShiv": 1,
"image": "0946f69d84464df29b24b94c744c868d"
"addShiv": 1
},
"DeadlyPoison": {
"name": "맹독",
@@ -625,8 +594,7 @@
"class": "bandit",
"rarity": "normal",
"desc": "중독을 5 부여합니다.",
"poison": 5,
"image": "19361e72087946b1888684185b40d935"
"poison": 5
},
"Snakebite": {
"name": "뱀 물기",
@@ -636,8 +604,7 @@
"rarity": "normal",
"desc": "보존. 중독을 7 부여합니다.",
"poison": 7,
"retain": true,
"image": "19361e72087946b1888684185b40d935"
"retain": true
},
"Untouchable": {
"name": "범접 불가",
@@ -647,8 +614,7 @@
"rarity": "normal",
"desc": "교활. 방어도를 6 얻습니다.",
"block": 6,
"sly": true,
"image": "0946f69d84464df29b24b94c744c868d"
"sly": true
},
"Skewer": {
"name": "꼬챙이",
@@ -657,10 +623,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "피해를 8만큼 X번 줍니다.",
"useAllEnergy": true,
"xDamagePerEnergy": 8,
"draw": 1,
"image": "92a5020c978c46bdabab910598118b86"
"draw": 1
},
"Backstab": {
"name": "배신",
@@ -669,9 +632,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "선천성. 피해를 11 줍니다. 소멸.",
"innate": true,
"damage": 11,
"image": "b1360ed0c4b942309d240634b8f36872"
"damage": 11
},
"PreciseCut": {
"name": "정밀한 베기",
@@ -680,9 +641,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "피해를 13 줍니다. 손에 있는 다른 카드 1장당 피해량이 2 감소합니다.",
"damage": 13,
"damagePerOtherHandCard": -2,
"image": "92a5020c978c46bdabab910598118b86"
"damage": 13
},
"Finisher": {
"name": "마무리",
@@ -691,9 +650,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "이번 턴에 사용한 공격 카드 1장당 피해를 6 줍니다.",
"damage": 0,
"damagePerAttackPlayedThisTurn": 6,
"image": "b1360ed0c4b942309d240634b8f36872"
"damage": 6
},
"MementoMori": {
"name": "메멘토 모리",
@@ -702,9 +659,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "피해를 9 줍니다. 이번 턴에 버린 카드 1장당 피해량이 4 증가합니다.",
"damage": 9,
"damagePerDiscardedThisTurn": 4,
"image": "0946f69d84464df29b24b94c744c868d"
"damage": 9
},
"Strangle": {
"name": "목 조르기",
@@ -713,8 +668,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "피해를 8 줍니다. 이번 턴에 카드를 사용할 때마다, 대상 적이 체력을 2 잃습니다.",
"damage": 8,
"image": "92a5020c978c46bdabab910598118b86"
"damage": 8
},
"Flechettes": {
"name": "프레췌",
@@ -723,9 +677,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "손에 있는 스킬 카드 1장당 피해를 5 줍니다.",
"damage": 0,
"damagePerSkillInHand": 5,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"damage": 5
},
"Pounce": {
"name": "덮치기",
@@ -734,9 +686,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "피해를 12 줍니다. 다음에 사용하는 스킬 카드의 비용이 0 이 됩니다.",
"damage": 12,
"nextSkillCostZero": true,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"damage": 12
},
"Dash": {
"name": "돌진",
@@ -746,8 +696,7 @@
"rarity": "unique",
"desc": "방어도를 10 얻습니다. 피해를 10 줍니다.",
"block": 10,
"damage": 10,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"damage": 10
},
"Predator": {
"name": "천적",
@@ -756,9 +705,8 @@
"class": "bandit",
"rarity": "unique",
"desc": "피해를 15 줍니다. 다음 턴에, 카드를 2장 뽑습니다.",
"nextTurnDraw": 2,
"damage": 15,
"image": "b1360ed0c4b942309d240634b8f36872"
"draw": 2,
"damage": 15
},
"Pinpoint": {
"name": "정밀 사격",
@@ -767,9 +715,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "피해를 15 줍니다. 이번 턴에 스킬을 사용할 때마다 비용이 1 감소합니다.",
"damage": 15,
"skillCostReductionThisTurn": 1,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"damage": 15
},
"CalculatedGamble": {
"name": "계산된 도박",
@@ -778,9 +724,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "손에 있는 모든 카드를 버린 뒤, 버린 카드의 수만큼 카드를 뽑습니다. 소멸.",
"image": "c1e19219745e44c39ae6ac2f77e347d9",
"discardAll": true,
"drawPerDiscarded": 1
"draw": 1
},
"Expose": {
"name": "들춰내기",
@@ -789,11 +733,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "대상 적의 모든 인공물과 방어도를 제거합니다. 취약을 2 부여합니다. 소멸.",
"vuln": 2,
"image": "0946f69d84464df29b24b94c744c868d",
"affectsAllEnemies": true,
"removeEnemyBlock": true,
"removeEnemyArtifact": true
"vuln": 2
},
"HiddenDaggers": {
"name": "숨겨진 단검",
@@ -803,8 +743,7 @@
"rarity": "unique",
"desc": "카드를 2장 버립니다. 표창을 2장 손으로 가져옵니다.",
"discard": 2,
"addShiv": 2,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"addShiv": 2
},
"EscapePlan": {
"name": "탈출구",
@@ -813,9 +752,8 @@
"class": "bandit",
"rarity": "unique",
"desc": "카드를 1장 뽑습니다. 뽑은 카드가 스킬 카드라면, 방어도를 3 얻습니다.",
"draw": 1,
"drawSkillBlock": 3,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"block": 3,
"draw": 1
},
"Acrobatics": {
"name": "곡예",
@@ -825,8 +763,7 @@
"rarity": "unique",
"desc": "카드를 3장 뽑습니다. 카드를 1장 버립니다.",
"draw": 3,
"discard": 1,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"discard": 1
},
"HandTrick": {
"name": "손기술",
@@ -835,9 +772,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "방어도를 7 얻습니다. 이번 턴 동안 손에 있는 스킬 카드 1장에 교활을 추가합니다.",
"block": 7,
"image": "c1e19219745e44c39ae6ac2f77e347d9",
"turnHandSlyCount": 1
"block": 7
},
"Mirage": {
"name": "신기루",
@@ -846,8 +781,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "모든 적에게 부여된 중독과 동일한 만큼의 방어도를 얻습니다. 소멸.",
"draw": 1,
"image": "0946f69d84464df29b24b94c744c868d"
"draw": 1
},
"Expertise": {
"name": "전문성",
@@ -856,8 +790,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "손에 있는 카드가 6장이 될 때까지 카드를 뽑습니다.",
"image": "c1e19219745e44c39ae6ac2f77e347d9",
"drawUntilHandSize": 6
"draw": 1
},
"BubbleBubble": {
"name": "차오르는 독",
@@ -866,9 +799,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "적이 중독을 보유하고 있다면, 중독을 9 부여합니다.",
"poison": 9,
"image": "19361e72087946b1888684185b40d935",
"poisonIfTargetPoisoned": true
"poison": 9
},
"Blur": {
"name": "흐릿함",
@@ -877,9 +808,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "방어도를 5 얻습니다. 다음 턴 시작 시 방어도가 사라지지 않습니다.",
"block": 5,
"nextTurnKeepBlock": true,
"image": "0946f69d84464df29b24b94c744c868d"
"block": 5
},
"LegSweep": {
"name": "다리 걸기",
@@ -889,8 +818,7 @@
"rarity": "unique",
"desc": "약화를 2 부여합니다. 방어도를 11 얻습니다.",
"block": 11,
"weak": 2,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"weak": 2
},
"UpMySleeve": {
"name": "비책",
@@ -899,9 +827,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "표창을 3장 손으로 가져옵니다. 이 카드의 비용이 1 감소합니다.",
"addShiv": 3,
"image": "1b0f2dc8abd0434990eee1befefcbe0d",
"combatCostReductionOnPlay": 1
"addShiv": 3
},
"BouncingFlask": {
"name": "탄성 플라스크",
@@ -910,10 +836,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "무작위 적에게 중독을 3만큼 3번 부여합니다.",
"poison": 3,
"poisonHits": 3,
"poisonRandomTargets": true,
"image": "19361e72087946b1888684185b40d935"
"poison": 9
},
"Reflex": {
"name": "반사신경",
@@ -923,8 +846,7 @@
"rarity": "unique",
"desc": "교활. 카드를 2장 뽑습니다.",
"draw": 2,
"sly": true,
"image": "49c8f279bfa64bf3954037f17da0052d"
"sly": true
},
"Haze": {
"name": "아지랑이",
@@ -934,8 +856,7 @@
"rarity": "unique",
"desc": "교활. 모든 적에게 중독을 4 부여합니다.",
"poison": 4,
"sly": true,
"image": "19361e72087946b1888684185b40d935"
"sly": true
},
"Tactician": {
"name": "전략가",
@@ -943,10 +864,10 @@
"kind": "Skill",
"class": "bandit",
"rarity": "unique",
"desc": "교활. 에너지를 1 얻습니다.",
"gainEnergy": 1,
"sly": true,
"image": "c1e19219745e44c39ae6ac2f77e347d9"
"desc": "교활. 얻습니다.",
"powerEffect": "energyPerTurn",
"value": 1,
"sly": true
},
"WellLaidPlans": {
"name": "괜찮은 전략",
@@ -955,9 +876,8 @@
"class": "bandit",
"rarity": "unique",
"desc": "내 턴 종료 시, 카드를 최대 1장까지 보존합니다.",
"powerEffect": "retainOne",
"value": 1,
"image": "c1e19219745e44c39ae6ac2f77e347d9"
"powerEffect": "blockPerTurn",
"value": 2
},
"InfiniteBlades": {
"name": "무한의 검날",
@@ -966,8 +886,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "내 턴 시작 시, 표창을 1장 손으로 가져옵니다.",
"turnStartShiv": 1,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"turnStartShiv": 1
},
"Footwork": {
"name": "발놀림",
@@ -976,8 +895,7 @@
"class": "bandit",
"rarity": "unique",
"desc": "민첩을 2 얻습니다.",
"dex": 2,
"image": "49c8f279bfa64bf3954037f17da0052d"
"dex": 2
},
"Outbreak": {
"name": "발병",
@@ -985,10 +903,11 @@
"kind": "Power",
"class": "bandit",
"rarity": "unique",
"desc": "독이 3번 부여 때마다 모든 적에게 11 피해를 줍니다.",
"image": "19361e72087946b1888684185b40d935",
"poisonApplicationBurstEvery": 3,
"poisonApplicationBurstDamage": 11
"desc": "중독을 3번 부여 때마다, 모든 적에게 피해를 11 줍니다.",
"aoe": true,
"powerEffect": "strengthPerTurn",
"value": 1,
"damage": 11
},
"NoxiousFumes": {
"name": "유독 가스",
@@ -998,9 +917,8 @@
"rarity": "unique",
"desc": "내 턴 시작 시, 모든 적에게 중독을 2 부여합니다.",
"poison": 2,
"powerEffect": "poisonPerTurn",
"value": 2,
"image": "19361e72087946b1888684185b40d935"
"powerEffect": "strengthPerTurn",
"value": 1
},
"Accuracy": {
"name": "정밀",
@@ -1009,8 +927,8 @@
"class": "bandit",
"rarity": "unique",
"desc": "표창의 피해량이 4 증가합니다.",
"shivDamageBonus": 4,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"powerEffect": "strengthPerTurn",
"value": 1
},
"PhantomBlades": {
"name": "환영검",
@@ -1019,9 +937,8 @@
"class": "bandit",
"rarity": "unique",
"desc": "표창이 보존을 얻습니다. 매 턴마다 처음으로 사용하는 표창의 피해량이 9 증가합니다.",
"shivRetain": true,
"firstShivDamageBonus": 9,
"image": "0946f69d84464df29b24b94c744c868d"
"powerEffect": "strengthPerTurn",
"value": 1
},
"Speedster": {
"name": "스피드스터",
@@ -1031,8 +948,9 @@
"rarity": "unique",
"desc": "내 턴 동안 카드를 뽑을 때마다, 모든 적에게 피해를 2 줍니다.",
"aoe": true,
"drawDamage": 2,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"powerEffect": "strengthPerTurn",
"value": 1,
"damage": 2
},
"GrandFinale": {
"name": "대단원의 막",
@@ -1041,10 +959,8 @@
"class": "bandit",
"rarity": "legend",
"desc": "뽑을 카드 더미에 카드가 없을 때만 사용할 수 있습니다. 모든 적에게 피해를 60 줍니다.",
"playableWhenDrawPileEmpty": true,
"aoe": true,
"damage": 60,
"image": "dbdbb1b56ae54672ae68ac6882fff6a2"
"damage": 60
},
"Assassinate": {
"name": "암살",
@@ -1053,10 +969,8 @@
"class": "bandit",
"rarity": "legend",
"desc": "선천성. 피해를 10 줍니다. 취약을 1 부여합니다. 소멸.",
"innate": true,
"vuln": 1,
"damage": 10,
"image": "b1360ed0c4b942309d240634b8f36872"
"damage": 10
},
"EchoingSlash": {
"name": "메아리 참격",
@@ -1066,9 +980,7 @@
"rarity": "legend",
"desc": "모든 적에게 피해를 10 줍니다. 적을 처치할 때마다 이 효과를 반복합니다.",
"aoe": true,
"damage": 10,
"image": "dbdbb1b56ae54672ae68ac6882fff6a2",
"repeatOnKill": true
"damage": 10
},
"TheHunt": {
"name": "사냥",
@@ -1077,9 +989,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "피해를 10 줍니다. 치명타라면, 카드 보상을 추가로 얻습니다. 소멸.",
"damage": 10,
"rewardOnKill": 1,
"image": "b1360ed0c4b942309d240634b8f36872"
"damage": 10
},
"Murder": {
"name": "살해",
@@ -1088,9 +998,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "피해를 1 줍니다. 이번 전투 동안 뽑은 카드 1장당 피해량이 1 증가합니다.",
"damage": 1,
"damagePerCardDrawnThisCombat": 1,
"image": "b1360ed0c4b942309d240634b8f36872"
"damage": 1
},
"Malaise": {
"name": "불쾌",
@@ -1099,9 +1007,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "적이 힘을 X 잃습니다. 약화를 X 부여합니다. 소멸.",
"useAllEnergy": true,
"xWeakPerEnergy": 1,
"image": "0946f69d84464df29b24b94c744c868d"
"weak": 3
},
"Adrenaline": {
"name": "아드레날린",
@@ -1109,10 +1015,10 @@
"kind": "Skill",
"class": "bandit",
"rarity": "legend",
"desc": "에너지를 1 얻습니다. 카드를 2장 뽑습니다. 소멸.",
"desc": " 얻습니다. 카드를 2장 뽑습니다. 소멸.",
"draw": 2,
"gainEnergy": 1,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"powerEffect": "energyPerTurn",
"value": 1
},
"StormOfSteel": {
"name": "강철의 폭풍",
@@ -1122,8 +1028,7 @@
"rarity": "legend",
"desc": "손에 있는 모든 카드를 버립니다. 버린 카드의 수만큼 표창을 손으로 가져옵니다.",
"discardAll": true,
"addShivPerDiscard": true,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"addShivPerDiscard": true
},
"ShadowStep": {
"name": "그림자 걸음",
@@ -1132,9 +1037,8 @@
"class": "bandit",
"rarity": "legend",
"desc": "손에 있는 모든 카드를 버립니다. 다음 턴에, 공격 카드의 피해량이 2배가 됩니다.",
"nextTurnAttackMultiplier": 2,
"discardAll": true,
"image": "0946f69d84464df29b24b94c744c868d"
"draw": 1,
"discardAll": true
},
"Shadowmeld": {
"name": "그림자 은신",
@@ -1143,8 +1047,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "이번 턴 동안 얻는 방어도가 2배가 됩니다.",
"blockGainMultiplier": 2,
"image": "0946f69d84464df29b24b94c744c868d"
"draw": 1
},
"CorrosiveWave": {
"name": "부식성 파도",
@@ -1153,8 +1056,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "이번 턴에 카드를 뽑을 때마다, 모든 적에게 중독을 2 부여합니다.",
"drawPoison": 2,
"image": "19361e72087946b1888684185b40d935"
"poison": 2
},
"BladeOfInk": {
"name": "잉크 칼날",
@@ -1163,8 +1065,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "잉크투성이 표창을 2장 손으로 가져옵니다.",
"addShiv": 2,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"addShiv": 2
},
"Burst": {
"name": "폭주",
@@ -1173,9 +1074,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "이번 턴에 다음에 사용하는 스킬 카드가 1번 추가로 사용됩니다.",
"draw": 1,
"nextSkillRepeatCount": 1,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"draw": 1
},
"KnifeTrap": {
"name": "칼날 함정",
@@ -1184,8 +1083,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "대상 적에게 소멸된 카드 더미에 있는 모든 표창을 사용합니다.",
"draw": 1,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"draw": 1
},
"BulletTime": {
"name": "불릿 타임",
@@ -1194,9 +1092,8 @@
"class": "bandit",
"rarity": "legend",
"desc": "이번 턴 동안 더 이상 카드를 뽑을 수 없습니다. 이번 턴 동안 손에 있는 모든 카드를 비용 없이 사용할 수 있습니다.",
"handCostZeroThisTurn": true,
"drawDisabledThisTurn": true,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
"powerEffect": "energyPerTurn",
"value": 1
},
"Nightmare": {
"name": "악몽",
@@ -1205,10 +1102,7 @@
"class": "bandit",
"rarity": "legend",
"desc": "카드를 1장 선택합니다. 다음 턴에, 그 카드의 복사본을 3장 손으로 가져옵니다. 소멸.",
"nextTurnCopies": 3,
"nextTurnSelectHandCard": true,
"nextTurnSelectPrompt": "복사할 카드를 선택하세요",
"image": "0946f69d84464df29b24b94c744c868d"
"draw": 1
},
"ToolsOfTheTrade": {
"name": "작업 도구",
@@ -1217,9 +1111,10 @@
"class": "bandit",
"rarity": "legend",
"desc": "내 턴 시작 시, 카드를 1장 뽑고 카드를 1장 버립니다.",
"turnStartDraw": 1,
"turnStartDiscard": 1,
"image": "c1e19219745e44c39ae6ac2f77e347d9"
"draw": 1,
"powerEffect": "energyPerTurn",
"value": 1,
"discard": 1
},
"Afterimage": {
"name": "잔상",
@@ -1228,8 +1123,9 @@
"class": "bandit",
"rarity": "legend",
"desc": "카드를 사용할 때마다, 방어도를 1 얻습니다.",
"image": "0946f69d84464df29b24b94c744c868d",
"cardPlayedBlock": 1
"block": 1,
"powerEffect": "blockPerTurn",
"value": 2
},
"Accelerant": {
"name": "촉진제",
@@ -1237,9 +1133,9 @@
"kind": "Power",
"class": "bandit",
"rarity": "legend",
"desc": "적 턴 시작 시 독이 한 번 더 틱합니다.",
"image": "19361e72087946b1888684185b40d935",
"extraPoisonTicks": 1
"desc": "중독이 1번 추가로 발동합니다.",
"powerEffect": "strengthPerTurn",
"value": 1
},
"Envenom": {
"name": "독 바르기",
@@ -1248,8 +1144,9 @@
"class": "bandit",
"rarity": "legend",
"desc": "공격 카드가 막히지 않은 피해를 줄 때마다, 중독을 1 부여합니다.",
"attackPoison": 1,
"image": "19361e72087946b1888684185b40d935"
"poison": 1,
"powerEffect": "strengthPerTurn",
"value": 1
},
"MasterPlanner": {
"name": "설계의 대가",
@@ -1257,9 +1154,9 @@
"kind": "Power",
"class": "bandit",
"rarity": "legend",
"desc": "사용한 스킬 카드 교활해집니다.",
"image": "c1e19219745e44c39ae6ac2f77e347d9",
"skillSlyOnPlay": true
"desc": "스킬 카드를 사용 시, 그 카드 교활을 얻습니다.",
"powerEffect": "strengthPerTurn",
"value": 1
},
"Tracking": {
"name": "추적",
@@ -1268,8 +1165,8 @@
"class": "bandit",
"rarity": "legend",
"desc": "약화 상태의 적이 공격 카드로 받는 피해가 2배가 됩니다.",
"attackDamageVsWeakMultiplier": 2,
"image": "b1360ed0c4b942309d240634b8f36872"
"powerEffect": "strengthPerTurn",
"value": 1
},
"FanOfKnives": {
"name": "칼날 부채",
@@ -1278,9 +1175,9 @@
"class": "bandit",
"rarity": "legend",
"desc": "표창이 이제 모든 적을 대상으로 합니다. 표창을 4장 손으로 가져옵니다.",
"addShiv": 4,
"shivAoe": true,
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
"powerEffect": "strengthPerTurn",
"value": 1,
"addShiv": 4
},
"SerpentForm": {
"name": "구렁이의 형상",
@@ -1289,8 +1186,9 @@
"class": "bandit",
"rarity": "legend",
"desc": "카드를 사용할 때마다, 무작위 적에게 피해를 4 줍니다.",
"cardPlayedRandomDamage": 4,
"image": "19361e72087946b1888684185b40d935"
"powerEffect": "strengthPerTurn",
"value": 1,
"damage": 4
},
"Abrasive": {
"name": "연마",
@@ -1301,8 +1199,7 @@
"desc": "교활. 민첩을 1 얻습니다. 가시를 4 얻습니다.",
"dex": 1,
"thorns": 4,
"sly": true,
"image": "49c8f279bfa64bf3954037f17da0052d"
"sly": true
},
"Suppress": {
"name": "진압",
@@ -1311,10 +1208,8 @@
"class": "bandit",
"rarity": "legend",
"desc": "선천성. 피해를 11 줍니다. 약화를 3 부여합니다.",
"innate": true,
"weak": 3,
"damage": 11,
"image": "b1360ed0c4b942309d240634b8f36872"
"damage": 11
},
"WraithForm": {
"name": "유령의 형상",
@@ -1323,9 +1218,27 @@
"class": "bandit",
"rarity": "legend",
"desc": "불가침을 2 얻습니다. 내 턴 종료 시 민첩을 1 잃습니다.",
"intangible": 2,
"endTurnDexLoss": 1,
"image": "0946f69d84464df29b24b94c744c868d"
"powerEffect": "blockPerTurn",
"value": 8
},
"Flanking": {
"name": "측면 공격",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"rarity": "legend",
"desc": "이번 턴에 대상 적이 다른 플레이어에게 받는 피해량이 2배가 됩니다.",
"draw": 1
},
"Sneaky": {
"name": "비열함",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"rarity": "legend",
"desc": "교활. 다른 플레이어가 적을 공격할 때마다, 방어도를 1 얻습니다.",
"block": 1,
"sly": true
}
},
"starterDecks": {

Binary file not shown.

View File

@@ -119,65 +119,6 @@
{ "kind": "Attack", "value": 12 },
{ "kind": "Attack", "value": 24 }
]
},
"octopus": {
"name": "문어",
"maxHp": 15,
"intents": [
{ "kind": "Attack", "value": 5 },
{ "kind": "Attack", "value": 6 },
{ "kind": "Defend", "value": 4 }
]
},
"kapa_drake": {
"name": "카파 드레이크",
"maxHp": 24,
"intents": [
{ "kind": "Attack", "value": 9 },
{ "kind": "Attack", "value": 6 },
{ "kind": "Defend", "value": 6 },
{ "kind": "Attack", "value": 11 }
]
},
"junior_neki": {
"name": "주니어 네키",
"maxHp": 18,
"intents": [
{ "kind": "Attack", "value": 6 },
{ "kind": "Attack", "value": 8 },
{ "kind": "Debuff", "effect": "weak", "value": 1 }
]
},
"junior_bugi": {
"name": "주니어 부기",
"maxHp": 20,
"intents": [
{ "kind": "Attack", "value": 7 },
{ "kind": "Defend", "value": 5 },
{ "kind": "Attack", "value": 9 }
]
},
"dile": {
"name": "다일",
"maxHp": 65,
"intents": [
{ "kind": "Attack", "value": 13 },
{ "kind": "Defend", "value": 9 },
{ "kind": "Attack", "value": 8 },
{ "kind": "Attack", "value": 16 },
{ "kind": "Debuff", "effect": "weak", "value": 1 }
]
},
"mano": {
"name": "마노",
"maxHp": 80,
"intents": [
{ "kind": "Defend", "value": 12 },
{ "kind": "Attack", "value": 14 },
{ "kind": "Debuff", "effect": "vuln", "value": 1 },
{ "kind": "Attack", "value": 10 },
{ "kind": "AddCard", "card": "Wound", "count": 1 }
]
}
},
"activeEnemy": "slime",

View File

@@ -1,10 +0,0 @@
# 공격 적중 독
`attackPoison`은 전투 중 파워가 들고 있는 공용 필드입니다.
동작:
- 공격 카드가 실제 피해를 주면 독을 부여합니다.
- `aoe` 공격이면 모든 적에게 같은 양의 독을 붙입니다.
- `Envenom` 같은 카드가 이 필드를 사용합니다.

View File

@@ -1,22 +0,0 @@
# Bandit Card Audit
Current status of bandit cards and shared effect hooks.
## Implemented
`Neutralize`, `SilentStrike`, `Survivor`, `SilentDefend`, `Slice`, `DaggerSpray`, `DaggerThrow`, `PoisonedStab`, `SuckerPunch`, `LeadingStrike`, `FollowThrough`, `FlickFlack`, `Prepared`, `Deflect`, `BladeDance`, `Backflip`, `DodgeAndRoll`, `CloakAndDagger`, `DeadlyPoison`, `Snakebite`, `Untouchable`, `Backstab`, `PreciseCut`, `Finisher`, `MementoMori`, `Flechettes`, `Dash`, `Predator`, `CalculatedGamble`, `HiddenDaggers`, `Acrobatics`, `Blur`, `LegSweep`, `Reflex`, `Haze`, `Tactician`, `WellLaidPlans`, `InfiniteBlades`, `Footwork`, `GrandFinale`, `Adrenaline`, `ShadowStep`, `Assassinate`, `Nightmare`, `ToolsOfTheTrade`, `Afterimage`, `Burst`, `StormOfSteel`, `Abrasive`, `Suppress`, `Expertise`, `Shadowmeld`, `Pounce`, `BouncingFlask`, `Accuracy`, `PhantomBlades`, `Speedster`, `CorrosiveWave`, `Tracking`, `FanOfKnives`, `Strangle`, `Mirage`, `Accelerant`, `MasterPlanner`, `Outbreak`, `EscapePlan`, `HandTrick`, `NoxiousFumes`, `Pinpoint`, `TheHunt`, `Murder`, `Malaise`, `BladeOfInk`, `KnifeTrap`, `BulletTime`, `Envenom`, `SerpentForm`, `WraithForm`, `Skewer`, `Ricochet`, `Anticipate`, `PiercingWail`, `Expose`, `UpMySleeve`, `EchoingSlash`, `BubbleBubble`
Shared hooks already in use:
- `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`
## Open questions
None at the moment.

View File

@@ -1,126 +0,0 @@
# 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.

View File

@@ -1,5 +0,0 @@
# 카드 사용 시 피해
`cardPlayedDamage`는 카드를 사용할 때마다 현재 대상에게 체력을 직접 깎는 공용 효과입니다. 방어도는 무시하고, 같은 필드를 다른 카드에도 그대로 붙여 재사용할 수 있습니다.
`cardPlayedRandomDamage`는 같은 시점에 살아 있는 적 하나를 랜덤으로 골라 체력을 직접 깎습니다. `Strangle``SerpentForm` 같은 카드가 이 계열을 씁니다.

View File

@@ -1,31 +0,0 @@
# Codex Workflow
이 저장소에서 작업할 때는 토큰과 변경량을 아끼는 쪽을 기본으로 둔다.
## 작업 원칙
- 이미 확인한 사실은 다시 읽지 않는다.
- 같은 내용을 통째로 지우고 새로 쓰지 않는다.
- 수정은 가능한 한 `apply_patch`로 섹션 단위만 한다.
- 문서는 전체 재작성보다 부분 수정으로 유지한다.
- 카드 구현은 한 번에 하나씩, 공용 필드 우선으로 넣는다.
- 새 기능은 `데이터 1곳 + 런타임 1곳 + 테스트 1곳` 순서로 맞춘다.
## 읽기 원칙
- 파일은 필요한 것만 읽는다.
- 비슷한 파일은 병렬로 한 번에 확인한다.
- 같은 정보를 여러 번 요약하지 않는다.
## 쓰기 원칙
- 공용으로 표현 가능한 효과는 카드 전용 분기로 만들지 않는다.
- 같은 의미의 효과는 같은 필드 이름을 쓴다.
- 문서는 카드별 상태표와 공용 필드 사전을 분리해서 유지한다.
## 응답 원칙
- 중간 보고는 짧게 한다.
- 바뀐 점과 남은 점만 말한다.
- 불필요한 재설명은 줄인다.

View File

@@ -1,86 +0,0 @@
# 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-차용-덱컨셉` 참조.

View File

@@ -1,8 +0,0 @@
# 전투 드로우 누적
`damagePerCardDrawnThisCombat`은 이번 전투 동안 실제로 뽑힌 카드 수를 기준으로 공격력을 올리는 공용 필드입니다.
적용 예시:
- `Murder`: 이번 전투 동안 뽑은 카드 1장당 피해량이 1 증가

View File

@@ -1,22 +0,0 @@
# 드로우 연동 효과
드로우 결과를 받아 후속 효과를 처리하는 공용 패턴을 정리합니다.
## 현재 구현
- `draw`: 카드를 뽑음
- `drawUntilHandSize`: 손패가 지정 수치가 될 때까지 뽑음
- `drawSkillBlock`: 이번 카드로 뽑힌 카드 중 스킬 카드마다 방어도를 얻음
## 동작 방식
- 드로우 함수는 이번에 뽑힌 카드 ID 목록을 반환합니다.
- 카드 효과는 그 목록을 보고 조건을 판정합니다.
- 그래서 `EscapePlan` 같은 카드뿐 아니라, 나중에 같은 규칙이 필요한 카드에도 같은 필드를 붙이면 됩니다.
## 예시
- `EscapePlan`
- `draw = 1`
- `drawSkillBlock = 3`

View File

@@ -1,5 +0,0 @@
# 불가침
`intangible`는 카드를 사용할 때 플레이어에게 불가침 수치를 부여하는 공용 필드입니다. 불가침이 남아 있는 동안 받는 피해는 1로 줄어들고, 턴이 끝날 때 1씩 감소합니다.
`endTurnDexLoss`는 그 카드가 활성화된 동안 매 턴 종료 시 민첩을 잃게 만드는 공용 필드입니다. `WraithForm` 같은 카드가 이 조합을 사용합니다.

View File

@@ -1,12 +0,0 @@
# Next Skill Repeat
`nextSkillRepeatCount`는 다음에 사용하는 스킬 카드의 효과를 추가 횟수만큼 다시 적용하는 공용 필드입니다.
현재 구현은 카드가 발동할 때 이 수치를 전역 상태에 누적해 두고, 다음 스킬 카드가 실제로 사용되면 그 효과를 같은 카드에 대해 다시 한 번 이상 적용합니다. 카드 종류는 고정하지 않았기 때문에, 같은 필드를 다른 카드에도 그대로 붙일 수 있습니다.
예시:
- `Burst`
- `nextSkillRepeatCount = 1`
- 다음 스킬을 한 번 더 적용

View File

@@ -1,5 +0,0 @@
# 처치 보상
`rewardOnKill`은 해당 카드가 적을 처치했을 때 전투 보상 화면을 한 번 더 이어서 보여주는 공용 필드입니다. 현재 보상 UI는 3장 선택을 유지하고, 보상 화면만 추가로 한 번 더 열립니다.
`TheHunt`는 이 규칙을 사용합니다. 같은 패턴이 필요한 다른 카드에도 그대로 붙일 수 있습니다.

View File

@@ -1,106 +0,0 @@
# 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 위치 이동(렌더 무관).

View File

@@ -1,73 +0,0 @@
# 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 잔류) |

View File

@@ -1,14 +0,0 @@
# X 코스트 카드
`useAllEnergy`는 카드가 사용될 때 남은 에너지를 전부 쓰는 공용 필드입니다.
연동 필드:
- `xDamagePerEnergy`: 에너지 1당 피해량
- `xWeakPerEnergy`: 에너지 1당 약화량
적용 예시:
- `Skewer`: 남은 에너지 전부를 써서 `8 * energy` 피해
- `Malaise`: 남은 에너지 전부를 써서 약화 부여

View File

@@ -1,7 +0,0 @@
@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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
// ⚠️ 전투 규칙은 tools/deck/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것.
// (데이터는 data/*.json 공유, 규칙 로직은 JS로 중복 재현)
import { readFileSync } from 'node:fs';
@@ -27,16 +27,6 @@ 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
@@ -54,11 +44,6 @@ 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;
@@ -85,50 +70,16 @@ 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, 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;
});
export function chooseAction(hand, cards, energy) {
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id] && cards[x.id].cost <= energy && !cards[x.id].unplayable);
const powers = entries.filter((x) => cards[x.id].kind === 'Power');
const attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
const 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 dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(cards[x.id].cost, 1);
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(cards[x.id].cost, 1);
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
if (powers.length) return powers[0].i;
if (attacks.length) return bestBy(attacks, dmgEff).i;
@@ -155,144 +106,30 @@ function bump(s, cost, dmg, blk) {
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 = prepareCombatDrawPile(shuffle(starterDeck, rng), cards);
let drawPile = shuffle(starterDeck, rng);
let discard = [];
const exhaust = [];
let hand = [];
let pHp = PLAYER_HP, pBlock = 0;
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0, 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;
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0;
const powers = [];
const mob = monsters.map((m) => ({
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: m.str || 0, weak: 0, vuln: 0, poison: 0, artifact: m.artifact || 0,
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: 0, weak: 0, vuln: 0, poison: 0,
intents: m.intents, intentIdx: 0, alive: true,
}));
let turns = 0;
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 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++) {
@@ -300,225 +137,49 @@ export function simulateCombat(data, rng, stats) {
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 discardForTurnStart(n) {
const cnt = Math.min(n, hand.length);
for (let i = 0; i < cnt; i++) {
const idx = 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.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);
if (alive.length && c.damage) {
const target = chooseTarget(alive, calcAttack(c.damage || 0, pStr, pWeak, 0));
if (c.weak) target.weak += c.weak;
if (c.vuln) target.vuln += c.vuln;
const hitN = c.hits || 1;
let totalNv = 0;
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
dmg = totalNv;
if (c.aoe === true) {
for (const m2 of aliveList()) {
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
const r2 = applyDamage(m2.hp, m2.block, d2);
m2.hp = r2.hp; m2.block = r2.block;
if (m2.hp <= 0) m2.alive = false;
}
} else {
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
if (c.pierce === true) {
target.hp -= dealt;
target.hp -= dmg;
if (target.hp < 0) target.hp = 0;
} else {
const r = applyDamage(target.hp, target.block, dealt);
const r = applyDamage(target.hp, target.block, dmg);
target.hp = r.hp; target.block = r.block;
}
const attackPoison = powerFieldTotal('attackPoison');
if (dealt > 0 && attackPoison > 0) applyPoisonToMonster(target, attackPoison);
let killed = false;
if (target.hp <= 0) {
target.alive = false;
killed = true;
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
}
return { killed, dealt };
};
const resolveAttackRound = () => {
let roundKilled = false;
let roundDamage = 0;
if (useAoe === true) {
for (const m2 of aliveList()) {
const r2 = dealToTarget(m2, perHit);
roundDamage += r2.dealt;
if (r2.killed) roundKilled = true;
}
} else if (c.randomTargetEachHit === true) {
for (let h = 0; h < hitN; h++) {
const target = randomAliveMonster();
if (!target) break;
const r = dealToTarget(target, perHit);
roundDamage += r.dealt;
if (r.killed) roundKilled = true;
}
} else {
const preview = perHit;
const target = chooseTarget(aliveList(), preview);
if (target) {
if (c.weak) applyMonsterWeak(target, c.weak);
if (c.vuln) applyMonsterVuln(target, c.vuln);
const totalNv = perHit * hitN;
const r = dealToTarget(target, totalNv);
roundDamage += r.dealt;
if (r.killed) roundKilled = true;
}
}
dmg += roundDamage;
damageDealtThisTurn += roundDamage;
return roundKilled;
};
let roundKilled = false;
do {
roundKilled = resolveAttackRound();
} while (c.repeatOnKill === true && roundKilled === true && countAliveMonsters() > 0);
if (target.hp <= 0) target.alive = false;
}
}
if (c.block) blockGained = addBlock(c.block);
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
} else if (c.kind === 'Power') {
if (recordStats) powers.push(id);
} else {
if (c.block) blockGained = 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.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
if ((c.weak || c.vuln || c.poison) && alive.length) {
const target = chooseTarget(alive, 0);
if (c.weak) target.weak += c.weak;
if (c.vuln) target.vuln += c.vuln;
if (c.poison) target.poison += c.poison;
}
}
if (c.strength) pStr += c.strength;
@@ -526,60 +187,19 @@ export function simulateCombat(data, rng, stats) {
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.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.draw) draw(c.draw);
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) return;
if (!c.sly && !skillSlyOnPlayCards.has(id) && !turnSkillSlyCards.has(id)) return;
if (!c?.sly) return;
resolveCardEffects(id, c, 0, false);
}
function discardHandCard(idx, trigger = true) {
const [id] = hand.splice(idx, 1);
if (!id) return;
discard.push(id);
turnDiscardedCards++;
if (trigger) triggerSly(id);
}
function applyDiscardEffects(c) {
@@ -592,96 +212,35 @@ export function simulateCombat(data, rng, stats) {
}
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 파워)
if (nextTurnKeepBlock === true) nextTurnKeepBlock = false;
else pBlock = 0;
turnAttackMultiplier = nextTurnAttackMultiplier;
nextTurnAttackMultiplier = 1;
pBlock = 0;
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;
}
}
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 + energyBonus;
const drawBonus = nextTurnDraw + powerTurnDraw;
nextTurnDraw = 0;
draw(HAND_SIZE + drawBonus);
if (powerTurnDiscard > 0) discardForTurnStart(powerTurnDiscard);
let energy = ENERGY + energyBonus; draw(HAND_SIZE);
while (true) {
const alive = aliveList();
if (alive.length === 0) break;
const idx = chooseAction(hand, cards, energy, { drawPileCount: drawPile.length, nextSkillCostZero, skillCostReductionThisTurn, handCostZeroThisTurn, combatCardCostReduction });
const idx = chooseAction(hand, cards, energy);
if (idx < 0) break;
const id = hand[idx], c = cards[id];
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);
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;
energy -= c.cost;
resolveCardEffects(id, c, c.cost);
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, bonusRewardScreens };
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
}
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
let burn = 0;
@@ -690,18 +249,10 @@ export function simulateCombat(data, rng, stats) {
const kept = [];
for (const hid of hand) {
const hc = cards[hid];
if (hc?.retain === true || (hc?.class === 'shiv' && powerFieldTotal('shivRetain') > 0)) kept.push(hid);
if (hc?.retain === true) 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--;
@@ -709,24 +260,19 @@ export function simulateCombat(data, rng, stats) {
for (const m of mob) {
if (!m.alive) continue;
// 독 틱 — 행동 시작 시 (Lua EnemyActStep 동기화). 사망 시 행동 생략
const poisonTicks = 1 + Math.max(0, powerFieldTotal('extraPoisonTicks'));
for (let tick = 0; tick < poisonTicks; tick++) {
if (m.poison <= 0) break;
if (m.poison > 0) {
m.hp -= m.poison;
m.poison--;
if (m.hp <= 0) { m.hp = 0; m.alive = false; break; }
if (m.hp <= 0) { m.hp = 0; m.alive = false; continue; }
}
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 = calcEnemyAttack(it.value, m.str, m.weak, pVuln, enemyStrengthLossThisTurn);
const atk = calcAttack(it.value, m.str, m.weak, pVuln);
const beforeHp = pHp;
let incoming = atk;
if (pIntangible > 0 && incoming > 1) incoming = 1;
const r = applyDamage(pHp, pBlock, incoming); pHp = r.hp; pBlock = r.block;
const r = applyDamage(pHp, pBlock, atk); pHp = r.hp; pBlock = r.block;
if (beforeHp > pHp && pThorns > 0) {
m.hp -= pThorns;
if (m.hp <= 0) m.alive = false;
@@ -747,9 +293,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, bonusRewardScreens };
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp };
}
return { win: false, turns, playerHpRemaining: pHp, draw: true, bonusRewardScreens };
return { win: false, turns, playerHpRemaining: pHp, draw: true };
}
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }

View File

@@ -1,7 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, calcEnemyAttack, rarityForRoll,
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, rarityForRoll,
} from './sim-balance.mjs';
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
@@ -13,85 +13,6 @@ 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 });
@@ -262,19 +183,6 @@ 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: {
@@ -553,642 +461,3 @@ test("simulateCombat: addShiv creates shuriken cards in hand", () => {
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);
});

View File

@@ -1,629 +0,0 @@
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 }
}

View File

@@ -1,29 +1,15 @@
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 { 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 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)
self:ShowLobby()
local lp = _UserService.LocalPlayer
if lp ~= nil then
self:ReqLoadAscension(lp.PlayerComponent.UserId)
@@ -31,38 +17,12 @@ if lp ~= nil then
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
@@ -89,7 +49,7 @@ 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))`),
self:SetText("/ui/DefaultGroup/LobbyHud/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`),
method('AscHpMult', `local m = 1
if self.AscensionLevel >= 1 then m = m + 0.1 end
if self.AscensionLevel >= 6 then m = m + 0.1 end

View File

@@ -10,63 +10,41 @@ self:RenderCharacterSelect()`),
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 = "bandit" } }
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]
method('RenderCharacterSelect', `local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then
if self.SelectedClass == "warrior" then
warrior.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
else
warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
end
end
local btns = { { p = "/WarriorButton", c = "warrior" }, { p = "/MageButton", c = "magician" }, { p = "/BanditButton", c = "bandit" } }
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
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then
if self.SelectedClass == "magician" then
mage.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
else
mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
end
end
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
if thief ~= nil and thief.SpriteGUIRendererComponent ~= nil then
if self.SelectedClass == "bandit" then
thief.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
else
thief.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
end
end
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 .. "방어를 쌓고 버티다 강하게 역공하는 단단한 탱커."
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨")
elseif self.SelectedClass == "bandit" then
name = "도적"
eng = "Thief"
btnName = "/BanditButton"
desc = "직업군 · 모험가" .. nl .. "표창 난사와 독으로 빠르게 몰아치는 민첩한 직업."
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "도적 선택됨")
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)`),
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "법사 선택됨")
else
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요")
end`),
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "bandit" and self.SelectedClass ~= "magician" then
self:SetText("/ui/SelectUIGroup/CharacterSelectHud/SelectedClassStatus", "직업을 먼저 선택하세요")
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요")
return
end
self:StartRun()`),
@@ -76,5 +54,5 @@ if e ~= nil then
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
], 2),
]),
];

View File

@@ -3,26 +3,10 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
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
@@ -41,77 +25,12 @@ 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
if self.Energy < c.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
self.Energy = self.Energy - c.cost
self:ResolveCardEffects(cardId, c, false)
table.remove(self.Hand, slot)
if c.exhaust == true then
if self.ExhaustPile == nil then self.ExhaustPile = {} end
@@ -125,19 +44,12 @@ 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
@@ -163,8 +75,9 @@ 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)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetFrame", active)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
end`),
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
@@ -177,7 +90,7 @@ if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
self.CardHoverTweenId = 0
end
for i = 1, 10 do
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i))
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
@@ -189,7 +102,7 @@ self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection
method('OnCardDrag', `if self.DragSlot ~= slot then
return
end
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
local ui = _UILogic:ScreenToUIPosition(touchPoint)
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
@@ -211,7 +124,7 @@ end`, [
return
end
self.DragSlot = 0
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
@@ -224,14 +137,6 @@ self:ResolveCardDrop(slot, touchPoint)`, [
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
@@ -266,7 +171,7 @@ end`, [
{ 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]
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
if m == nil or m.alive ~= true then
m = nil
for i = 1, #self.Monsters do
@@ -274,159 +179,35 @@ if m == nil or m.alive ~= true then
end
end
if m == nil then
return false
return
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`, [
end`, [
{ 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")
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
if fx ~= nil then
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
fx.SpriteGUIRendererComponent.ImageRUID = image
@@ -446,15 +227,7 @@ _TimerService:SetTimerOnce(function()
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:DealDamageToTarget(damage, pierce)
self:ShowDmgPop(targetIndex, shown)
self:RenderCombat()
self:CheckCombatEnd()
@@ -465,7 +238,7 @@ end, 0.35)`, [
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
]),
method('PlayAoeFx', `self.FxBusy = true
local fx = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/SkillFx")
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
if fx ~= nil then
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
fx.SpriteGUIRendererComponent.ImageRUID = image
@@ -478,7 +251,6 @@ 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
@@ -486,35 +258,20 @@ _TimerService:SetTimerOnce(function()
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)`, [
@@ -530,7 +287,7 @@ 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)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false)
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
@@ -540,9 +297,6 @@ if self.PlayerBlock > 0 then
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
@@ -587,28 +341,21 @@ if idx == 0 or self.PlayerHp <= 0 then
return
end
local m = self.Monsters[idx]
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(idx)
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. 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
if m.poison ~= nil and m.poison > 0 then
m.hp = m.hp - m.poison
self:ShowDmgPop(idx, m.poison)
self:MonsterHitMotion(idx)
m.poison = m.poison - 1
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
self:RenderCombat()
self:SetEntityEnabled(base .. "/ActFrame", false)
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
return
end
end
m.block = 0
@@ -617,10 +364,6 @@ _TimerService:SetTimerOnce(function()
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
@@ -674,25 +417,10 @@ 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
method('CheckCombatEnd', `local anyAlive = false
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then anyAlive = true; break end
end

View File

@@ -10,60 +10,56 @@ 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
method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton")
if endTurn ~= nil and endTurn.ButtonComponent ~= nil then
if self.EndTurnHandler ~= nil then
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
self.EndTurnHandler = nil
end
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
end
local drawPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/DrawPile")
if drawPile ~= nil and (drawPile.ButtonComponent ~= nil or drawPile:AddComponent("ButtonComponent") ~= nil) then
local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile")
if drawPile ~= nil and drawPile.ButtonComponent ~= nil then
if self.DrawPileHandler ~= nil then
drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)
self.DrawPileHandler = nil
end
self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end)
end
local discardPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/DiscardPile")
if discardPile ~= nil and (discardPile.ButtonComponent ~= nil or discardPile:AddComponent("ButtonComponent") ~= nil) then
local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile")
if discardPile ~= nil and discardPile.ButtonComponent ~= nil then
if self.DiscardPileHandler ~= nil then
discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)
self.DiscardPileHandler = nil
end
self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end)
end
local exhaustPile = _EntityService:GetEntityByPath("/ui/RunUIGroup/DeckHud/ExhaustPile")
if exhaustPile ~= nil and (exhaustPile.ButtonComponent ~= nil or exhaustPile:AddComponent("ButtonComponent") ~= nil) then
local exhaustPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/ExhaustPile")
if exhaustPile ~= nil and exhaustPile.ButtonComponent ~= nil then
if self.ExhaustPileHandler ~= nil then
exhaustPile:DisconnectEvent(ButtonClickEvent, self.ExhaustPileHandler)
self.ExhaustPileHandler = nil
end
self.ExhaustPileHandler = exhaustPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("exhaust") end)
end
local inspectClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud/Close")
if inspectClose ~= nil and (inspectClose.ButtonComponent ~= nil or inspectClose:AddComponent("ButtonComponent") ~= nil) then
local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close")
if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then
if self.DeckInspectCloseHandler ~= nil then
inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)
self.DeckInspectCloseHandler = nil
end
self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)
end
local allDeckButton = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/AllDeckButton")
if allDeckButton ~= nil and (allDeckButton.ButtonComponent ~= nil or allDeckButton:AddComponent("ButtonComponent") ~= nil) then
local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton")
if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then
if self.AllDeckHandler ~= nil then
allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)
self.AllDeckHandler = nil
end
self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)
end
local allDeckClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:AddComponent("ButtonComponent") ~= nil) then
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
if self.AllDeckCloseHandler ~= nil then
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
self.AllDeckCloseHandler = nil
@@ -71,20 +67,10 @@ if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:
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))
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
local cardPath = "/ui/RunUIGroup/CardHand/Card" .. tostring(i)
local cardPath = "/ui/DefaultGroup/CardHand/Card" .. tostring(i)
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
@@ -92,24 +78,24 @@ for i = 1, 10 do
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
if cardEntity.ButtonComponent ~= nil then
cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end)
end
end
end
for i = 1, 3 do
local rc = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Reward" .. tostring(i))
if rc ~= nil and (rc.ButtonComponent ~= nil or rc:AddComponent("ButtonComponent") ~= nil) then
local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i))
if rc ~= nil and rc.ButtonComponent ~= nil then
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
if rc.UITouchReceiveComponent ~= nil then
local cardPath = "/ui/RunUIGroup/RewardHud/Reward" .. tostring(i)
local cardPath = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(i)
rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
end
end
end
local skip = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Skip")
if skip ~= nil and (skip.ButtonComponent ~= nil or skip:AddComponent("ButtonComponent") ~= nil) then
local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip")
if skip ~= nil and skip.ButtonComponent ~= nil then
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
end
local mapNodeIds = {}
@@ -121,42 +107,42 @@ 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
local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid)
if mn ~= nil and mn.ButtonComponent ~= nil then
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
end
end
for i = 1, 3 do
local sc = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Card" .. tostring(i))
if sc ~= nil and (sc.ButtonComponent ~= nil or sc:AddComponent("ButtonComponent") ~= nil) then
local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i))
if sc ~= nil and sc.ButtonComponent ~= nil then
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
if sc.UITouchReceiveComponent ~= nil then
local cardPath = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i)
local cardPath = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
end
end
end
local shopLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Leave")
if shopLeave ~= nil and (shopLeave.ButtonComponent ~= nil or shopLeave:AddComponent("ButtonComponent") ~= nil) then
local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave")
if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then
shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
end
local shopRelic = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Relic")
if shopRelic ~= nil and (shopRelic.ButtonComponent ~= nil or shopRelic:AddComponent("ButtonComponent") ~= nil) then
local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)
end
local restLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/RestHud/Leave")
if restLeave ~= nil and (restLeave.ButtonComponent ~= nil or restLeave:AddComponent("ButtonComponent") ~= nil) then
local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave")
if restLeave ~= nil and restLeave.ButtonComponent ~= nil then
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
end
for i = 1, ${MAX_MONSTERS} do
local ms = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i))
if ms ~= nil and (ms.ButtonComponent ~= nil or ms:AddComponent("ButtonComponent") ~= nil) then
local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i))
if ms ~= nil and ms.ButtonComponent ~= nil then
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
end
end
for i = 1, 10 do
local rs = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
local idx = i
rs:ConnectEvent(UITouchEnterEvent, function()
@@ -170,7 +156,7 @@ for i = 1, 10 do
end
end
for i = 1, 5 do
local ps = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
local idx = i
ps:ConnectEvent(UITouchEnterEvent, function()
@@ -184,42 +170,42 @@ for i = 1, 5 do
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
local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use")
if pmUse ~= nil and pmUse.ButtonComponent ~= nil then
pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end)
end
local pmToss = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Toss")
if pmToss ~= nil and (pmToss.ButtonComponent ~= nil or pmToss:AddComponent("ButtonComponent") ~= nil) then
local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss")
if pmToss ~= nil and pmToss.ButtonComponent ~= nil then
pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end)
end
local pmClose = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/PotionMenu/Close")
if pmClose ~= nil and (pmClose.ButtonComponent ~= nil or pmClose:AddComponent("ButtonComponent") ~= nil) then
local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close")
if pmClose ~= nil and pmClose.ButtonComponent ~= nil then
pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end)
end
local shopPotion = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Potion")
if shopPotion ~= nil and (shopPotion.ButtonComponent ~= nil or shopPotion:AddComponent("ButtonComponent") ~= nil) then
local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
end
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
if chest ~= nil and (chest.ButtonComponent ~= nil or chest:AddComponent("ButtonComponent") ~= nil) then
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
if chest ~= nil and chest.ButtonComponent ~= nil then
chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end)
end
local treasureLeave = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Leave")
if treasureLeave ~= nil and (treasureLeave.ButtonComponent ~= nil or treasureLeave:AddComponent("ButtonComponent") ~= nil) then
local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave")
if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then
treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
end
local jcRelic = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobChoiceHud/RelicButton")
if jcRelic ~= nil and (jcRelic.ButtonComponent ~= nil or jcRelic:AddComponent("ButtonComponent") ~= nil) then
local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton")
if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then
jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end)
end
local jcJob = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobChoiceHud/JobButton")
if jcJob ~= nil and (jcJob.ButtonComponent ~= nil or jcJob:AddComponent("ButtonComponent") ~= nil) then
local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton")
if jcJob ~= nil and jcJob.ButtonComponent ~= nil then
jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end)
end
for i = 1, 3 do
local slotIdx = i
local jb = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i))
if jb ~= nil and (jb.ButtonComponent ~= nil or jb:AddComponent("ButtonComponent") ~= nil) then
local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i))
if jb ~= nil and jb.ButtonComponent ~= nil then
jb:ConnectEvent(ButtonClickEvent, function()
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
self:SetJob(self.JobOpts[slotIdx].id)
@@ -228,43 +214,13 @@ for i = 1, 3 do
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
self.PlayerBlock = 0
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]]
@@ -275,124 +231,16 @@ if self.PlayerPowers ~= nil 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:DrawCards(5)
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
@@ -400,24 +248,6 @@ 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]]
@@ -433,38 +263,19 @@ 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
\tif c ~= nil and c.retain == true then
\t\ttable.insert(kept, cardId)
\telse
\t\ttable.insert(self.DiscardPile, cardId)
\tend
end
self.Hand = kept
if self.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
self:EnemyTurn()`),
method('DrawCards', `local drawnSlots = {}
for i = 1, amount do
\tif #self.DrawPile <= 0 then
\t\tself:RecycleDiscardIntoDraw()
@@ -473,33 +284,28 @@ for i = 1, amount do
\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)
\t\tif #self.Hand <= 5 then
\t\t\ttable.insert(drawnSlots, #self.Hand)
\t\tend
\tend
end
self:RenderPiles()
if drewAny == true then
\tself:RenderHand(false)
end
if animate == true and #drawnSlots > 0 then
\tself:RenderHand(false)
\tlocal drawStart = Vector2(-590, 8)
\tfor i = 1, #drawnSlots do
\t\tlocal slot = drawnSlots[i]
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
\tend
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
@@ -527,11 +333,11 @@ for i = 1, #self.DiscardPile do
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")
method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", self:FormatNumber(#self.DrawPile))
self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", self:FormatNumber(#self.DiscardPile))
self:SetText("/ui/DefaultGroup/DeckHud/ExhaustPile/Count", self:FormatNumber(#(self.ExhaustPile or {})))
self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then
self:OpenDeckInspect(self.DeckInspectKind)
end`),

View File

@@ -6,7 +6,7 @@ export const deckViewMethods = [
method('OpenDeckInspect', `self.DeckInspectKind = kind
if self.DeckAllOpen == true then
self.DeckAllOpen = false
local allHud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if allHud ~= nil then
allHud.Enable = false
end
@@ -24,12 +24,12 @@ else
title = "뽑을 덱"
end
self:RenderDeckInspect(pile, title)
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if hud ~= nil then
hud.Enable = true
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
method('CloseDeckInspect', `self.DeckInspectKind = ""
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if hud ~= nil then
hud.Enable = false
end`),
@@ -41,46 +41,51 @@ 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)
self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix)
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty")
if empty ~= nil then
empty.Enable = count <= 0
end
for i = 1, 60 do
local 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)
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i))
if e ~= nil then
local cardId = nil
if pile ~= nil then
cardId = pile[i]
end
if cardId == nil then
e.Enable = false
else
e.Enable = true
self:ApplyInspectCardVisual(i, cardId)
end
end
end`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
]),
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/WarriorTab")
if warriorTab ~= nil and (warriorTab.ButtonComponent ~= nil or warriorTab:AddComponent("ButtonComponent") ~= nil) then
method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/WarriorTab")
if warriorTab ~= nil and warriorTab.ButtonComponent ~= nil then
if self.WarriorDeckTabHandler ~= nil then
warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler)
self.WarriorDeckTabHandler = nil
end
self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end)
end
local thiefTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/ThiefTab")
if thiefTab ~= nil and (thiefTab.ButtonComponent ~= nil or thiefTab:AddComponent("ButtonComponent") ~= nil) then
local thiefTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/ThiefTab")
if thiefTab ~= nil and thiefTab.ButtonComponent ~= nil then
if self.ThiefDeckTabHandler ~= nil then
thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler)
self.ThiefDeckTabHandler = nil
end
self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("bandit") end)
end
local mageTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/MageTab")
if mageTab ~= nil and (mageTab.ButtonComponent ~= nil or mageTab:AddComponent("ButtonComponent") ~= nil) then
local mageTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/MageTab")
if mageTab ~= nil and mageTab.ButtonComponent ~= nil then
if self.MageDeckTabHandler ~= nil then
mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler)
self.MageDeckTabHandler = nil
@@ -89,31 +94,12 @@ if mageTab ~= nil and (mageTab.ButtonComponent ~= nil or mageTab:AddComponent("B
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")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if hud ~= nil then
hud.Enable = true
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
method('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 ~= "bandit" then
className = "bandit"
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
@@ -161,38 +147,39 @@ 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 = "bandit" },
{ path = "/ui/DeckUIGroup/DeckAllHud/MageTab", cls = "magician" },
{ path = "/ui/DefaultGroup/DeckAllHud/WarriorTab", cls = "warrior" },
{ path = "/ui/DefaultGroup/DeckAllHud/ThiefTab", cls = "bandit" },
{ path = "/ui/DefaultGroup/DeckAllHud/MageTab", cls = "magician" },
}
for i = 1, #tabs do
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)
if e ~= nil then
e.Enable = self.ClassDeckMode == true
if e.SpriteGUIRendererComponent ~= nil then
if self.ClassDeckClass == tabs[i].cls then
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1)
else
e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1)
end
end
end
end`),
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud")
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if inspectHud ~= nil then
inspectHud.Enable = false
end
self.DeckInspectKind = ""
self.ClassDeckMode = false
self.ClassDeckClass = ""
self.DebugCardPickerMode = false
self:RenderClassDeckTabs()
self.DeckAllOpen = true
self:RenderAllDeck()
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if hud ~= nil then
hud.Enable = true
end`),
method('CloseAllDeck', `self.DeckAllOpen = false
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if hud ~= nil then
hud.Enable = false
end
@@ -202,7 +189,6 @@ if self.ClassDeckMode == true then
self.ClassDeckTitle = ""
self.ClassDeckClass = ""
end
self.DebugCardPickerMode = false
self:RenderClassDeckTabs()
if self.CodexMode == true then
self.CodexMode = false
@@ -213,46 +199,31 @@ 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:SetText("/ui/DefaultGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")")
self:RenderClassDeckTabs()
self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud/Empty", count <= 0)
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Empty")
if empty ~= nil then
empty.Enable = count <= 0
end
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)
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(i))
if e ~= nil then
local cardId = pile[i]
if cardId == nil then
e.Enable = false
else
e.Enable = true
self:ApplyAllDeckCardVisual(i, cardId)
end
end
end`),
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
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' },
]),
];

View File

@@ -3,42 +3,6 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
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
@@ -56,7 +20,7 @@ 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))
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
\tif cardEntity ~= nil then
\t\tlocal cardId = self.Hand[i]
\t\tif cardId == nil then
@@ -96,7 +60,7 @@ if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
end
self:SetText(base .. "/Cost", string.format("%d", c.cost))
self:SetText(base .. "/Name", c.name)
self:SetText(base .. "/Desc", self:FormatCardDescription(c.desc))
self:SetText(base .. "/Desc", c.desc)
local art = _EntityService:GetEntityByPath(base .. "/Art")
if art ~= nil then
if c.image ~= nil and c.image ~= "" then
@@ -117,11 +81,11 @@ local xs = {}
local baseY = 0
local hoverIndex = 0
local push = 110
if string.find(path, "/ui/RunUIGroup/CardHand/Card") == 1 then
if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then
if self.DragSlot ~= nil and self.DragSlot > 0 then
return
end
prefix = "/ui/RunUIGroup/CardHand/Card"
prefix = "/ui/DefaultGroup/CardHand/Card"
count = 0
if self.Hand ~= nil then count = #self.Hand end
for i = 1, count do
@@ -129,14 +93,14 @@ if string.find(path, "/ui/RunUIGroup/CardHand/Card") == 1 then
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"
elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then
prefix = "/ui/DefaultGroup/RewardHud/Reward"
count = 3
xs = { -300, 0, 300 }
baseY = 0
hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0
elseif string.find(path, "/ui/RunUIGroup/ShopHud/Card") == 1 then
prefix = "/ui/RunUIGroup/ShopHud/Card"
elseif string.find(path, "/ui/DefaultGroup/ShopHud/Card") == 1 then
prefix = "/ui/DefaultGroup/ShopHud/Card"
count = 3
xs = { -300, 0, 300 }
baseY = 20
@@ -195,7 +159,7 @@ 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)`, [
method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
@@ -217,7 +181,7 @@ 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))
method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if cardEntity == nil or cardEntity.UITransformComponent == nil then
\treturn
end
@@ -239,132 +203,15 @@ end, 1 / 60)`, [
{ 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
@@ -377,9 +224,6 @@ 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
@@ -387,187 +231,22 @@ 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
method('ResolveCardEffects', `if c == nil 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
if c.damage ~= 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)
total = total + self:CalcPlayerAttack(c.damage)
end
local useAoe = c.aoe == true
if c.class == "shiv" and (self.ShivAoeThisCombat == true or self:HasPowerField("shivAoe") == true) then
useAoe = true
if c.aoe == true then
self:PlayAoeFx(c.fx or c.image, total)
else
self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true)
end
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)
@@ -599,146 +278,40 @@ 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
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
local tm = self.Monsters[self.TargetIndex]
if tm == nil or tm.alive ~= true then
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
end
end
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
if tm ~= nil and tm.alive == true then
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end
if c.vuln ~= nil then
tm.vuln = tm.vuln + c.vuln
if self:HasRelic("championBelt") then
tm.weak = tm.weak + 1
end
end
end
end
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
self:DrawCards(c.draw, true)
end
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
self:AddCardsToHand("Shiv", c.addShiv)
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
{ Type: '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
if c == nil or c.sly ~= true 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' }]),
self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
method('DiscardHandCard', `if self.Hand == nil then
return
end
@@ -746,40 +319,22 @@ 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")
method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/DiscardPrompt")
if e == nil then
return
end
if self:IsDiscardSelecting() == true then
local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining
self:SetText("/ui/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)
self:SetText("/ui/DefaultGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. self:FormatNumber(picked + 1) .. "/" .. self:FormatNumber(self.DiscardSelectTotal))
e.Enable = true
else
e.Enable = false
@@ -787,11 +342,10 @@ 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
if c.discardAll == true then
n = #self.Hand
elseif c.discard ~= nil then
n = math.min(c.discard, #self.Hand)
end
if n <= 0 then
@@ -801,137 +355,28 @@ 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()
if shivCount > 0 then
self:AddCardsToHand("Shiv", shivCount)
end
if delayRender == true then
_TimerService:SetTimerOnce(finish, 0.22)
else
finish()
end`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delayRender' }]),
self:RenderHand(false)
self:RenderPiles()
self:RenderCombat()
self:CheckCombatEnd()`),
method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then
return false
end
@@ -939,21 +384,18 @@ if self.Hand == nil or self.Hand[slot] == nil then
return true
end
local discarded = self.Hand[slot]
self:DiscardHandCard(slot, true, true)
self:DiscardHandCard(slot, true)
if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then
self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick
end
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)
self:FinishDiscardSelection()
else
self:UpdateDiscardPrompt()
self:RenderHand(false)
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'),
];

View File

@@ -95,7 +95,7 @@ if self:AddPotion(pid) == true then
self:Toast("물약 획득: " .. p.name)
end`),
method('RenderPotions', `for i = 1, 5 do
local base = "/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local pid = nil
@@ -121,11 +121,11 @@ 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)
self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc)
end
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('ClosePotionMenu', `self.PotionMenuSlot = 0
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", false)`),
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`),
method('UsePotion', `if self.PotionMenuSlot <= 0 then
return
end
@@ -133,8 +133,8 @@ 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")
local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud")
local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand")
if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then
self:Toast("전투 중에만 사용할 수 있습니다")
return
@@ -189,7 +189,7 @@ if self.RunRelics ~= nil then
count = #self.RunRelics
end
for i = 1, 10 do
local base = "/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local rid = nil
@@ -209,5 +209,5 @@ local of = ""
if count > 10 then
of = "+" .. tostring(count - 9)
end
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/RelicOverflow", of)`),
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`),
];

View File

@@ -3,10 +3,10 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const jobMethods = [
method('ShowJobChoice', `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)
method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`),
method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
if kind == "relic" then
local bid = self:PickNewRelic()
if bid ~= "" then
@@ -26,7 +26,7 @@ if opts == nil then
end
self.JobOpts = opts
for i = 1, 3 do
local base = "/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i)
local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)
local o = opts[i]
if o ~= nil then
self:SetEntityEnabled(base, true)
@@ -40,7 +40,7 @@ for i = 1, 3 do
self:SetEntityEnabled(base, false)
end
end
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", true)`),
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`),
method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then
for cls, list in pairs(self.Jobs) do
for i = 1, #list do
@@ -73,7 +73,7 @@ if starter ~= "" then
self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name)
end
end
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
self:SetEntityEnabled("/ui/SelectUIGroup/JobSelectHud", false)
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]),
];

View File

@@ -1,21 +0,0 @@
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' }]),
];

View File

@@ -115,7 +115,7 @@ for i = 1, #list do
end
end
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
method('RenderMapNode', `local base = "/ui/RunUIGroup/MapHud/Node_" .. id
method('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id
local e = _EntityService:GetEntityByPath(base)
if e == nil then
return
@@ -151,7 +151,7 @@ if e.SpriteGUIRendererComponent ~= nil then
e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85)
end
end
if (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then
if e.ButtonComponent ~= nil then
e.ButtonComponent.Enable = reachable
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
method('RenderMapDots', `local node = self.MapNodes[fromId]
@@ -162,7 +162,7 @@ if node ~= nil then
end
end
for k = 1, 3 do
local d = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
if d ~= nil then
d.Enable = has
if has == true and d.SpriteGUIRendererComponent ~= nil then
@@ -210,7 +210,7 @@ if self.VisitedNodes == nil then
self.VisitedNodes = {}
end
table.insert(self.VisitedNodes, id)
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
if hud ~= nil then
hud.Enable = false
end

View File

@@ -1,34 +0,0 @@
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)`),
];

View File

@@ -1,18 +0,0 @@
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' }]),
];

View File

@@ -1,296 +1,308 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const renderMethods = [
method('BuffsLabel', `local parts = {}
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
return table.concat(parts, " ")`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
], 0, 'string'),
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
local base = "/ui/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))`),
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const renderMethods = [
method('BuffsLabel', `local parts = {}
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
return table.concat(parts, " ")`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
], 0, 'string'),
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)
local m = self.Monsters[i]
if m ~= nil and m.alive == true then
self:SetEntityEnabled(base, true)
self:SetText(base .. "/Name", m.name)
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
local intent = m.intents[m.intentIdx]
local t = ""
if intent ~= nil then
if intent.kind == "Attack" then
local atk = intent.value + m.str
if m.weak > 0 then atk = math.floor(atk * 0.75) end
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
t = "공격 " .. tostring(atk)
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
elseif intent.kind == "Debuff" then
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
else t = "취약 " .. tostring(intent.value) .. " 부여" end
elseif intent.kind == "AddCard" then
t = "저주 카드 추가"
end
end
self:SetText(base .. "/Intent", t)
local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
local shownTarget = self.TargetIndex
if dragActive == true then shownTarget = self.DragTargetIndex end
self:SetEntityEnabled(base .. "/TargetFrame", i == shownTarget)
self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive)
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
if intent.kind == "Attack" then
intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1)
elseif intent.kind == "Debuff" then
intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1)
elseif intent.kind == "AddCard" then
intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1)
else
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
end
end
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0))
else
self:SetEntityEnabled(base, false)
end
end
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
if self.PlayerDex ~= nil and self.PlayerDex > 0 then
if pb ~= "" then pb = pb .. " " end
pb = pb .. "민첩+" .. tostring(self.PlayerDex)
end
if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then
if pb ~= "" then pb = pb .. " " end
pb = pb .. "가시" .. tostring(self.PlayerThorns)
end
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
local names = {}
for i = 1, #self.PlayerPowers do
local pc = self.Cards[self.PlayerPowers[i]]
if pc ~= nil then table.insert(names, pc.name) end
end
if pb ~= "" then pb = pb .. " · " end
pb = pb .. table.concat(names, " ")
end
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb)
self:RenderRun()`),
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
local base = "/ui/DefaultGroup/CombatHud/DmgPop" .. slotKey
local pop = _EntityService:GetEntityByPath(base)
if pop == nil then
return
end
self.DmgPopSeq = (self.DmgPopSeq or 0) + 1
local popSeq = self.DmgPopSeq
self:SetText(base, "")
local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
local shown = tostring(math.max(0, math.floor(amount)))
if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
end
local digits = {}
for i = 1, string.len(shown) do
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
end
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false)
end
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitPath = base .. "/Digit" .. tostring(i)
local digitEntity = _EntityService:GetEntityByPath(digitPath)
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
if digits[i] ~= nil then
digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
if digitEntity.UITransformComponent ~= nil then
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
end
self:SetEntityEnabled(digitPath, true)
else
self:SetEntityEnabled(digitPath, false)
end
end
end
local popPos = nil
local m = self.Monsters[slot]
if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
local wp = m.entity.TransformComponent.WorldPosition
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45}))
popPos = _UILogic:ScreenToUIPosition(screen)
else
local slotEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. slotKey)
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
local sp = slotEntity.UITransformComponent.anchoredPosition
popPos = Vector2(sp.x, sp.y + 76)
end
end
if pop ~= nil and pop.UITransformComponent ~= nil then
if popPos ~= nil then
pop.UITransformComponent.anchoredPosition = popPos
else
pop.UITransformComponent.anchoredPosition = Vector2(0, 120)
end
end
self:SetEntityEnabled(base, true)
for i = 1, 6 do
_TimerService:SetTimerOnce(function()
if self.DmgPopSeq ~= popSeq then
return
end
local p = _EntityService:GetEntityByPath(base)
if p ~= nil and p.UITransformComponent ~= nil then
local cur = p.UITransformComponent.anchoredPosition
p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
end
end, 0.045 * i)
end
_TimerService:SetTimerOnce(function()
if self.DmgPopSeq ~= popSeq then
return
end
self:SetEntityEnabled(base, false)
end, 0.48)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
]),
method('ShowPlayerDmgPop', `local base = "/ui/DefaultGroup/CombatHud/PlayerPanel/DmgPop"
if amount > 0 then
self:SetText(base, "-" .. string.format("%d", amount))
else
self:SetText(base, "막음")
end
self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
if lp == nil then
return
end
if lp.StateComponent == nil then
return
end
pcall(function() lp.StateComponent:ChangeState("ATTACK") end)
_TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("IDLE") end)
end
end, 0.5)`),
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer
if lp == nil then
return
end
if lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("HIT") end)
end
local tr = lp.TransformComponent
if tr == nil then
return
end
local p = tr.Position
tr.Position = Vector3(p.x - 0.15, p.y, p.z)
_TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end
end, 0.15)`),
method('MonsterLunge', `local m = self.Monsters[idx]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return
end
if m.motionBusy == true then
return
end
m.motionBusy = true
local e = m.entity
local tr = e.TransformComponent
if tr == nil then
m.motionBusy = false
return
end
local p = tr.Position
tr.Position = Vector3(p.x - 0.35, p.y, p.z)
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end
m.motionBusy = false
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]),
method('MonsterHitMotion', `local m = self.Monsters[slot]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return
end
local e = m.entity
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.hitClip
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.standClip
end
end, 0.5)
else
if m.motionBusy == true then
return
end
m.motionBusy = true
local tr = e.TransformComponent
if tr == nil then
m.motionBusy = false
return
end
local p = tr.Position
local seq = { 0.12, -0.12, 0 }
for i = 1, #seq do
local dx = seq[i]
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z)
end
if i == #seq then
m.motionBusy = false
end
end, 0.06 * i)
end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
if e == nil or e.UITransformComponent == nil then
return
end
local ratio = 0
if maxHp > 0 then ratio = hp / maxHp end
if ratio < 0 then ratio = 0 end
local w = width * ratio
e.UITransformComponent.RectSize = Vector2(w, 14)`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
]),
method('PositionMonsterSlot', `local m = self.Monsters[slot]
if m == nil or m.entity == nil or not isvalid(m.entity) then
return
end
local tr = m.entity.TransformComponent
if tr == nil then
return
end
local wp = tr.WorldPosition
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y}))
local uipos = _UILogic:ScreenToUIPosition(screen)
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = uipos
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
self.TargetIndex = slot
self:RenderCombat()
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층"
if self.AscensionLevel > 0 then
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
end
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Floor", floorText)
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
];

View File

@@ -11,8 +11,8 @@ for id, c in pairs(self.Cards) do
end
table.sort(pool)
return pool`, [], 0, 'any'),
method('OfferReward', `self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", false)
method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
local pool = self:CardPool()
local byRarity = {}
for _, id in ipairs(pool) do
@@ -30,15 +30,15 @@ for i = 1, 3 do
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
self:ApplyRewardVisual(i, self.RewardChoices[i])
end
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
if hud ~= nil then
hud.Enable = true
end`),
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/RunUIGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
return
end
if slot ~= 0 and self.RewardChoices ~= nil then
@@ -47,12 +47,7 @@ if slot ~= 0 and self.RewardChoices ~= 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")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
if hud ~= nil then
hud.Enable = false
end

View File

@@ -1,5 +1,5 @@
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 { 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 runMethods = [
@@ -32,7 +32,6 @@ self.PlayerJob = ""
${luaJobsTable(JOBS)}
${luaFramesTable()}
${luaNodeIconsTable()}
${luaCharsTable()}
self:GenerateMap()
self:BindButtons()
self:AddRelic("${RELICS.startingRelic}")
@@ -59,45 +58,21 @@ _TimerService:SetTimerOnce(function()
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:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/DiscardPrompt", false)
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
self.MaxEnergy = 3
self.Turn = 0
self.PlayerBlock = 0
self.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
@@ -105,16 +80,6 @@ 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 = {}
@@ -125,24 +90,11 @@ 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)`),
self:RenderCombat()`),
method('RegisterMonster', `if self.Registered == nil then
self.Registered = {}
end
@@ -240,7 +192,7 @@ for i = 1, n do
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,
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0,
hitClip = hitClip, standClip = standClip, motionBusy = false,
intents = intents, intentIdx = startIdx, alive = true, slot = i }
self:ReviveMonsterEntity(item.entity)

View File

@@ -1,24 +1,37 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const runEndMethods = [
method('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' }]),
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const runEndMethods = [
method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} }
local target = maps[self.Floor]
if target == nil then
return
end
local lp = _UserService.LocalPlayer
if lp == nil then
return
end
if lp.CurrentMapName == target then
return
end
_TeleportService:TeleportToMapPosition(lp, Vector3(-6, 0.03, 0), target)`),
method('ShowResult', `self:SetText("/ui/DefaultGroup/CombatHud/Result", text)
local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/Result")
if entity ~= nil then
entity.Enable = true
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
method('EndRun', `local msg = text
if text == "런 클리어!" and self.AscensionLevel >= self.AscensionUnlocked and self.AscensionUnlocked < 10 then
self.AscensionUnlocked = self.AscensionUnlocked + 1
local lp = _UserService.LocalPlayer
if lp ~= nil then
self:SaveAscension(self.AscensionUnlocked, lp.PlayerComponent.UserId)
end
self:RenderAscension()
msg = "런 클리어! 승천 " .. string.format("%d", self.AscensionUnlocked) .. " 해금!"
end
self:ShowResult(msg)
self.RunActive = false
_TimerService:SetTimerOnce(function() self:ShowLobby() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
];

View File

@@ -20,11 +20,11 @@ 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))
method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold))
for i = 1, 3 do
local cid = self.ShopChoices[i]
local c = self.Cards[cid]
local base = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i)
local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
if c ~= nil then
self:ApplyCardFace(base, cid)
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
@@ -38,9 +38,9 @@ for i = 1, 3 do
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")
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
local re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
if re ~= nil and re.SpriteGUIRendererComponent ~= nil then
if self.ShopRelicBought == true then
re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
@@ -51,9 +51,9 @@ if rr ~= nil then
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")
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc)
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소")
local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then
if self.ShopPotionBought == true then
pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
@@ -106,24 +106,24 @@ 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:SetText("/ui/DefaultGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")")
self:RenderCombat()
self:ShowState("rest")`),
method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud")
method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud")
if s ~= nil then
s.Enable = false
end
local r = _EntityService:GetEntityByPath("/ui/RunUIGroup/RestHud")
local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud")
if r ~= nil then
r.Enable = false
end
local t = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud")
local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud")
if t ~= nil then
t.Enable = false
end
self:ShowMap()`),
method('ShowTreasure', `self.ChestOpened = false
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
if chest ~= nil then
if chest.SpriteGUIRendererComponent ~= nil then
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}"
@@ -132,15 +132,15 @@ if chest ~= 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:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false)
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true)
self:ShowState("treasure")`),
method('OpenChest', `if self.ChestOpened == true then
return
end
self.ChestOpened = true
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Hint", false)
local chest = _EntityService:GetEntityByPath("/ui/RunUIGroup/TreasureHud/Chest")
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false)
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
local steps = { 10, -10, 8, -8, 5, 0 }
for i = 1, #steps do
local dx = steps[i]
@@ -167,7 +167,7 @@ _TimerService:SetTimerOnce(function()
end
self.Gold = self.Gold + g
self:RenderRun()
self:SetText("/ui/RunUIGroup/TreasureHud/Reward", msg)
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud/Reward", true)
self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg)
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true)
end, 0.55)`),
];

View File

@@ -6,8 +6,8 @@ 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)`),
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`),
method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
local e1, pts = ds:GetAndWait("soulPoints")
local e2, unl = ds:GetAndWait("soulUnlocks")
@@ -63,7 +63,7 @@ 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 base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)
local d = defs[i]
if d == nil then
self:SetEntityEnabled(base, false)
@@ -87,8 +87,8 @@ 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
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i))
if e ~= nil and e.ButtonComponent ~= nil then
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
end
end`),

View File

@@ -2,49 +2,41 @@ import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, A
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { 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 = [
export const stateMethods = [
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false)
self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false)
self:SetEntityEnabled("/ui/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),
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
method('ShowState', `self:HideGameHud()
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
self:SetEntityEnabled("/ui/SelectUIGroup/CharacterSelectHud", state == "charselect")
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", state == "lobby")
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect")
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", state == "lobby")
if state == "map" then
self:SetEntityEnabled("/ui/RunUIGroup/MapHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true)
elseif state == "combat" then
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud", true)
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", true)
self:SetEntityEnabled("/ui/RunUIGroup/CardHand", true)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true)
elseif state == "shop" then
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true)
elseif state == "rest" then
self:SetEntityEnabled("/ui/RunUIGroup/RestHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true)
elseif state == "treasure" then
self:SetEntityEnabled("/ui/RunUIGroup/TreasureHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true)
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
method('ShowMainMenu', `self.SelectedClass = ""
self:RenderAscension()
@@ -54,39 +46,39 @@ 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 buttonEntity ~= nil and buttonEntity.ButtonComponent ~= nil then
if self.NewGameHandler ~= nil then
buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)
self.NewGameHandler = nil
end
self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowCharacterSelect() end)
end
local warrior = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/WarriorButton")
if warrior ~= nil and (warrior.ButtonComponent ~= nil or warrior:AddComponent("ButtonComponent") ~= nil) then
local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
if warrior ~= nil and warrior.ButtonComponent ~= nil then
if self.WarriorSelectHandler ~= nil then
warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler)
self.WarriorSelectHandler = nil
end
self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end)
end
local thief = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/BanditButton")
if thief ~= nil and (thief.ButtonComponent ~= nil or thief:AddComponent("ButtonComponent") ~= nil) then
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
if thief ~= nil and thief.ButtonComponent ~= nil then
if self.ThiefSelectHandler ~= nil then
thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler)
self.ThiefSelectHandler = nil
end
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") end)
end
local mage = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/MageButton")
if mage ~= nil and (mage.ButtonComponent ~= nil or mage:AddComponent("ButtonComponent") ~= nil) then
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
if mage ~= nil and mage.ButtonComponent ~= nil then
if self.MageSelectHandler ~= nil then
mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler)
self.MageSelectHandler = nil
end
self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end)
end
local allDeckClose = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:AddComponent("ButtonComponent") ~= nil) then
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
if self.AllDeckCloseHandler ~= nil then
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
self.AllDeckCloseHandler = nil
@@ -94,16 +86,16 @@ if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:
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
local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton")
if start ~= nil and start.ButtonComponent ~= nil then
if self.StartGameHandler ~= nil then
start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler)
self.StartGameHandler = nil
end
self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)
end
local charBack = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/BackButton")
if charBack ~= nil and (charBack.ButtonComponent ~= nil or charBack:AddComponent("ButtonComponent") ~= nil) then
local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton")
if charBack ~= nil and charBack.ButtonComponent ~= nil then
if self.CharBackHandler ~= nil then
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
self.CharBackHandler = nil
@@ -111,7 +103,7 @@ if charBack ~= nil and (charBack.ButtonComponent ~= nil or charBack:AddComponent
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 ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then
if self.AscMinusHandler ~= nil then
ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler)
self.AscMinusHandler = nil
@@ -119,7 +111,7 @@ if ascMinus ~= nil and (ascMinus.ButtonComponent ~= nil or ascMinus:AddComponent
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 ascPlus ~= nil and ascPlus.ButtonComponent ~= nil then
if self.AscPlusHandler ~= nil then
ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler)
self.AscPlusHandler = nil
@@ -130,45 +122,72 @@ end`),
self:RenderAscension()
self:RenderSoulLabel()
self:ShowState("lobby")
self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false)
self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/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('GoLobbyMap', `self.LobbyTpTries = 0
local eventId = 0
local function go()
self.LobbyTpTries = self.LobbyTpTries + 1
local lp = _UserService.LocalPlayer
if lp ~= nil then
if lp.CurrentMapName ~= "${LOBBY_MAP}" then
_TeleportService:TeleportToMapPosition(lp, ${LOBBY_SPAWN}, "${LOBBY_MAP}")
end
_TimerService:ClearTimer(eventId)
elseif self.LobbyTpTries > 50 then
_TimerService:ClearTimer(eventId)
end
end
eventId = _TimerService:SetTimerRepeat(go, 0.1)`),
method('OnLobbyNpcInteract', `if self.RunActive == true then
return
end
if id == "run" then
self:ShowCharacterSelect()
elseif id == "codex" then
self:ShowCodex()
elseif id == "shop" then
self:ShowSoulShop()
elseif id == "board" then
self:ShowBoard()
end`, [{ Type: 'string', DefaultValue: '""', SyncDirection: 0, Attributes: [], Name: 'id' }]),
method('RenderSoulLabel', `local s = self.SoulPoints or 0
self:SetText("/ui/DefaultGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", s))
self:SetText("/ui/DefaultGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", s))`),
method('BindLobbyButtons', `if self.LobbyBound == true then
return
end
self.LobbyBound = true
local function bindClick(path, handler)
local entity = _EntityService:GetEntityByPath(path)
if entity ~= nil and (entity.ButtonComponent ~= nil or entity:AddComponent("ButtonComponent") ~= nil) then
entity:ConnectEvent(ButtonClickEvent, handler)
local function bindClick(path, fn)
local e = _EntityService:GetEntityByPath(path)
if e ~= nil and e.ButtonComponent ~= nil then
e:ConnectEvent(ButtonClickEvent, fn)
end
end
bindClick("/ui/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)`),
bindClick("/ui/DefaultGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end)
bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end)
bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end)
bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`),
method('ShowCodex', `self.CodexMode = true
self.ClassDeckMode = true
local close = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")
if close ~= nil and (close.ButtonComponent ~= nil or close:AddComponent("ButtonComponent") ~= nil) then
local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
if close ~= nil and close.ButtonComponent ~= nil then
if self.AllDeckCloseHandler ~= nil then
close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
end
self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
end
self:BindClassDeckTabs()
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
self:SetClassDeckTab("warrior")
local hud = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/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)`),
method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`),
method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`),
];

View File

@@ -3,47 +3,6 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
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
@@ -105,14 +64,11 @@ return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [
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 e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
local tx = 0
if e ~= nil and e.UITransformComponent ~= nil then
tx = e.UITransformComponent.anchoredPosition.x
@@ -131,7 +87,7 @@ if c ~= nil then
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))
method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
end
@@ -141,9 +97,9 @@ self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, At
{ 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")
method('ShowTooltipAt', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name)
self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc)
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox")
if e ~= nil then
if e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(x, y)
@@ -155,5 +111,5 @@ end`, [
{ 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)`),
method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`),
];

View File

@@ -1,12 +1,9 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { POTIONS } from './lib/data.mjs';
import { prop, codeblock, RUN_LENGTH } from './lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs';
import { prop, method, codeblock, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from './lib/codeblock.mjs';
import { bootMethods } from './cb/boot.mjs';
import { screensMethods } from './cb/screens.mjs';
import { npcMethods } from './cb/npc.mjs';
import { navigationMethods } from './cb/navigation.mjs';
import { layoutMethods } from './cb/layout.mjs';
import { stateMethods } from './cb/state.mjs';
import { soulMethods } from './cb/soul.mjs';
import { charSelectMethods } from './cb/charselect.mjs';
import { runMethods } from './cb/run.mjs';
@@ -22,7 +19,243 @@ import { itemMethods } from './cb/items.mjs';
import { tooltipMethods } from './cb/tooltip.mjs';
import { mapMethods } from './cb/map.mjs';
import { shopMethods } from './cb/shop.mjs';
import { COMMON_FILE } from './lib/ui-helpers.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from './lib/ui-helpers.mjs';
import { buildDeckHud } from './hud/deckhud.mjs';
import { buildDeckInspect } from './hud/deckinspect.mjs';
import { buildDeckAll } from './hud/deckall.mjs';
import { buildCombat } from './hud/combat.mjs';
import { buildReward } from './hud/reward.mjs';
import { buildMap } from './hud/map.mjs';
import { buildShop } from './hud/shop.mjs';
import { buildRest } from './hud/rest.mjs';
import { buildTreasure } from './hud/treasure.mjs';
import { buildJobChoice } from './hud/jobchoice.mjs';
import { buildJobSelect } from './hud/jobselect.mjs';
import { buildLobby } from './hud/lobby.mjs';
import { buildBoard } from './hud/board.mjs';
import { buildSoulShop } from './hud/soulshop.mjs';
import { buildMainMenu } from './hud/mainmenu.mjs';
import { buildCharSelect } from './hud/charselect.mjs';
function upsertUi() {
const ui = JSON.parse(readFileSync(UI_FILE, 'utf8'));
const E = ui.ContentProto.Entities;
// CardHand는 스톡 섹션이라 과거 생성된 단색판(NamePlate/CostPlate)이 잔존 → 프레임 이미지 도입으로 제거
const obsoletePlate = /^\/ui\/DefaultGroup\/CardHand\/Card\d+\/(NamePlate|CostPlate)$/;
ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e) && !obsoletePlate.test(e.path));
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
const uiSections = new Map();
const emit = (section, entities) => {
if (uiSections.has(section)) {
throw new Error(`[gen-slaydeck] duplicate generated UI section: ${section}`);
}
uiSections.set(section, entities);
};
for (const path of DISABLED_STOCK_CONTROLS.map((name) => uiPath(name))) {
const e = byPath.get(path);
if (e != null) {
e.jsonString.enable = false;
e.jsonString.visible = false;
for (const component of e.jsonString['@components'] || []) {
component.Enable = false;
if (component.RaycastTarget != null) component.RaycastTarget = false;
}
}
}
// 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시
const previewIds = Object.keys(CARDS.cards);
const cards = Array.from({ length: 10 }, (_, i) => {
const c = CARDS.cards[previewIds[i % previewIds.length]];
return { name: c.name, cost: String(c.cost), desc: c.desc, frame: frameRuid(c) };
});
// 손패 슬롯 10개 (최대 손패 한도). Card1~5는 기존 엔티티, Card6~10은 신규 생성.
for (let i = 1; i <= 10; i++) {
const cardPath = `/ui/DefaultGroup/CardHand/Card${i}`;
let card = byPath.get(cardPath);
if (!card) {
card = entity({
id: guid('dck', 500 + i),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent',
displayOrder: 4,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: 0, y: 0 } }),
sprite({ color: WHITE, type: 0, raycast: true }),
button(),
],
});
ui.ContentProto.Entities.push(card);
byPath.set(cardPath, card);
}
const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent');
const sx = -680 + (i - 1) * 150;
tr.RectSize = { x: CARD_W, y: CARD_H };
tr.anchoredPosition = { x: sx, y: 0 };
tr.OffsetMin = { x: sx - CARD_W / 2, y: -CARD_H / 2 };
tr.OffsetMax = { x: sx + CARD_W / 2, y: CARD_H / 2 };
sp.ImageRUID = { DataId: cards[i - 1].frame };
sp.Type = 0;
sp.Color = WHITE;
sp.RaycastTarget = true;
const comps = card.jsonString['@components'];
if (!comps.some((c) => c['@type'] === 'MOD.Core.ButtonComponent')) {
comps.push(button());
}
if (!card.componentNames.includes('MOD.Core.ButtonComponent')) {
card.componentNames += ',MOD.Core.ButtonComponent';
}
if (!comps.some((c) => c['@type'] === 'MOD.Core.UITouchReceiveComponent')) {
comps.push({ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true });
}
if (!card.componentNames.includes('MOD.Core.UITouchReceiveComponent')) {
card.componentNames += ',MOD.Core.UITouchReceiveComponent';
}
card.jsonString.enable = true;
card.jsonString.visible = true;
const handLayout = cardFaceLayout(CARD_W);
const previewValues = { Cost: cards[i - 1].cost, Name: cards[i - 1].name, Desc: cards[i - 1].desc };
const children = handLayout.texts.map(([suffix, cfg]) => [suffix, { ...cfg, value: previewValues[suffix] }]);
for (const [suffix, cfg] of children) {
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
let child = byPath.get(path);
if (!child) {
child = entity({
id: guid('dck', i * 10 + children.findIndex(([s]) => s === suffix)),
path,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: dOrder,
components: [
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
],
});
ui.ContentProto.Entities.push(child);
byPath.set(path, child);
} else {
child.id = guid('dck', i * 10 + children.findIndex(([s]) => s === suffix));
child.jsonString.enable = true;
child.jsonString.visible = true;
child.jsonString.displayOrder = dOrder;
const ctr = child.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
if (ctr) {
const pivot = { x: 0.5, y: 0.5 };
ctr.RectSize = cfg.size;
ctr.anchoredPosition = cfg.pos;
ctr.OffsetMin = { x: cfg.pos.x - pivot.x * cfg.size.x, y: cfg.pos.y - pivot.y * cfg.size.y };
ctr.OffsetMax = { x: cfg.pos.x + (1 - pivot.x) * cfg.size.x, y: cfg.pos.y + (1 - pivot.y) * cfg.size.y };
}
child.jsonString['@components'][2].Text = cfg.value;
child.jsonString['@components'][2].FontSize = cfg.fontSize;
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
child.jsonString['@components'][2].FontColor = cfg.color;
child.jsonString['@components'][2].Bold = cfg.bold;
child.jsonString['@components'][2].DropShadow = cfg.dropShadow === true;
child.jsonString['@components'][2].DropShadowDistance = cfg.dropShadow === true ? 18 : 32;
child.jsonString['@components'][2].OutlineWidth = cfg.outlineWidth || 1;
}
}
// 프레임 이미지가 이름판·코스트판을 내장하므로 Art만 유지 (잔존 NamePlate/CostPlate는 upsertUi 초입에서 제거)
const frameKids = [
['Art', 5, handLayout.art, WHITE, 0],
];
for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) {
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
let fe = byPath.get(fPath);
if (!fe) {
fe = entity({
id: guid('dck', 200 + i * 10 + dOrder),
path: fPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: dOrder,
components: [
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color, type: spriteType, raycast: false }),
],
});
ui.ContentProto.Entities.push(fe);
byPath.set(fPath, fe);
} else {
const ftr = fe.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
if (ftr) {
ftr.RectSize = cfg.size;
ftr.anchoredPosition = cfg.pos;
ftr.OffsetMin = { x: cfg.pos.x - cfg.size.x / 2, y: cfg.pos.y - cfg.size.y / 2 };
ftr.OffsetMax = { x: cfg.pos.x + cfg.size.x / 2, y: cfg.pos.y + cfg.size.y / 2 };
}
}
}
}
emit('DeckHud', buildDeckHud());
emit('DeckInspectHud', buildDeckInspect());
emit('DeckAllHud', buildDeckAll());
emit('CombatHud', buildCombat());
emit('RewardHud', buildReward());
emit('MapHud', buildMap());
emit('ShopHud', buildShop());
emit('RestHud', buildRest());
// 유물 방 — 보물 상자 (P8)
emit('TreasureHud', buildTreasure());
// 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직
emit('JobChoiceHud', buildJobChoice());
emit('JobSelectHud', buildJobSelect());
emit('MainMenu', buildMainMenu());
emit('CharacterSelectHud', buildCharSelect());
// ── LobbyHud — 반복 런의 허브. NPC 클릭으로 런시작/도감/영혼상점/게시판 ──
emit('LobbyHud', buildLobby());
// ── BoardHud — 게시판(공지/팁) ──
emit('BoardHud', buildBoard());
// ── SoulShopHud — 영혼 메타 상점 (Phase 9에서 해금 항목·구매 로직 채움) ──
emit('SoulShopHud', buildSoulShop());
for (const section of UI_APPEND_ORDER) {
const entities = uiSections.get(section);
if (entities == null) {
throw new Error(`[gen-slaydeck] missing generated UI section: ${section}`);
}
appendUiSection(ui, section, entities);
}
// 엔티티 id 유일성 검증 — 같은 id가 다른 path에 재배정되면 메이커 refresh 병합이 꼬임
const seenIds = new Map();
for (const e of ui.ContentProto.Entities) {
const prev = seenIds.get(e.id);
if (prev != null) throw new Error(`[gen-slaydeck] 엔티티 id 중복: ${e.id} (${prev}${e.path})`);
seenIds.set(e.id, e.path);
}
JSON.parse(JSON.stringify(ui));
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
}
function writeCodeblocks() {
const combat = codeblock('SlayDeckController', 'SlayDeckController', [
@@ -61,14 +294,10 @@ function writeCodeblocks() {
prop('any', 'AllDeckCloseHandler'),
prop('number', 'SoulPoints', '0'),
prop('boolean', 'LobbyBound', 'false'),
prop('boolean', 'ButtonsBound', 'false'),
prop('number', 'LobbyTpTries', '0'),
prop('boolean', 'CodexMode', 'false'),
prop('any', 'CodexCards'),
prop('boolean', 'ClassDeckMode', 'false'),
prop('boolean', 'DebugCardPickerMode', 'false'),
prop('boolean', 'DebugCtrlDown', 'false'),
prop('boolean', 'DebugShiftDown', 'false'),
prop('any', 'ClassDeckCards'),
prop('string', 'ClassDeckTitle', '""'),
prop('string', 'ClassDeckClass', '""'),
@@ -80,12 +309,10 @@ function writeCodeblocks() {
prop('any', 'Cards'),
prop('any', 'CardFrames'),
prop('any', 'NodeIcons'),
prop('any', 'ClassPortraits'),
prop('any', 'ClassToFrame'),
prop('number', 'PlayerHp', '0'),
prop('number', 'PlayerMaxHp', '80'),
prop('number', 'PlayerBlock', '0'),
prop('number', 'BlockGainMultiplier', '1'),
prop('number', 'PlayerDex', '0'),
prop('number', 'PlayerThorns', '0'),
prop('boolean', 'CombatOver', 'false'),
@@ -117,34 +344,13 @@ function writeCodeblocks() {
prop('number', 'PlayerStr', '0'),
prop('number', 'PlayerWeak', '0'),
prop('number', 'PlayerVuln', '0'),
prop('number', 'PlayerIntangible', '0'),
prop('any', 'PlayerPowers'),
prop('any', 'Potions'),
prop('any', 'RunPotions'),
prop('number', 'PotionSlots', String(POTIONS.baseSlots)),
prop('string', 'ShopPotion', '""'),
prop('boolean', 'ShopPotionBought', 'false'),
prop('number', 'CardsDrawnThisCombat', '0'),
prop('boolean', 'HandCostZeroThisTurn', 'false'),
prop('boolean', 'DrawDisabledThisTurn', 'false'),
prop('number', 'SkillCostReductionThisTurn', '0'),
prop('any', 'SkillSlyOnPlayCards'),
prop('any', 'TurnSkillSlyCards'),
prop('boolean', 'ShivFirstDamageBonusUsed', 'false'),
prop('any', 'CombatCardCostReduction'),
prop('number', 'ActiveAttackDamageVsWeakMultiplier', '1'),
prop('number', 'DrawDamageThisTurn', '0'),
prop('number', 'DrawPoisonThisTurn', '0'),
prop('boolean', 'ShivAoeThisCombat', 'false'),
prop('number', 'PoisonApplicationsThisCombat', '0'),
prop('number', 'EnemyStrengthLossThisTurn', '0'),
prop('number', 'ActiveKillReward', '0'),
prop('number', 'BonusRewardScreens', '0'),
prop('number', 'FightAttackCount', '0'),
prop('number', 'TurnAttackCardsPlayed', '0'),
prop('number', 'TurnCardsPlayedThisTurn', '0'),
prop('number', 'DamageDealtThisTurn', '0'),
prop('number', 'TurnDiscardedCards', '0'),
prop('boolean', 'FirstHpLossDone', 'false'),
prop('number', 'ClayBlockNext', '0'),
prop('number', 'PotionMenuSlot', '0'),
@@ -156,25 +362,9 @@ function writeCodeblocks() {
prop('number', 'DiscardSelectTotal', '0'),
prop('number', 'DiscardPostShiv', '0'),
prop('number', 'DiscardShivPerPick', '0'),
prop('number', 'DiscardPostDraw', '0'),
prop('number', 'DiscardDrawPerPick', '0'),
prop('boolean', 'RetainSelectActive', 'false'),
prop('boolean', 'ReserveSelectActive', 'false'),
prop('number', 'NextTurnBlock', '0'),
prop('number', 'NextTurnDraw', '0'),
prop('boolean', 'NextTurnKeepBlock', 'false'),
prop('number', 'NextTurnAttackMultiplier', '1'),
prop('number', 'TurnAttackMultiplier', '1'),
prop('string', 'NextTurnSelectPrompt', '""'),
prop('number', 'NextTurnSelectCopies', '0'),
prop('boolean', 'NextSkillCostZero', 'false'),
prop('number', 'NextSkillRepeatCount', '0'),
prop('any', 'NextTurnAddCards'),
], [
...bootMethods,
...screensMethods,
...npcMethods,
...navigationMethods,
...stateMethods,
...soulMethods,
...charSelectMethods,
...runMethods,
@@ -185,7 +375,6 @@ function writeCodeblocks() {
...jobMethods,
...runEndMethods,
...renderMethods,
...layoutMethods,
...rewardMethods,
...itemMethods,
...tooltipMethods,
@@ -209,7 +398,8 @@ function patchCommon() {
writeFileSync(COMMON_FILE, JSON.stringify(common, null, 2), 'utf8');
}
upsertUi();
writeCodeblocks();
patchCommon();
console.log('SlayDeckController/common 생성 완료 (UI는 메이커 저작 — 생성기 미접근).');
console.log('Slay deck UI and combat codeblocks generated.');

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 = [];

View File

@@ -0,0 +1,171 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
export function buildCharSelect() {
const select = [];
select.push(entity({
id: guid('menu', 100),
path: '/ui/DefaultGroup/CharacterSelectHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 21,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }),
],
}));
select.push(entity({
id: guid('menu', 190),
path: '/ui/DefaultGroup/CharacterSelectHud/OpaqueBackdrop',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
],
}));
select.push(entity({
id: guid('menu', 101),
path: '/ui/DefaultGroup/CharacterSelectHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 72 }, pos: { x: 0, y: 355 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT }),
text({ value: '\uCE90\uB9AD\uD130 \uC120\uD0DD', fontSize: 42, bold: true, color: GOLD, alignment: 0 }),
],
}));
select.push(entity({
id: guid('menu', 102),
path: '/ui/DefaultGroup/CharacterSelectHud/Status',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 680, y: 44 }, pos: { x: 0, y: 298 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT }),
text({ value: '\uC804\uC0AC\uB97C \uC120\uD0DD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694', fontSize: 22, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }),
],
}));
const classCards = [
{ key: 'Warrior', classId: 'warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } },
{ key: 'Thief', classId: 'bandit', label: '\uB3C4\uC801', desc: '\uB3C5\u00B7\uB2E8\uAC80\u00B7\uB4DC\uB85C\uC6B0', x: 0, enabled: true, tint: { r: 0.26, g: 0.5, b: 0.34, a: 1 } },
{ key: 'Mage', classId: 'magician', label: '\uB9C8\uBC95\uC0AC', desc: '\uB9C8\uBC95 \uC6D0\uAC70\uB9AC \uB51C\uB7EC', x: 360, enabled: true, tint: { r: 0.3, g: 0.4, b: 0.75, a: 1 } },
];
for (let i = 0; i < classCards.length; i++) {
const cls = classCards[i];
const base = `/ui/DefaultGroup/CharacterSelectHud/${cls.key}Button`;
select.push(entity({
id: guid('menu', 110 + i),
path: base,
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: 10 + i,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 270, y: 330 }, pos: { x: cls.x, y: 40 }, align: ALIGN_CENTER }),
sprite({ color: cls.enabled ? { r: 0.16, g: 0.2, b: 0.26, a: 1 } : { r: 0.11, g: 0.12, b: 0.14, a: 1 }, type: 1, raycast: cls.enabled }),
button({ enabled: cls.enabled }),
],
}));
select.push(entity({
id: guid('menu', 200 + i),
path: `${base}/Art`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 318 }, pos: { x: 0, y: 0 } }),
sprite({ dataId: CHARS.portraits[cls.classId], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }),
],
}));
select.push(entity({
id: guid('menu', 210 + i),
path: `${base}/NameBanner`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 1,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 60 }, pos: { x: 0, y: -137 } }),
sprite({ color: { r: 0, g: 0, b: 0, a: 0.55 }, type: 1, raycast: false }),
],
}));
select.push(entity({
id: guid('menu', 120 + i),
path: `${base}/Name`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 54 }, pos: { x: 0, y: -137 } }),
sprite({ color: TRANSPARENT }),
text({ value: cls.label, fontSize: 34, bold: true, color: cls.enabled ? GOLD : { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 4 }),
],
}));
if (!cls.enabled) {
select.push(entity({
id: guid('menu', 150 + i),
path: `${base}/LockBody`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 3,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 76, y: 58 }, pos: { x: 0, y: 4 } }),
sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }),
],
}));
select.push(entity({
id: guid('menu', 160 + i),
path: `${base}/LockShackle`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 4,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 54, y: 42 }, pos: { x: 0, y: 48 } }),
sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }),
],
}));
}
}
select.push(entity({
id: guid('menu', 180),
path: '/ui/DefaultGroup/CharacterSelectHud/StartButton',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 20,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 68 }, pos: { x: 720, y: -360 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }),
button(),
text({ value: '\uC2DC\uC791', fontSize: 30, bold: true, color: GOLD, alignment: 0 }),
],
}));
select.push(entity({
id: guid('menu', 230),
path: '/ui/DefaultGroup/CharacterSelectHud/BackButton',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 22,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 56 }, pos: { x: -800, y: 430 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }),
button(),
text({ value: '\u2190 \uB4A4\uB85C', fontSize: 26, bold: true, color: GOLD, alignment: 0 }),
],
}));
select[0].jsonString.enable = false;
return select;
}

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 };

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 = [];
@@ -116,12 +116,11 @@ export function buildDeckAll() {
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: i,
components: [
transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }),
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }),
button(),
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
],
});
card.jsonString.enable = false;

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildJobSelect() {
const jobSelect = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildLobby() {
const lobby = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildMainMenu() {
const menu = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildMap() {
const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스', shop: '상점', rest: '휴식' };

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildRest() {
const rest = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildReward() {
const reward = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildShop() {
const shop = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildSoulShop() {
const soulShop = [];

View File

@@ -1,5 +1,5 @@
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../../lib/ui-helpers.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../../lib/data.mjs';
import { 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 buildTreasure() {
const treasure = [];

View File

@@ -1,246 +0,0 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { CARDS, frameRuid } from '../lib/data.mjs';
import { UI_FILE, isGeneratedUiEntity, DISABLED_STOCK_CONTROLS, uiPath, guid, entity, transform, sprite, button, text, WHITE, TRANSPARENT, CARD_W, CARD_H, cardFaceLayout, UI_APPEND_ORDER, appendUiSection } from '../lib/ui-helpers.mjs';
import { buildDeckHud } from './hud/deckhud.mjs';
import { buildDeckInspect } from './hud/deckinspect.mjs';
import { buildDeckAll } from './hud/deckall.mjs';
import { buildCombat } from './hud/combat.mjs';
import { buildReward } from './hud/reward.mjs';
import { buildMap } from './hud/map.mjs';
import { buildShop } from './hud/shop.mjs';
import { buildRest } from './hud/rest.mjs';
import { buildTreasure } from './hud/treasure.mjs';
import { buildJobChoice } from './hud/jobchoice.mjs';
import { buildJobSelect } from './hud/jobselect.mjs';
import { buildLobby } from './hud/lobby.mjs';
import { buildBoard } from './hud/board.mjs';
import { buildSoulShop } from './hud/soulshop.mjs';
import { buildMainMenu } from './hud/mainmenu.mjs';
// ⚠️ 휴면(LEGACY): 메이커 저작 전환 후 생성기는 .ui를 안 만든다. 이 파일은 옛 DefaultGroup.ui
// 단일 저작 로직의 롤백/참조용. import는 무해(함수만 정의), 직접 실행할 때만 .ui를 옛 생성본으로 덮어쓴다.
function upsertUi() {
const ui = JSON.parse(readFileSync(UI_FILE, 'utf8'));
const E = ui.ContentProto.Entities;
// CardHand는 스톡 섹션이라 과거 생성된 단색판(NamePlate/CostPlate)이 잔존 → 프레임 이미지 도입으로 제거
const obsoletePlate = /^\/ui\/DefaultGroup\/CardHand\/Card\d+\/(NamePlate|CostPlate)$/;
ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e) && !obsoletePlate.test(e.path));
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
const uiSections = new Map();
const emit = (section, entities) => {
if (uiSections.has(section)) {
throw new Error(`[gen-slaydeck] duplicate generated UI section: ${section}`);
}
uiSections.set(section, entities);
};
for (const path of DISABLED_STOCK_CONTROLS.map((name) => uiPath(name))) {
const e = byPath.get(path);
if (e != null) {
e.jsonString.enable = false;
e.jsonString.visible = false;
for (const component of e.jsonString['@components'] || []) {
component.Enable = false;
if (component.RaycastTarget != null) component.RaycastTarget = false;
}
}
}
// 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시
const previewIds = Object.keys(CARDS.cards);
const cards = Array.from({ length: 10 }, (_, i) => {
const c = CARDS.cards[previewIds[i % previewIds.length]];
return { name: c.name, cost: String(c.cost), desc: c.desc, frame: frameRuid(c) };
});
// 손패 슬롯 10개 (최대 손패 한도). Card1~5는 기존 엔티티, Card6~10은 신규 생성.
for (let i = 1; i <= 10; i++) {
const cardPath = `/ui/DefaultGroup/CardHand/Card${i}`;
let card = byPath.get(cardPath);
if (!card) {
card = entity({
id: guid('dck', 500 + i),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent',
displayOrder: 4,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: 0, y: 0 } }),
sprite({ color: WHITE, type: 0, raycast: true }),
button(),
],
});
ui.ContentProto.Entities.push(card);
byPath.set(cardPath, card);
}
const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent');
const sx = -680 + (i - 1) * 150;
tr.RectSize = { x: CARD_W, y: CARD_H };
tr.anchoredPosition = { x: sx, y: 0 };
tr.OffsetMin = { x: sx - CARD_W / 2, y: -CARD_H / 2 };
tr.OffsetMax = { x: sx + CARD_W / 2, y: CARD_H / 2 };
sp.ImageRUID = { DataId: cards[i - 1].frame };
sp.Type = 0;
sp.Color = WHITE;
sp.RaycastTarget = true;
const comps = card.jsonString['@components'];
if (!comps.some((c) => c['@type'] === 'MOD.Core.ButtonComponent')) {
comps.push(button());
}
if (!card.componentNames.includes('MOD.Core.ButtonComponent')) {
card.componentNames += ',MOD.Core.ButtonComponent';
}
if (!comps.some((c) => c['@type'] === 'MOD.Core.UITouchReceiveComponent')) {
comps.push({ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true });
}
if (!card.componentNames.includes('MOD.Core.UITouchReceiveComponent')) {
card.componentNames += ',MOD.Core.UITouchReceiveComponent';
}
card.jsonString.enable = true;
card.jsonString.visible = true;
const handLayout = cardFaceLayout(CARD_W);
const previewValues = { Cost: cards[i - 1].cost, Name: cards[i - 1].name, Desc: cards[i - 1].desc };
const children = handLayout.texts.map(([suffix, cfg]) => [suffix, { ...cfg, value: previewValues[suffix] }]);
for (const [suffix, cfg] of children) {
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
let child = byPath.get(path);
if (!child) {
child = entity({
id: guid('dck', i * 10 + children.findIndex(([s]) => s === suffix)),
path,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: dOrder,
components: [
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
],
});
ui.ContentProto.Entities.push(child);
byPath.set(path, child);
} else {
child.id = guid('dck', i * 10 + children.findIndex(([s]) => s === suffix));
child.jsonString.enable = true;
child.jsonString.visible = true;
child.jsonString.displayOrder = dOrder;
const ctr = child.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
if (ctr) {
const pivot = { x: 0.5, y: 0.5 };
ctr.RectSize = cfg.size;
ctr.anchoredPosition = cfg.pos;
ctr.OffsetMin = { x: cfg.pos.x - pivot.x * cfg.size.x, y: cfg.pos.y - pivot.y * cfg.size.y };
ctr.OffsetMax = { x: cfg.pos.x + (1 - pivot.x) * cfg.size.x, y: cfg.pos.y + (1 - pivot.y) * cfg.size.y };
}
child.jsonString['@components'][2].Text = cfg.value;
child.jsonString['@components'][2].FontSize = cfg.fontSize;
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
child.jsonString['@components'][2].FontColor = cfg.color;
child.jsonString['@components'][2].Bold = cfg.bold;
child.jsonString['@components'][2].DropShadow = cfg.dropShadow === true;
child.jsonString['@components'][2].DropShadowDistance = cfg.dropShadow === true ? 18 : 32;
child.jsonString['@components'][2].OutlineWidth = cfg.outlineWidth || 1;
}
}
// 프레임 이미지가 이름판·코스트판을 내장하므로 Art만 유지 (잔존 NamePlate/CostPlate는 upsertUi 초입에서 제거)
const frameKids = [
['Art', 5, handLayout.art, WHITE, 0],
];
for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) {
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
let fe = byPath.get(fPath);
if (!fe) {
fe = entity({
id: guid('dck', 200 + i * 10 + dOrder),
path: fPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: dOrder,
components: [
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color, type: spriteType, raycast: false }),
],
});
ui.ContentProto.Entities.push(fe);
byPath.set(fPath, fe);
} else {
const ftr = fe.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
if (ftr) {
ftr.RectSize = cfg.size;
ftr.anchoredPosition = cfg.pos;
ftr.OffsetMin = { x: cfg.pos.x - cfg.size.x / 2, y: cfg.pos.y - cfg.size.y / 2 };
ftr.OffsetMax = { x: cfg.pos.x + cfg.size.x / 2, y: cfg.pos.y + cfg.size.y / 2 };
}
}
}
}
emit('DeckHud', buildDeckHud());
emit('DeckInspectHud', buildDeckInspect());
emit('DeckAllHud', buildDeckAll());
emit('CombatHud', buildCombat());
emit('RewardHud', buildReward());
emit('MapHud', buildMap());
emit('ShopHud', buildShop());
emit('RestHud', buildRest());
// 유물 방 — 보물 상자 (P8)
emit('TreasureHud', buildTreasure());
// 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직
emit('JobChoiceHud', buildJobChoice());
emit('JobSelectHud', buildJobSelect());
emit('MainMenu', buildMainMenu());
// ── LobbyHud — 반복 런의 허브. NPC 클릭으로 런시작/도감/영혼상점/게시판 ──
emit('LobbyHud', buildLobby());
// ── BoardHud — 게시판(공지/팁) ──
emit('BoardHud', buildBoard());
// ── SoulShopHud — 영혼 메타 상점 (Phase 9에서 해금 항목·구매 로직 채움) ──
emit('SoulShopHud', buildSoulShop());
for (const section of UI_APPEND_ORDER) {
const entities = uiSections.get(section);
if (entities == null) {
throw new Error(`[gen-slaydeck] missing generated UI section: ${section}`);
}
appendUiSection(ui, section, entities);
}
// 엔티티 id 유일성 검증 — 같은 id가 다른 path에 재배정되면 메이커 refresh 병합이 꼬임
const seenIds = new Map();
for (const e of ui.ContentProto.Entities) {
const prev = seenIds.get(e.id);
if (prev != null) throw new Error(`[gen-slaydeck] 엔티티 id 중복: ${e.id} (${prev}${e.path})`);
seenIds.set(e.id, e.path);
}
JSON.parse(JSON.stringify(ui));
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
}
// 롤백/참조용 직접 실행 시에만 동작 (import 시에는 실행 안 함)
if (process.argv[1] === fileURLToPath(import.meta.url)) {
upsertUi();
}

View File

@@ -79,10 +79,6 @@ function luaNodeIconsTable() {
const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n');
return `self.NodeIcons = {\n${rows}\n}`;
}
function luaCharsTable() {
const rows = Object.entries(CHARS.portraits).map(([c, ruid]) => `\t${c} = ${luaStr(ruid)},`).join('\n');
return `self.ClassPortraits = {\n${rows}\n}`;
}
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7)
@@ -158,29 +154,10 @@ function luaCardsTable(cards) {
const lines = Object.entries(cards).map(([id, c]) => {
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
if (c.damage != null) fields.push(`damage = ${c.damage}`);
if (c.damagePerOtherHandCard != null) fields.push(`damagePerOtherHandCard = ${c.damagePerOtherHandCard}`);
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`);
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`);
if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${c.damagePerSkillInHand}`);
if (c.damagePerCardDrawnThisCombat != null) fields.push(`damagePerCardDrawnThisCombat = ${c.damagePerCardDrawnThisCombat}`);
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`);
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
if (c.firstCardDamageBonus != null) fields.push(`firstCardDamageBonus = ${c.firstCardDamageBonus}`);
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`);
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`);
if (c.attackPoison != null) fields.push(`attackPoison = ${c.attackPoison}`);
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`);
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`);
if (c.block != null) fields.push(`block = ${c.block}`);
if (c.blockGainMultiplier != null) fields.push(`blockGainMultiplier = ${c.blockGainMultiplier}`);
if (c.blockPerDamageDealtThisTurn != null) fields.push(`blockPerDamageDealtThisTurn = ${c.blockPerDamageDealtThisTurn}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`);
if (c.dex != null) fields.push(`dex = ${c.dex}`);
if (c.thorns != null) fields.push(`thorns = ${c.thorns}`);
if (c.cardPlayedBlock != null) fields.push(`cardPlayedBlock = ${c.cardPlayedBlock}`);
if (c.weak != null) fields.push(`weak = ${c.weak}`);
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
@@ -192,58 +169,13 @@ function luaCardsTable(cards) {
if (c.pierce === true) fields.push('pierce = true');
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
if (c.draw != null) fields.push(`draw = ${c.draw}`);
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${c.drawSkillBlock}`);
if (c.drawDamage != null) fields.push(`drawDamage = ${c.drawDamage}`);
if (c.drawPoison != null) fields.push(`drawPoison = ${c.drawPoison}`);
if (c.heal != null) fields.push(`heal = ${c.heal}`);
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
if (c.poison != null) fields.push(`poison = ${c.poison}`);
if (c.discard != null) fields.push(`discard = ${c.discard}`);
if (c.discardAll === true) fields.push('discardAll = true');
if (c.drawPerDiscarded != null) fields.push(`drawPerDiscarded = ${c.drawPerDiscarded}`);
if (c.addShiv != null) fields.push(`addShiv = ${c.addShiv}`);
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`);
if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${c.turnStartDraw}`);
if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${c.turnStartDiscard}`);
if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = true');
if (c.drawDisabledThisTurn === true) fields.push('drawDisabledThisTurn = true');
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
if (c.useAllEnergy === true) fields.push('useAllEnergy = true');
if (c.shivDamageBonus != null) fields.push(`shivDamageBonus = ${c.shivDamageBonus}`);
if (c.firstShivDamageBonus != null) fields.push(`firstShivDamageBonus = ${c.firstShivDamageBonus}`);
if (c.shivRetain === true) fields.push('shivRetain = true');
if (c.shivAoe === true) fields.push('shivAoe = true');
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${c.attackDamageVsWeakMultiplier}`);
if (c.poisonHits != null) fields.push(`poisonHits = ${c.poisonHits}`);
if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = true');
if (c.poisonIfTargetPoisoned === true) fields.push('poisonIfTargetPoisoned = true');
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${c.xDamagePerEnergy}`);
if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${c.xWeakPerEnergy}`);
if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${c.nextTurnBlock}`);
if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`);
if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true');
if (c.nextTurnAttackMultiplier != null) fields.push(`nextTurnAttackMultiplier = ${c.nextTurnAttackMultiplier}`);
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${c.nextSkillRepeatCount}`);
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`);
if (c.skillSlyOnPlay === true) fields.push('skillSlyOnPlay = true');
if (c.turnHandSlyCount != null) fields.push(`turnHandSlyCount = ${c.turnHandSlyCount}`);
if (c.combatCostReductionOnPlay != null) fields.push(`combatCostReductionOnPlay = ${c.combatCostReductionOnPlay}`);
if (c.randomTargetEachHit === true) fields.push('randomTargetEachHit = true');
if (c.repeatOnKill === true) fields.push('repeatOnKill = true');
if (c.affectsAllEnemies === true) fields.push('affectsAllEnemies = true');
if (c.removeEnemyBlock === true) fields.push('removeEnemyBlock = true');
if (c.removeEnemyArtifact === true) fields.push('removeEnemyArtifact = true');
if (c.enemyStrengthLossThisTurn != null) fields.push(`enemyStrengthLossThisTurn = ${c.enemyStrengthLossThisTurn}`);
if (c.extraPoisonTicks != null) fields.push(`extraPoisonTicks = ${c.extraPoisonTicks}`);
if (c.poisonApplicationBurstEvery != null) fields.push(`poisonApplicationBurstEvery = ${c.poisonApplicationBurstEvery}`);
if (c.poisonApplicationBurstDamage != null) fields.push(`poisonApplicationBurstDamage = ${c.poisonApplicationBurstDamage}`);
if (c.innate === true) fields.push('innate = true');
if (c.playableWhenDrawPileEmpty === true) fields.push('playableWhenDrawPileEmpty = true');
if (c.sly === true) fields.push('sly = true');
if (c.retain === true) fields.push('retain = true');
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) fields.push('exhaust = true');
@@ -262,4 +194,4 @@ function luaDeckTable(deck) {
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
}
export { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, luaCharsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable };
export { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable };

View File

@@ -14,6 +14,7 @@ const GENERATED_UI_SECTIONS = [
'JobChoiceHud',
'JobSelectHud',
'MainMenu',
'CharacterSelectHud',
'LobbyHud',
'BoardHud',
'SoulShopHud',
@@ -31,6 +32,7 @@ const UI_APPEND_ORDER = [
'DeckInspectHud',
'DeckAllHud',
'MainMenu',
'CharacterSelectHud',
'LobbyHud',
'BoardHud',
'SoulShopHud',

View File

@@ -1,28 +0,0 @@
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
// 일회성·멱등 마이그레이션: cb/*.mjs의 UI 경로 리터럴을 메이커 재편 UIGroup으로 재연결.
// 이미 이동된 경로는 매치 안 됨(멱등). MainMenu·Button_Attack/Jump·UIJoystick(=DefaultGroup 잔류)은 미변경.
// 섹션→UIGroup 매핑은 tools/verify/uimap.mjs 탐색으로 검증된 실제 .ui 분포 기준.
const MOVE = {
CharacterSelectHud: 'SelectUIGroup', JobChoiceHud: 'SelectUIGroup', JobSelectHud: 'SelectUIGroup',
LobbyHud: 'LobbyUIGroup', BoardHud: 'LobbyUIGroup', SoulShopHud: 'LobbyUIGroup',
CombatHud: 'RunUIGroup', DeckHud: 'RunUIGroup', CardHand: 'RunUIGroup', MapHud: 'RunUIGroup',
RewardHud: 'RunUIGroup', ShopHud: 'RunUIGroup', RestHud: 'RunUIGroup', TreasureHud: 'RunUIGroup',
DeckInspectHud: 'DeckUIGroup', DeckAllHud: 'DeckUIGroup',
};
const CB_DIR = 'tools/deck/cb';
let n = 0;
for (const f of readdirSync(CB_DIR).filter((x) => x.endsWith('.mjs'))) {
const p = join(CB_DIR, f);
const before = readFileSync(p, 'utf8');
let s = before;
// 1) 몬스터 슬롯: 그룹+이름 동시 (CombatHud 일반 remap보다 먼저). 슬롯 5→4는 MAX_MONSTERS(=4)가 이미 반영.
s = s.split('/ui/DefaultGroup/CombatHud/MonsterSlot').join('/ui/RunUIGroup/CombatHud/MonsterStatus');
// 2) 섹션별 그룹 접두사 remap
for (const [section, group] of Object.entries(MOVE)) {
s = s.split(`/ui/DefaultGroup/${section}`).join(`/ui/${group}/${section}`);
}
if (s !== before) { writeFileSync(p, s, 'utf8'); n++; console.log(' remapped', f); }
}
console.log(`reconnect-ui-paths: ${n} files updated`);

View File

@@ -1,69 +0,0 @@
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
// cb 컨트롤러가 참조하는 /ui/DefaultGroup/... 경로를, 사용자가 메이커에서 재편한
// 새 UIGroup(.ui) 구조에 대조해 "그대로 옮겨지면 해결(resolved)" vs "이름/구조가 바뀌어
// 못 찾음(GAP)"을 분류. GAP = 사용자에게 구→신 매핑을 받아야 할 항목.
// 출력은 경로 이름 + EXISTS/GAP boolean 뿐(엔티티 값 본문 미출력 → RULES §2 준수).
// 섹션(=DefaultGroup 다음 첫 세그먼트) → 이동된 UIGroup (uimap.mjs 탐색 결과 기반)
const SECTION_TO_GROUP = {
MainMenu: 'DefaultGroup', Button_Attack: 'DefaultGroup', Button_Jump: 'DefaultGroup', UIJoystick: 'DefaultGroup',
CharacterSelectHud: 'SelectUIGroup', JobChoiceHud: 'SelectUIGroup', JobSelectHud: 'SelectUIGroup',
LobbyHud: 'LobbyUIGroup', BoardHud: 'LobbyUIGroup', SoulShopHud: 'LobbyUIGroup',
CombatHud: 'RunUIGroup', DeckHud: 'RunUIGroup', CardHand: 'RunUIGroup', MapHud: 'RunUIGroup',
RewardHud: 'RunUIGroup', ShopHud: 'RunUIGroup', RestHud: 'RunUIGroup', TreasureHud: 'RunUIGroup',
DeckInspectHud: 'DeckUIGroup', DeckAllHud: 'DeckUIGroup',
};
// 새 .ui 로드
const UI_DIR = 'ui';
const ui = {};
for (const f of readdirSync(UI_DIR).filter((x) => x.endsWith('.ui'))) {
ui[f.replace('.ui', '')] = readFileSync(join(UI_DIR, f), 'utf8');
}
// cb 소스에서 /ui/DefaultGroup/... 경로 리터럴 추출 (템플릿 ${...} 포함)
const CB_DIR = 'tools/deck/cb';
const re = /\/ui\/DefaultGroup\/[^"'`\s),]+/g;
const paths = new Set();
for (const f of readdirSync(CB_DIR).filter((x) => x.endsWith('.mjs'))) {
const src = readFileSync(join(CB_DIR, f), 'utf8');
for (const m of src.match(re) || []) paths.add(m);
}
// 동적 세그먼트(${...}) 앞 정적 prefix만 취해 존재검사 (e.g. .../Card${i} → .../Card)
function staticPrefix(p) {
const i = p.indexOf('${');
if (i === -1) return { p, dyn: false };
// ${...} 직전까지 (마지막 세그먼트의 정적 앞부분 포함)
return { p: p.slice(0, i), dyn: true };
}
const bySection = {};
for (const p of [...paths].sort()) {
const rest = p.slice('/ui/DefaultGroup/'.length); // Section/child/...
const section = rest.split('/')[0].split('${')[0];
const group = SECTION_TO_GROUP[section] || '??';
const newPath = group === '??' ? p : p.replace('/ui/DefaultGroup/', `/ui/${group}/`);
const { p: probe } = staticPrefix(newPath);
const blob = ui[group] || '';
const exists = group !== '??' && blob.includes(probe);
(bySection[section] ||= []).push({ old: p, neu: newPath, exists, group });
}
// 보고
let totResolved = 0, totGap = 0;
for (const section of Object.keys(bySection).sort()) {
const rows = bySection[section];
const group = rows[0].group;
const gaps = rows.filter((r) => !r.exists);
const ok = rows.length - gaps.length;
totResolved += ok; totGap += gaps.length;
console.log(`\n[${section}] → ${group} 해결 ${ok} / GAP ${gaps.length} (총 ${rows.length})`);
for (const g of gaps) {
// GAP만 상세 출력 (사용자가 신규 이름 채워야 할 대상)
console.log(` GAP ${g.old.replace('/ui/DefaultGroup/', '')} →(없음) ${g.neu.replace(`/ui/${group}/`, `${group}: `)}`);
}
}
console.log(`\n=== 합계: 자동해결 ${totResolved} / GAP ${totGap} (distinct 경로 ${paths.size}) ===`);

View File

@@ -1,34 +0,0 @@
// cb/*.mjs의 `self.<Field> = ` 대입 ↔ gen-slaydeck.mjs 선언 prop 대조.
// 미선언 prop에 대입하면 MSW 런타임 "cannot set X, no such field" → 그 후보를 정적 검출.
// 사용: node tools/verify/cbprops.mjs
import { readFileSync, readdirSync } from 'node:fs';
const orch = readFileSync('tools/deck/gen-slaydeck.mjs', 'utf8');
const declared = new Set();
for (const m of orch.matchAll(/prop\(\s*'[^']*'\s*,\s*'([^']+)'/g)) declared.add(m[1]);
// MSW 빌트인/설정으로 대입 가능한 self 필드(프롭 아님) — 오탐 제외 화이트리스트.
const BUILTIN = new Set(['Entity']);
const dir = 'tools/deck/cb';
const files = readdirSync(dir).filter((f) => f.endsWith('.mjs'));
const assigns = new Map(); // name -> Set(files)
for (const f of files) {
const src = readFileSync(`${dir}/${f}`, 'utf8');
// self.Name = (단, == / ~= / .Y= / [..]= 는 제외)
for (const m of src.matchAll(/self\.([A-Za-z_]\w*)\s*=(?!=)/g)) {
const name = m[1];
if (!assigns.has(name)) assigns.set(name, new Set());
assigns.get(name).add(f);
}
}
const missing = [...assigns.keys()]
.filter((n) => !declared.has(n) && !BUILTIN.has(n))
.sort();
console.log(`선언 prop: ${declared.size} | 대입된 self.X distinct: ${assigns.size}`);
console.log(`미선언 대입 (no such field 후보): ${missing.length}`);
for (const n of missing) console.log(` - ${n} [${[...assigns.get(n)].join(', ')}]`);
console.log(missing.length ? 'RESULT: MISSING PROPS ABOVE' : 'RESULT: 모든 self 대입이 선언됨 ✓');
process.exit(missing.length ? 1 : 0);

View File

@@ -1,40 +0,0 @@
// 순서 무관 codeblock 메서드 집합 비교. 본문 미출력 — 이름·차이 카운트만.
// 메서드 이동 리팩터의 무손실 검증용: 워킹트리 codeblock vs ref(기본 HEAD).
// 사용: node tools/verify/cbset.mjs [ref]
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
const PATH = 'RootDesk/MyDesk/SlayDeckController.codeblock';
const ref = process.argv[2] || 'HEAD';
function methodsOf(jsonText) {
const obj = JSON.parse(jsonText);
const arr = obj.ContentProto.Json.Methods;
const map = new Map();
for (const m of arr) {
map.set(m.Name, { code: m.Code, exec: m.ExecSpace, params: JSON.stringify(m.Parameters || []) });
}
return map;
}
const work = methodsOf(readFileSync(PATH, 'utf8'));
const base = methodsOf(execSync(`git show ${ref}:${PATH}`, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }));
const onlyWork = [...work.keys()].filter((k) => !base.has(k));
const onlyBase = [...base.keys()].filter((k) => !work.has(k));
const changed = [];
for (const k of work.keys()) {
if (!base.has(k)) continue;
const a = work.get(k);
const b = base.get(k);
if (a.code !== b.code || a.exec !== b.exec || a.params !== b.params) changed.push(k);
}
console.log(`ref=${ref} work=${work.size} base=${base.size}`);
console.log(`only-in-work (${onlyWork.length}): ${onlyWork.join(', ') || '-'}`);
console.log(`only-in-base (${onlyBase.length}): ${onlyBase.join(', ') || '-'}`);
console.log(`body/exec/params changed (${changed.length}): ${changed.join(', ') || '-'}`);
const ok = onlyWork.length === 0 && onlyBase.length === 0 && changed.length === 0;
console.log(ok ? 'RESULT: IDENTICAL SET (무손실)' : 'RESULT: DIFFERENCES ABOVE');
process.exit(ok ? 0 : 1);

View File

@@ -1,35 +0,0 @@
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
// UIGroup(.ui) 매핑 헬퍼 (RULES §2: 내용 출력 금지·카운트만).
// 어떤 섹션/엔티티 이름이 어느 .ui 파일에 들어있는지 카운트 매트릭스로 보고.
// 산출물 경로를 명령줄에 노출하지 않아(디렉토리 스캔) deny 회피.
//
// 사용: node tools/verify/uimap.mjs <pattern> [<pattern> ...]
// 각 pattern은 정규식. 출력은 "pattern | file=count ..." 형식(본문 미출력).
const UI_DIR = 'ui';
const files = readdirSync(UI_DIR).filter((f) => f.endsWith('.ui'));
const cache = {};
for (const f of files) {
cache[f] = readFileSync(join(UI_DIR, f), 'utf8');
}
// 파일별 크기/JSON 유효성 헤더
console.log('=== ui/*.ui ===');
for (const f of files) {
let ok = false;
try { JSON.parse(cache[f]); ok = true; } catch { ok = false; }
console.log(` ${f} bytes=${cache[f].length} jsonValid=${ok}`);
}
const pats = process.argv.slice(2);
if (pats.length === 0) process.exit(0);
console.log('=== matches (file=count, 0 생략) ===');
for (const pat of pats) {
const re = new RegExp(pat, 'g');
const hits = [];
for (const f of files) {
const m = cache[f].match(re);
const n = m ? m.length : 0;
if (n > 0) hits.push(`${f}=${n}`);
}
console.log(` /${pat}/ ${hits.length ? hits.join(' ') : '(none)'}`);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -97,7 +97,7 @@
{
"@type": "MOD.Core.UIGroupComponent",
"DefaultShow": false,
"GroupOrder": 2,
"GroupOrder": 1,
"GroupType": 1,
"Enable": true
},
@@ -585,7 +585,7 @@
{
"id": "94a274e4-4111-40f1-924d-c95a3a1f14d5",
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnOK",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent",
"jsonString": {
"name": "PopupBtnOK",
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnOK",
@@ -719,6 +719,53 @@
"Type": 1,
"Enable": true
},
{
"@type": "MOD.Core.ButtonComponent",
"Colors": {
"NormalColor": {
"r": 1.0,
"g": 1.0,
"b": 1.0,
"a": 1.0
},
"HighlightedColor": {
"r": 0.9607843,
"g": 0.9607843,
"b": 0.9607843,
"a": 1.0
},
"PressedColor": {
"r": 0.784313738,
"g": 0.784313738,
"b": 0.784313738,
"a": 1.0
},
"SelectedColor": {
"r": 0.9607843,
"g": 0.9607843,
"b": 0.9607843,
"a": 1.0
},
"DisabledColor": {
"r": 0.784313738,
"g": 0.784313738,
"b": 0.784313738,
"a": 0.5019608
},
"ColorMultiplier": 1.0,
"FadeDuration": 0.1
},
"ImageRUIDs": {
"HighlightedSprite": null,
"PressedSprite": null,
"SelectedSprite": null,
"DisabledSprite": null
},
"KeyCode": 0,
"OverrideSorting": false,
"Transition": 1,
"Enable": true
},
{
"@type": "MOD.Core.TextComponent",
"Alignment": 4,
@@ -773,7 +820,7 @@
{
"id": "0f5de49b-2adc-409a-816d-15aa43df8e0d",
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnCancel",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent",
"jsonString": {
"name": "PopupBtnCancel",
"path": "/ui/PopupGroup/PopupBack/PopupPanel/PopupBtnCancel",
@@ -907,6 +954,53 @@
"Type": 1,
"Enable": true
},
{
"@type": "MOD.Core.ButtonComponent",
"Colors": {
"NormalColor": {
"r": 1.0,
"g": 1.0,
"b": 1.0,
"a": 1.0
},
"HighlightedColor": {
"r": 0.9607843,
"g": 0.9607843,
"b": 0.9607843,
"a": 1.0
},
"PressedColor": {
"r": 0.784313738,
"g": 0.784313738,
"b": 0.784313738,
"a": 1.0
},
"SelectedColor": {
"r": 0.9607843,
"g": 0.9607843,
"b": 0.9607843,
"a": 1.0
},
"DisabledColor": {
"r": 0.784313738,
"g": 0.784313738,
"b": 0.784313738,
"a": 0.5019608
},
"ColorMultiplier": 1.0,
"FadeDuration": 0.1
},
"ImageRUIDs": {
"HighlightedSprite": null,
"PressedSprite": null,
"SelectedSprite": null,
"DisabledSprite": null
},
"KeyCode": 0,
"OverrideSorting": false,
"Transition": 1,
"Enable": true
},
{
"@type": "MOD.Core.TextComponent",
"Alignment": 4,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -97,7 +97,7 @@
{
"@type": "MOD.Core.UIGroupComponent",
"DefaultShow": false,
"GroupOrder": 3,
"GroupOrder": 2,
"GroupType": 1,
"Enable": true
},