Merge origin/main into exhaust tooltip branch

This commit is contained in:
2026-06-16 01:31:36 +09:00
25 changed files with 1883 additions and 1038 deletions

View File

@@ -2,17 +2,19 @@
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"deny": [
"Read(./ui/DefaultGroup.ui)",
"Read(./ui/*.ui)",
"Read(./map/*.map)",
"Read(./RootDesk/MyDesk/SlayDeckController.codeblock)",
"Edit(./ui/DefaultGroup.ui)",
"Read(./RootDesk/MyDesk/*.codeblock)",
"Edit(./ui/*.ui)",
"Edit(./map/*.map)",
"Edit(./RootDesk/MyDesk/SlayDeckController.codeblock)",
"Edit(./RootDesk/MyDesk/*.codeblock)",
"Edit(./Global/common.gamelogic)",
"Write(./ui/DefaultGroup.ui)",
"Edit(./Global/SectorConfig.config)",
"Write(./ui/*.ui)",
"Write(./map/*.map)",
"Write(./RootDesk/MyDesk/SlayDeckController.codeblock)",
"Write(./Global/common.gamelogic)"
"Write(./RootDesk/MyDesk/*.codeblock)",
"Write(./Global/common.gamelogic)",
"Write(./Global/SectorConfig.config)"
]
}
}

View File

@@ -1,6 +1,6 @@
# SlayMaple — CLAUDE.md
MapleStory Worlds 기반 Slay the Spire 풍 덱빌더. 게임 전체가 데이터(`data/*.json`) + 생성기(`tools/`) 단일 소스이고, `ui/DefaultGroup.ui`(8.3MB)·codeblock·map 파일은 **생성 산출물**이다.
MapleStory Worlds 기반 Slay the Spire 풍 덱빌더. 게임 전체가 데이터(`data/*.json`) + 생성기(`tools/`) 단일 소스이고, `ui/DefaultGroup.ui`(~7.1MB)·codeblock·map 파일은 **생성 산출물**이다.
@RULES.md

View File

@@ -11,13 +11,29 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
| 산출물 (절대 Read/Edit 금지) | 크기 | 단일 소스 (여기만 편집) | 재생성 명령 |
|---|---|---|---|
| `ui/DefaultGroup.ui` | **8.3MB** | `data/*.json` + `tools/deck/gen-slaydeck.mjs` | `node tools/deck/gen-slaydeck.mjs` |
| `RootDesk/MyDesk/SlayDeckController.codeblock` | 132KB | 〃 | 〃 |
| `Global/common.gamelogic` | 1KB | 〃 | 〃 |
| `map/map01.map`~`map11.map` | 각 ~200KB | `tools/map/`·`tools/monster/`·`tools/camera/` | 해당 생성기 |
| `ui/DefaultGroup.ui` | **~7.1MB** | `data/*.json` + `tools/deck/gen-slaydeck.mjs` | `node tools/deck/gen-slaydeck.mjs` |
| `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | 〃 | 〃 |
| `Global/common.gamelogic` | ~1KB | 〃 | 〃 |
| `map/map01.map`~`map05.map`, `map/lobby.map` | 각 ~210KB | `tools/map/`·`tools/monster/`·`tools/camera/`·`tools/player/` (↓ 보조 생성기) | 해당 생성기 |
| `RootDesk/MyDesk/CombatMonster.codeblock` | ~2KB | `tools/monster/gen-combat-monster.mjs` | `node tools/monster/gen-combat-monster.mjs` |
| `RootDesk/MyDesk/PlayerLock.codeblock` | ~2KB | `tools/player/gen-player-lock.mjs` | `node tools/player/gen-player-lock.mjs` |
| `RootDesk/MyDesk/MapCamera.codeblock` | ~2KB | `tools/camera/gen-camera.mjs` (값: `data/camera.json`) | `node tools/camera/gen-camera.mjs` |
| `RootDesk/MyDesk/LobbyNpc.codeblock`·`LobbyMobility.codeblock` | 각 ~2-3KB | `tools/player/gen-lobby-npc.mjs` | `node tools/player/gen-lobby-npc.mjs` |
| `Global/SectorConfig.config` | ~1KB | `tools/map/gen-maps.mjs`·`gen-lobby-map.mjs` (패치) | 해당 생성기 |
- `.claude/settings.json`의 permissions.deny가 위 파일의 Read/Edit/Write 도구 사용을 차단한다 (이 저장소를 열면 자동 적용).
- `.claude/settings.json`의 permissions.deny가 위 파일의 Read/Edit/Write 도구 사용을 차단한다 (이 저장소를 열면 자동 적용). deny는 **glob**`ui/*.ui`·`map/*.map`·`RootDesk/MyDesk/*.codeblock`·`Global/common.gamelogic`·`Global/SectorConfig.config`. 따라서 **메이커 저작 codeblock/UI**(`Monster`·`MonsterAttack`·`PlayerAttack`·`PlayerHit`·`UIPopup`·`UIToast`.codeblock, `ui/PopupGroup.ui`·`ui/ToastGroup.ui`)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발.
- 게임 로직·UI 수정 = **`tools/deck/gen-slaydeck.mjs`(생성기 JS) 또는 `data/*.json`(데이터)을 수정** → 재생성 → 산출물은 통째로 커밋.
- **보조 생성기**(각자 자기 산출물의 단일 소스 — 위 표의 메인 `gen-slaydeck.mjs` 외):
- `tools/camera/gen-camera.mjs``MapCamera.codeblock` + map01~05 카메라 부착 (값 `data/camera.json`)
- `tools/map/gen-maps.mjs``map02~05` + `Global/SectorConfig.config` (map01 템플릿 클론)
- `tools/map/gen-lobby-map.mjs``map/lobby.map` + `SectorConfig.config`
- `tools/map/gen-map-encounters.mjs` → map01~05 노드 타입별 몬스터 그룹 재구성
- `tools/monster/gen-combat-monster.mjs``CombatMonster.codeblock` + map01~05 부착
- `tools/monster/freeze-turn-monsters.mjs` → 몬스터 `.model`·맵 AI 컴포넌트 제거
- `tools/player/gen-player-lock.mjs``PlayerLock.codeblock` + map01~05 부착
- `tools/player/gen-lobby-npc.mjs``LobbyNpc.codeblock`·`LobbyMobility.codeblock`
- `tools/player/freeze-turn-player.mjs``Global/DefaultPlayer.model` 이동 0 고정
- `tools/deck/gen-cardhand.mjs``DefaultGroup.ui` 카드핸드 보조 패처
## 2. 산출물 검증은 카운트로, 내용 출력 금지

View File

@@ -85,7 +85,7 @@
"Name": null
},
"Arguments": [],
"Code": "local monster = self.Entity.Monster\nif not monster then\n\treturn\nend\n\nself.Shape = BoxShape(Vector2.zero, Vector2.one, 0)\n\n-- sprite 사이즈를 가져와 공격 영역으로 사용한다\n_ResourceService:PreloadAsync({self.Entity.SpriteRendererComponent.SpriteRUID}, function()\n\tlocal clip = _ResourceService:LoadAnimationClipAndWait(self.Entity.SpriteRendererComponent.SpriteRUID)\n\tlocal firstFrameSprite = clip.Frames[1].FrameSprite\n\tlocal firstSpriteSizeInPixel = Vector2(firstFrameSprite.Width, firstFrameSprite.Height)\n\tlocal ppu = firstFrameSprite.PixelPerUnit\n\n\tself.SpriteSize = firstSpriteSizeInPixel / ppu\n\tself.PositionOffset = (firstSpriteSizeInPixel / 2 - firstFrameSprite.PivotPixel:ToVector2()) / ppu\n\t\n\t_TimerService:SetTimerRepeat(function() \n\t\tif monster.IsDead == false then\n\t\t\tself:AttackNear()\n\t\tend\n\tend, self.AttackInterval)\nend)",
"Code": "local monster = self.Entity.Monster\nif not monster then\n\treturn\nend\n\nself.Shape = BoxShape(Vector2.zero, Vector2.one, 0)\n\n-- sprite 사이즈를 가져와 공격 영역으로 사용한다\n_ResourceService:PreloadAsync({self.Entity.SpriteRendererComponent.SpriteRUID}, function()\n\tif _ResourceService:GetTypeAndWait(self.Entity.SpriteRendererComponent.SpriteRUID) ~= ResourceType.AnimationClip then\n\t\treturn\n\tend\n\tlocal clip = _ResourceService:LoadAnimationClipAndWait(self.Entity.SpriteRendererComponent.SpriteRUID)\n\tif clip == nil then\n\t\treturn\n\tend\n\tlocal firstFrameSprite = clip.Frames[1].FrameSprite\n\tlocal firstSpriteSizeInPixel = Vector2(firstFrameSprite.Width, firstFrameSprite.Height)\n\tlocal ppu = firstFrameSprite.PixelPerUnit\n\n\tself.SpriteSize = firstSpriteSizeInPixel / ppu\n\tself.PositionOffset = (firstSpriteSizeInPixel / 2 - firstFrameSprite.PivotPixel:ToVector2()) / ppu\n\t\n\t_TimerService:SetTimerRepeat(function() \n\t\tif monster.IsDead == false then\n\t\t\tself:AttackNear()\n\t\tend\n\tend, self.AttackInterval)\nend)",
"Scope": 2,
"ExecSpace": 1,
"Attributes": [],

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://2d659478140c4b1c8f37febbb61bdaa0",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/2d659478140c4b1c8f37febbb61bdaa0/639171361295360405",
"upload_hash": "13E6D3B629261148095059F7C1D8EDC012C7A60422FC769ECB574A7C5A75759E",
"name": "archmage(fire_poison)",
"resource_guid": "2d659478140c4b1c8f37febbb61bdaa0",
"resource_version": "6a3022013e53f03801a4ac58"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://ddd0a729328f452b9ff0802ab1f6f579",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/ddd0a729328f452b9ff0802ab1f6f579/639171361295458274",
"upload_hash": "7B874B7774FA37E570B16E369112EC467EEA5EC1395A32E4DF01FB6165062E67",
"name": "archmage(thun_cold)",
"resource_guid": "ddd0a729328f452b9ff0802ab1f6f579",
"resource_version": "6a3022012c6a274be88a0819"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://efa920e58d31426486ef974106e7dc8b",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/efa920e58d31426486ef974106e7dc8b/639171361295547945",
"upload_hash": "C4B0469A46B70C2356DC8B0F99D36FD9480BFDA5832E251960DD1FF30C201B7F",
"name": "bandit",
"resource_guid": "efa920e58d31426486ef974106e7dc8b",
"resource_version": "6a302201a81bed5f59770e23"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://93e615d645e948f5a76656bfdd9dce15",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/93e615d645e948f5a76656bfdd9dce15/639171361295501823",
"upload_hash": "A5A1263A9E1F5D58B0F985AC58DE7DDE1EA13E0CC5CEE56FC34C3FD792A8D39E",
"name": "bowmaster",
"resource_guid": "93e615d645e948f5a76656bfdd9dce15",
"resource_version": "6a302201a0766b148f66ec2c"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://3c752ffcd4984dcb9f04baab06544f02",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/3c752ffcd4984dcb9f04baab06544f02/639171361295496567",
"upload_hash": "ED1ABC3DBCB04FCBD365BAD0408C8CA1D2458FB337DBDEB4A7EC5DF1BAC32496",
"name": "cleric",
"resource_guid": "3c752ffcd4984dcb9f04baab06544f02",
"resource_version": "6a302201d03493c632770e41"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://e207e6839a4a4bd0aab681bd296a609a",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/e207e6839a4a4bd0aab681bd296a609a/639171361295401732",
"upload_hash": "D5931943781C46611D43595594234BFB133F8BCCAA920C13BCA7E2F8AC9C09D0",
"name": "darkknight",
"resource_guid": "e207e6839a4a4bd0aab681bd296a609a",
"resource_version": "6a302201644d4c175c75d435"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://0efbf37bb7414aea82b257781068372b",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/0efbf37bb7414aea82b257781068372b/639171361295431877",
"upload_hash": "DE22318162E1F93E7B90A0B6A59BE31FC76DEEC122914979915D6D575A4D99B5",
"name": "hero",
"resource_guid": "0efbf37bb7414aea82b257781068372b",
"resource_version": "6a302201c377d9630d82c463"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://fd460e6ee38a40e3b6b05580d00773b6",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/fd460e6ee38a40e3b6b05580d00773b6/639171361295408991",
"upload_hash": "B832DE85217EA6544BA4477F0DBA5D2148E4E392A170944336E0DA74E8D41961",
"name": "hunter",
"resource_guid": "fd460e6ee38a40e3b6b05580d00773b6",
"resource_version": "6a3022013d5de2eb0c7d2a51"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://3b9ea1f066a744bb859df47fef817277",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/3b9ea1f066a744bb859df47fef817277/639171361295599801",
"upload_hash": "770C9D83241F8CE2C0EA8B742887D8351A580E2DE15A6380380653855A974F14",
"name": "mage",
"resource_guid": "3b9ea1f066a744bb859df47fef817277",
"resource_version": "6a302201cc7e89479f12a1ac"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://994c64290e6d4248bd60aba03a595f72",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/994c64290e6d4248bd60aba03a595f72/639171361295422460",
"upload_hash": "A0B7146F6D8E9D72CEACDB7E21A2CD6EEBD4135554D3965B4E1C06BC98FC1211",
"name": "nightlord",
"resource_guid": "994c64290e6d4248bd60aba03a595f72",
"resource_version": "6a30220139613d284615a1e1"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://415d423954764b659574fe829f9aff52",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/415d423954764b659574fe829f9aff52/639171361329450040",
"upload_hash": "EB94BA457C18E04D87964201DD50042695A3C7F93651CBC8ED9509BDDE07F331",
"name": "palladin",
"resource_guid": "415d423954764b659574fe829f9aff52",
"resource_version": "6a302205a0766b148f66ec2d"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://94d6417c55da48e9861964c405991219",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/94d6417c55da48e9861964c405991219/639171361329410075",
"upload_hash": "EBEC4B43CDBB1759C0BC92C051252A7DF84E6B89C9C56ECBFEFF543A0E260889",
"name": "pirate",
"resource_guid": "94d6417c55da48e9861964c405991219",
"resource_version": "6a3022052c6a274be88a081a"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://06c1060586e3457f897f2c596eb5cd71",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/06c1060586e3457f897f2c596eb5cd71/639171361329387468",
"upload_hash": "42EE2E63508762E86391486107A23322E11038070F48851D03FE91255CF41596",
"name": "shadower",
"resource_guid": "06c1060586e3457f897f2c596eb5cd71",
"resource_version": "6a302205a81bed5f59770e24"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://b2e099f2e5334705af122e3f88840ba7",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/b2e099f2e5334705af122e3f88840ba7/639171361329408374",
"upload_hash": "2AB66279D07E64A17CD2AD05BB03F732632805B0076278EC94F51C0227341CC5",
"name": "singung",
"resource_guid": "b2e099f2e5334705af122e3f88840ba7",
"resource_version": "6a302204c377d9630d82c464"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Id": "",
"GameId": "",
"EntryKey": "sprite://28c88fdc5ab44f34a8b3fc1e19d4ce78",
"ContentType": "x-mod/sprite",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/28c88fdc5ab44f34a8b3fc1e19d4ce78/639171361330139251",
"upload_hash": "2929F452FBB26215631886FFB430EE6035D55EB42B1770E880C1B0A34D97BDA0",
"name": "warrior",
"resource_guid": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
"resource_version": "6a30220539613d284615a1e2"
}
}
}

7
data/characters.json Normal file
View File

@@ -0,0 +1,7 @@
{
"portraits": {
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
"magician": "3b9ea1f066a744bb859df47fef817277",
"bandit": "efa920e58d31426486ef974106e7dc8b"
}
}

View File

@@ -0,0 +1,205 @@
# 직업 선택 캐릭터 이미지 + 뒤로가기 — 구현 계획
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans 로 태스크 단위 구현. 단계는 `- [ ]` 체크박스.
**Goal:** CharacterSelectHud의 단색 박스를 캐릭터 이미지 카드로 바꾸고(선택 시 금색 테두리), 뒤로가기 버튼으로 로비 복귀를 추가한다.
**Architecture:** 단일 생성기 `tools/deck/gen-slaydeck.mjs` 수정 + `data/characters.json` 신설(초상화 RUID 단일 소스). 이미지는 생성 시 `sprite({dataId})`로 주입, 선택 표시는 기존 `RenderCharacterSelect`의 Button Color를 금색으로. 뒤로가기는 ShopHud 나가기 패턴 재사용 → `ShowLobby()`. 산출물(ui/codeblock) 재생성.
**Tech Stack:** Node ESM 생성기, MSW Lua codeblock, MSW UI JSON. 검증=카운트+메이커 플레이테스트(이 저장소는 단위테스트 대신 카운트/플레이테스트).
**확정 RUID (메이커 임포트 완료, `.sprite`에서 추출):**
- warrior `28c88fdc5ab44f34a8b3fc1e19d4ce78`
- magician `3b9ea1f066a744bb859df47fef817277`
- bandit `efa920e58d31426486ef974106e7dc8b`
---
### Task 1: `data/characters.json` + 생성기 로드·검증
**Files:**
- Create: `data/characters.json`
- Modify: `tools/deck/gen-slaydeck.mjs:91-96` 인접(NODEICONS 로드 블록 뒤)
- [ ] **Step 1:** `data/characters.json` 작성
```json
{
"portraits": {
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
"magician": "3b9ea1f066a744bb859df47fef817277",
"bandit": "efa920e58d31426486ef974106e7dc8b"
}
}
```
- [ ] **Step 2:** gen-slaydeck.mjs NODEICONS 검증 블록(`:96`) 바로 뒤에 로드+fail-fast 검증 추가
```js
// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
for (const c of ['warrior', 'magician', 'bandit']) {
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
}
```
- [ ] **Step 3:** 생성기 실행해 에러 없는지 확인(아직 UI 미사용이라 출력 동일)
```
node tools/deck/gen-slaydeck.mjs
```
Expected: 성공 메시지 1줄, throw 없음.
---
### Task 2: CharacterSelectHud — 카드 이미지화 (classCards 루프)
**Files:** Modify `tools/deck/gen-slaydeck.mjs:2516-2540` (Portrait/Desc 블록), `:2503-2515` (Name)
카드 본체 `{key}Button`(2490-2502)·DeckButton(2567-2580)·StartButton·click 바인딩 경로는 **불변**. `cls.tint`/`cls.desc`는 더는 안 쓰이나 배열 정의는 그대로 둬도 무방.
- [ ] **Step 1:** `Name`(2503-2515) 위치를 하단으로 — `transform``pos: { x: 0, y: 108 }``pos: { x: 0, y: -137 }`. (displayOrder 0 유지) — 텍스트는 그대로(금색).
- [ ] **Step 2:** `Portrait` 엔티티(2516-2527)를 **`Art` 이미지로 교체**. 경로·guid·sprite 변경:
```js
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 }),
],
}));
```
(258×318, 6px 인셋 → 부모 Button 색이 테두리로 보임. type:0=이미지 풀, raycast off=클릭은 부모 Button으로.)
- [ ] **Step 3:** `Desc` 엔티티(2528-2540) **삭제**(emit 안 함).
- [ ] **Step 4:** `Name` 뒤에 반투명 하단 배너 `NameBanner` 추가(displayOrder 1, Art 위·Name 아래). Name의 displayOrder를 2로 올림.
```js
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 }),
],
}));
```
그리고 Name 엔티티의 `displayOrder: 0``displayOrder: 2`로.
- [ ] **Step 5:** 생성 + 카운트 검증
```
node tools/deck/gen-slaydeck.mjs
node tools/verify/count.mjs ui "CharacterSelectHud/WarriorButton/Art" "CharacterSelectHud/MageButton/Art" "CharacterSelectHud/ThiefButton/Art"
grep -c "28c88fdc5ab44f34a8b3fc1e19d4ce78" ui/DefaultGroup.ui # warrior RUID 1
```
Expected: Art 3개 존재, RUID 등장. (count.mjs 없으면 `grep -c '/Art"' ui/DefaultGroup.ui`.)
---
### Task 3: RenderCharacterSelect — 선택 = 금색 테두리
**Files:** Modify `tools/deck/gen-slaydeck.mjs:3362-3394`
- [ ] **Step 1:** 선택 시 색을 금색으로. 세 군데 `Color(0.28, 0.36, 0.46, 1)``Color(1, 0.82, 0.3, 1)` (미선택 `Color(0.16, 0.2, 0.26, 1)`는 유지). Status 텍스트 로직 불변.
- `gen-slaydeck.mjs`에서 `Color(0.28, 0.36, 0.46, 1)``Color(1, 0.82, 0.3, 1)` 로 (RenderCharacterSelect 내 3회) 치환.
- [ ] **Step 2:** 생성 + 확인
```
node tools/deck/gen-slaydeck.mjs
grep -c "Color(1, 0.82, 0.3, 1)" RootDesk/MyDesk/SlayDeckController.codeblock # 증가 확인(기존 사용처 + 3)
```
---
### Task 4: 뒤로가기 버튼 + 바인딩
**Files:** Modify `tools/deck/gen-slaydeck.mjs` — CharacterSelectHud emit(StartButton 뒤 `:2595` 직후), BindMenuButtons(`:3158` 뒤), prop 선언부
- [ ] **Step 1:** StartButton emit(2582-2595) 직후에 BackButton emit 추가(StartButton 패턴 복제, 좌상단 배치)
```js
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: '← 뒤로', fontSize: 26, bold: true, color: GOLD, alignment: 0 }),
],
}));
```
- [ ] **Step 2:** BindMenuButtons(StartGameHandler 블록 `:3151-3158` 뒤)에 BackButton 바인딩 추가
```lua
local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton")
if charBack ~= nil and charBack.ButtonComponent ~= nil then
if self.CharBackHandler ~= nil then
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
self.CharBackHandler = nil
end
self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
end
```
(이 Lua는 BindMenuButtons 메서드 본문 문자열 끝에 삽입. 실제 탭/`\t` 스타일은 해당 메서드 본문 규칙을 따른다 — BindMenuButtons는 실탭 사용.)
- [ ] **Step 3:** prop `CharBackHandler` 선언 추가. 기존 핸들러 prop 목록(예: `StartGameHandler`/`NewGameHandler``prop('any','...')` 선언부)을 grep으로 찾아 같은 형식으로 `CharBackHandler` 추가.
```
grep -n "StartGameHandler" tools/deck/gen-slaydeck.mjs # prop 선언 위치 확인
```
- [ ] **Step 4:** 생성 + 검증
```
node tools/deck/gen-slaydeck.mjs
node tools/verify/count.mjs ui "CharacterSelectHud/BackButton" # 1
grep -c "CharBackHandler" RootDesk/MyDesk/SlayDeckController.codeblock # ≥2 (선언+바인딩+해제)
```
---
### Task 5: 산출물 재생성 커밋 + .sprite 커밋 + 플레이테스트
**Files:** `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock`(재생성), `RootDesk/MyDesk/*.sprite`(임포트)
- [ ] **Step 1:** 최종 재생성 + git status로 의도 외 변경 없는지 확인
```
node tools/deck/gen-slaydeck.mjs
git status --short
```
Expected: 변경 = gen-slaydeck.mjs, data/characters.json, ui/DefaultGroup.ui, SlayDeckController.codeblock (+ common.gamelogic은 churn이면 내용 동일 시 git checkout 복원). untracked = 임포트 .sprite.
- [ ] **Step 2:** 소스 커밋(생성기+데이터) → 산출물 커밋(재생성 명시) → .sprite 커밋 분리
```
git add tools/deck/gen-slaydeck.mjs data/characters.json
git commit -m "feat(charselect): 직업 카드 캐릭터 이미지 + 뒤로가기 (소스)"
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
git commit -m "chore: 산출물 재생성 (charselect 이미지+뒤로가기)"
git add "RootDesk/MyDesk/warrior.sprite" "RootDesk/MyDesk/mage.sprite" "RootDesk/MyDesk/bandit.sprite"
git commit -m "chore(assets): 캐릭터 초상화 스프라이트 임포트(전사/법사/도적)"
```
(2차전직 아트 12종 .sprite는 별도 — 향후 2차 전직 선택 이미지용. 사용자 의사 확인 후 커밋/보류.)
- [ ] **Step 3:** 메이커 플레이테스트(사용자 워크스페이스 reload 후): 로비 NPC→직업 선택 진입→3 카드에 캐릭터 이미지 표시→클릭 시 금색 테두리·Status 갱신→시작 시 그 직업으로 런→뒤로가기 시 로비 복귀. 빌드 콘솔 0 에러.
- 이미지 비율 왜곡/잘림 보이면 Art size(258×318) 조정.
- 뒤로가기 시 재텔레포트 jolt 보이면 BackButton 바인딩을 `self:ShowState("lobby")`로 축소.
- [ ] **Step 4:** push + PR (`node tools/git/gitea-pr.mjs create <spec.json>`, UTF-8).
---
## Self-Review
- **스펙 커버리지**: 이미지 적용(T1,T2) · 선택→진행 연결(기존 SelectClass/StartNewGame 불변, T2가 클릭경로 보존) · 선택 금색 테두리(T3) · 뒤로가기→로비(T4) · characters.json 단일소스(T1) · 검증/플레이테스트(T5). 누락 없음.
- **플레이스홀더**: RUID·좌표·색·Lua 전부 구체값. count.mjs 부재 시 grep 대체 명시.
- **타입 일관성**: `CHARS.portraits[classId]`(classId=warrior/magician/bandit, classCards.classId와 일치). 핸들러 `CharBackHandler` 일관. Art/NameBanner guid(200+i/210+i/230) 미사용 번호.
- **리스크**: 이미지 비율(T5 Step3 조정), ShowLobby 재텔레포트(T5 Step3 폴백 ShowState), 메이커 reload 필수(산출물 디스크 반영).

View File

@@ -0,0 +1,105 @@
# 직업 선택 — 캐릭터 이미지 + 뒤로가기 설계
작성일: 2026-06-16
브랜치: `feature/charselect-images`
## 목표
런 시작 시 띄우는 **캐릭터(직업) 선택 화면**(`CharacterSelectHud`)을 두 가지로 개선한다:
1. 직업 3종(전사/도적/마법사)을 지금의 **단색 네모 박스** → **각 직업 캐릭터 이미지 카드**로. 이미지를 선택하면 그 직업으로 런 진행(기존 연결 유지).
2. 직업 선택 화면에 **뒤로가기** 버튼 추가 → 로비로 복귀.
요청 원문: "런 시작 시, 직업 선택 창을 뒤로가기도 가능하게 추가. 각 직업별로 지금은 네모 박스인데 각각 이미지(warrior/mage/bandit.png) 추가해서 적용하고, 선택했을 때 그 캐릭터로 진행하도록 연결."
## 확정된 결정 (브레인스토밍)
| 항목 | 결정 |
|---|---|
| 이미지 RUID 확보 | **사용자가 메이커에서 3 PNG 로컬 임포트**`.sprite`+RUID(P13 카드프레임과 동일). MCP/계정 업로드는 흰박스라 불가 |
| 이미지 배치 | **카드 전체를 이미지로**, 이름은 하단 배너, 선택 시 **금색 테두리** |
| 뒤로가기 대상 | **로비로** (`ShowLobby()`) — 로비 NPC에서 진입하므로 |
### 소스 이미지 (사용자 임포트 대상)
- 전사: `C:\Users\jaeoh\Desktop\workspace\source\images\maple\character\warrior.png` (~1.05MB)
- 법사: `…\mage.png` (~1.28MB)
- 도적: `…\bandit.png` (~1.0MB)
세 PNG는 현재 워크스페이스 미임포트(코드 미참조). 기존 `RootDesk/MyDesk/*_normal|unique|legend.sprite`는 P13 **카드 프레임**이지 캐릭터 초상화가 아니다.
## 현재 구조 (조사 결과)
- **CLASSES** 상수 `gen-slaydeck.mjs:7-11``warrior{label,maxHp}`, `bandit`, `magician`.
- **CharacterSelectHud emit** `:2432-2598`. `classCards` 배열 `:2482-2486` (key Warrior/Thief/Mage, classId warrior/bandit/magician, x 360/0/360, tint). 각 카드(270×330)의 자식: `Name`(상단, 108), `Portrait`(142×142 색상 tint, `:2524-2525` 부근), `Desc`(하단 105), `LockBody`/`LockShackle`(비활성 직업용). 별도 `…DeckButton`(덱 보기)·`StartButton`.
- **선택 로직**: 클릭 바인딩 `BindMenuButtons``:3100/3108/3116``SelectClass(classId)` `:3358-3361`(=`self.SelectedClass=…`+`RenderCharacterSelect()`). 시작 `:3151-3157``StartNewGame` `:3395-3399`(미선택 가드 후 `StartRun()`).
- **RenderCharacterSelect** `:3362-3394` — 선택 카드 밝게/미선택 어둡게 + Status 텍스트.
- **진입/전환**: `ShowState` `:3062-3078`가 HUD 토글. 진입 = 로비 NPC `OnLobbyNpcInteract` `:3199-3203`(런 비활성 시 `ShowCharacterSelect()` `:3355-3357`) 및 (사실상 미사용) MainMenu `:3092`. `ShowLobby` `:3175`. 게임은 OnBeginPlay→`ShowLobby`로 부팅(로비 허브).
- **emit 헬퍼**: `entity():466`, `transform():286`, `sprite():311`(`dataId`로 ImageRUID 주입 가능), `button():347`, `text():372`, `guid()`.
- **이미지 외부화 패턴**: 카드프레임은 `data/cardframes.json``luaFramesTable()`(`:72` 부근) → `self.CardFrames` Lua 테이블 + 런타임 `ApplyCardFace` `:4167-4202``e.SpriteGUIRendererComponent.ImageRUID=ruid` 주입. 생성 시 주입은 `sprite({dataId})`.
## 상세 설계
### 1) `data/characters.json` (신설 — 단일 소스)
```json
{
"portraits": {
"warrior": "<32hex RUID>",
"magician": "<32hex RUID>",
"bandit": "<32hex RUID>"
}
}
```
- 사용자 임포트 후 `RootDesk/MyDesk/*.sprite`에서 RUID를 읽어 채운다(파일명은 임포트 시 결정 — `warrior.sprite` 등으로 매칭, 모호하면 사용자 확인).
- 나중에 이미지 교체 = 이 파일 RUID만 바꿔 재생성.
### 2) `gen-slaydeck.mjs` — 로드·검증·주입
- 상단에서 `const CHARS = JSON.parse(readFileSync('data/characters.json','utf8'))` 로드(cardframes 로드 패턴 인접).
- **fail-fast 검증**: `portraits``warrior`/`magician`/`bandit` 3키 존재 + 각 값이 32hex. 누락 시 throw.
- 카드 Art 이미지는 **생성 시 `dataId` 주입**(런타임 테이블 불필요). 즉 `classCards`의 classId로 `CHARS.portraits[classId]`를 조회해 Art 스프라이트 `dataId`에 박는다.
### 3) CharacterSelectHud — 카드 전체 이미지화 (`:2432-2598`, `classCards` emit 루프)
각 직업 카드 구조를 다음으로 변경(엔티티 경로 `…/{key}Button`·클릭 바인딩은 **불변**):
- `{key}Button`(270×330): 클릭 가능한 **테두리 프레임**. sprite Color = 미선택 어둡게(`0.16,0.2,0.26,1`)/선택 금색(`1,0.82,0.3,1`). raycast on, `button()` 유지.
- 신규 자식 `Art`(약 258×318, 6px 인셋, center): `sprite({ dataId: CHARS.portraits[classId], type:1, raycast:false })` — 캐릭터 이미지 풀블리드. (테두리가 이미지 뒤로 6px 보임 → 금색 테두리 효과.)
- `Name`(하단 배너): 반투명 어두운 띠 sprite(예: `0,0,0,0.55`, 270×54, 하단) + 금색 텍스트. 기존 `Name` 재배치.
- **제거**: 기존 색상 `Portrait` 박스, `Desc` 텍스트(선택 레이아웃에 없음).
- `LockBody`/`LockShackle`: 비활성 직업용으로 유지(현재 3직업 모두 enabled라 표시 안 됨).
### 4) `RenderCharacterSelect` Lua 변경 (`:3362-3394`)
- 기존 "박스 밝게/어둡게"를 **테두리(=`{key}Button` sprite Color) 금색/어둡게**로 교체. 선택된 classId의 카드만 `Color(1,0.82,0.3,1)`, 나머지 `Color(0.16,0.2,0.26,1)`.
- Art 이미지는 생성 시 고정 주입이라 런타임 변경 없음. Status 텍스트 로직은 유지.
### 5) 뒤로가기 버튼
- 신규 `CharacterSelectHud/BackButton`(ShopHud `Leave` 패턴 재사용 `:2020-2031`): 좌상단(예: `pos {x:-820,y:430}`, 180×56), text "← 뒤로", DARK sprite + `button()`.
- `BindMenuButtons`에 바인딩 추가(ShopHud Leave 바인딩 패턴 `:3715-3717`): `back:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)`. 핸들러 prop 저장(재바인딩 시 해제).
- `ShowCharacterSelect`/`SelectClass`/`StartNewGame`/`StartRun` 로직 불변.
### 6) GUID 네임스페이스
- 신규 엔티티(Art·NameBanner·BackButton)는 CharacterSelect용 기존 prefix에 번호 추가. 미등록 prefix면 ns 바이트 등록(생성기 끝 id 유일성 검증이 충돌 잡음).
## 흐름
```
로비(맵) ──NPC 상호작용──> ShowCharacterSelect (HUD 오버레이)
카드3=캐릭터 이미지, 클릭 → SelectClass → 금색 테두리
[시작] → StartNewGame(가드) → StartRun (그 직업으로)
[← 뒤로] → ShowLobby() → 로비 HUD 복귀
```
## 미러/테스트 영향
- 전투규칙·맵생성 **미변경**`sim-balance`/`rogue-map` 미러 동기화 불필요.
- 카운트 검증: `CharacterSelectHud/.../Art` ImageRUID 3개, `BackButton` 1개, characters.json 3 RUID 등장(`tools/verify/count.mjs` 또는 `grep -c`).
- 메이커 플레이테스트: 로비 NPC→3 이미지 표시→클릭 금색 테두리→시작 그 직업으로 진행→뒤로 로비 복귀.
## 리스크
- **이미지 임포트 선행 의존**: RUID가 있어야 생성기 실행 가능. 사용자 임포트 완료 후 진행(임포트 무관한 코드 골격은 먼저 작성 가능).
- **이미지 비율**: PNG가 세로 초상화면 258×318(≈0.81 비율)에서 잘리거나 여백 — 임포트 후 스크린샷으로 인셋/사이즈 조정.
- **`ShowLobby()` 재텔레포트**: 이미 로비 맵 위라 `GoLobbyMap` 재호출 시 위치/카메라 jolt 가능 → 보이면 뒤로가기를 `ShowState("lobby")`로 축소(플레이테스트 확인).
- 흰박스: 공식 절차(로컬 임포트)면 렌더됨. reload 필수.
## 변경 파일 요약
| 파일 | 변경 |
|---|---|
| `data/characters.json` | **신설** — 직업 3종 초상화 RUID(단일 소스) |
| `tools/deck/gen-slaydeck.mjs` | characters.json 로드·검증, CharacterSelectHud 카드 이미지화(Art/NameBanner), RenderCharacterSelect 테두리 선택표시, BackButton emit+바인딩 |
| `RootDesk/MyDesk/*.sprite` (×3) | 사용자 임포트 산출물(커밋) |
| `ui/DefaultGroup.ui`·`SlayDeckController.codeblock` | 재생성 산출물 |

View File

@@ -95,6 +95,15 @@ for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) {
}
if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류');
// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
for (const c of ['warrior', 'magician', 'bandit']) {
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
}
// 전투 카메라 고정값(StS2: 플레이어 좌·몬스터 우). KickCombatCamera가 StartCombat에서 재confine에 사용.
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
for (const id of RELICS.relicPool) {
@@ -2500,41 +2509,40 @@ function upsertUi() {
],
}));
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',
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: 230, y: 54 }, pos: { x: 0, y: 108 } }),
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 }),
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', 130 + i),
path: `${base}/Portrait`,
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: 142, y: 142 }, pos: { x: 0, y: 8 } }),
sprite({ color: cls.tint, type: 1 }),
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', 140 + i),
path: `${base}/Desc`,
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: 50 }, pos: { x: 0, y: -105 } }),
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.desc, fontSize: 20, color: cls.enabled ? { r: 0.86, g: 0.9, b: 0.94, a: 1 } : { r: 0.52, g: 0.55, b: 0.59, a: 1 }, alignment: 4 }),
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) {
@@ -2592,6 +2600,20 @@ function upsertUi() {
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;
emit('MainMenu', menu);
emit('CharacterSelectHud', select);
@@ -2901,6 +2923,7 @@ function writeCodeblocks() {
prop('number', 'AscensionLevel', '0'),
prop('number', 'AscensionUnlocked', '0'),
prop('any', 'StartGameHandler'),
prop('any', 'CharBackHandler'),
prop('string', 'SelectedClass', '""'),
prop('any', 'DrawPileHandler'),
prop('any', 'DiscardPileHandler'),
@@ -3157,6 +3180,14 @@ if start ~= nil and start.ButtonComponent ~= nil then
end
self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)
end
local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton")
if charBack ~= nil and charBack.ButtonComponent ~= nil then
if self.CharBackHandler ~= nil then
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
self.CharBackHandler = nil
end
self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
end
local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus")
if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then
if self.AscMinusHandler ~= nil then
@@ -3363,7 +3394,7 @@ self:RenderCharacterSelect()`, [
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(0.28, 0.36, 0.46, 1)
warrior.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
else
warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
end
@@ -3371,7 +3402,7 @@ 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(0.28, 0.36, 0.46, 1)
mage.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
else
mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
end
@@ -3379,7 +3410,7 @@ 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(0.28, 0.36, 0.46, 1)
thief.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1)
else
thief.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
end
@@ -3441,7 +3472,25 @@ self:ApplySoulUnlocks()
self:RenderPotions()
self:TeleportToActMap()
self:ShowMap()`),
method('KickCombatCamera', `local cam = nil
local lp = _UserService.LocalPlayer
if lp ~= nil then cam = lp.CameraComponent end
if cam == nil then cam = _CameraService:GetCurrentCameraComponent() end
if cam ~= nil then cam.ConfineCameraArea = false end
_TimerService:SetTimerOnce(function()
local cc = nil
local lp2 = _UserService.LocalPlayer
if lp2 ~= nil then cc = lp2.CameraComponent end
if cc == nil then cc = _CameraService:GetCurrentCameraComponent() end
if cc ~= nil then
cc.ZoomRatio = ${CAM.zoomRatio}
cc.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY})
cc.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY})
cc.ConfineCameraArea = true
end
end, 0.2)`),
method('StartCombat', `self:ShowState("combat")
self:KickCombatCamera()
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)

File diff suppressed because it is too large Load Diff