12 Commits

Author SHA1 Message Date
8f233296af Merge pull request 'feat: P14 — 반복 런·로비·영혼·도적·몬스터 랜덤성 (요청 17항목)' (#52) from feature/p14-loop-lobby-soul into main
Reviewed-on: #52
2026-06-14 01:50:19 +09:00
5f89d61a8b fix(lobby): 로비 카드 도감용 Cards/Frames 테이블을 OnBeginPlay에 시드
- ShowCodex가 self.Cards를 순회하는데 런 시작 전(로비)엔 미설정이라 pairs(nil) 오류
  → OnBeginPlay에서 luaCardsTable/luaFramesTable을 미리 주입(StartRun과 중복 무해)
- 메이커 플레이테스트로 로비·도감·가로맵·전투(도적/메소아이콘/스킬아이콘) 전 루프 검증

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:28:56 +09:00
8879647b26 feat(soul): 영혼 메타 성장 시스템 (P14-9)
- 2차 전직 상태로 맵 보스 클리어 시 영혼 +1 적립(CheckCombatEnd 보스 분기 AwardSouls)
- UserDataStorage 영속 RPC: ReqLoadSouls/RecvSouls/SaveSouls(ExecSpace 5/6, 승천 패턴 복제)
  · key soulPoints(수치)·soulUnlocks(해금 set, 콤마 직렬화). OnBeginPlay에서 로드
- 로비 영혼 상점(SoulShopHud) 4종 해금: 두둑한 지갑(시작 메소+60)·단련된 육체(시작HP+15)
  ·덱 정제(기본카드 1장 제거)·유물 수집가(시작 유물+1). BuySoulUnlock 구매·영속 저장
- ApplySoulUnlocks가 StartRun에서 해금 효과 적용 → 덱빌딩 이점. 승천(패널티)과 분리 공존
- 산출물 재생성(id 유일성·JSON 검증 통과)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:23:00 +09:00
7ee323ea8b feat(lobby): 로비 허브 + NPC 4종 + 반복 런 루프 (P14-8)
- LobbyHud: 메이플 로비 화면. NPC 4종 클릭 — 모험가(런 시작)·사서(카드 도감)·
  상인(영혼 상점)·안내원(게시판) + 승천 +/- + 영혼 수치 표시
- ShowState "lobby" 추가, OnBeginPlay·EndRun → ShowLobby (첫 실행/패배/클리어 모두
  로비 기점, 반복 런 루프). ShowLobby가 BindMenuButtons도 호출(전직 버튼 바인딩 보존)
- 카드 도감: DeckAllHud 재사용(CodexMode) — 저주 제외 전 카드 표시, 닫으면 로비 복귀
- 게시판(BoardHud): 게임 규칙/팁 패널. 영혼 상점(SoulShopHud): 셸(P9에서 해금 채움)
- guid 네임스페이스 lob/brd/soul 추가(id 충돌 방지). 산출물 재생성(id 유일성 검증 통과)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:16:56 +09:00
7cc311ee91 feat(economy): 메소 표기 완성 + 메소 코인 아이콘 추가 (P14-7)
- relics.json goldIdol 설명 "골드"→"메소" (유일하게 남았던 표면 잔존 정정)
- CombatHud TopBar·ShopHud 메소 금액 옆에 금색 코인 아이콘 스프라이트 추가
  (공식 RUID 흰박스 리스크 회피 위해 색상 채움 스프라이트 사용)
- 내부 식별자 self.Gold 등은 산출물 id 안정성 위해 유지. 산출물 재생성

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:06:59 +09:00
5cde11647f feat(card-ux): 손패 최대 10장·초과 자동버림 + 마우스오버 확대/툴팁 (P14-6)
- 손패 슬롯 5→10 확장(Card6~10 신규 생성), RenderHand 장수 비례 동적 간격 배치
- DrawCards 손패 10장 상한 — 초과 드로는 버린 더미로 자동 이동(StS식, sim 미러)
- 카드 마우스오버: UITouchEnter→UIScale 1.3배 확대 + ShowTooltip(이름/설명),
  Exit→복원+숨김. 드래그 중(DragSlot) 확대 억제. RenderHand가 UIScale 리셋
- HoverCard/UnhoverCard 메서드 신설. sim 35/35 통과. 산출물 재생성

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:04:24 +09:00
8296775e21 feat(cards): 실제 메이플 스킬 아이콘 적용 + 피격 이펙트(fx) 분리 (P14-5)
- 카드 아트(image)를 경로 검증된 실제 스킬 아이콘 RUID로 교체(28종)
  · mapleImgFullPath의 /icon 경로 확인으로 스킬 아이콘 보장(기존엔 이펙트 프레임·맵
    오브젝트가 섞여 있었음)
- 피격 이펙트(fx) 필드 신설(18종) — 스킬 effect/hit 프레임 RUID
  · PlayCard가 PlayAttackFx/PlayAoeFx에 c.fx or c.image 전달(이펙트 분리, 없으면 폴백)
  · luaCardsTable fx 직렬화. MSW asset 메타데이터 경로 검증으로 수급(워크플로 6에이전트)
- 아이콘 미발견 카드는 기존 RUID 유지. sim 35/35 통과. 산출물 재생성

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:57:24 +09:00
d546d62755 feat(thief): 도적 클래스 + 2차 전직(어쌔신/시프) 추가 (P14-4)
- CLASSES.thief(maxHp 75), JOBS.thief = [어쌔신(CriticalThrow), 시프(SavageBlow)]
- 카드 11종: 도적1차(럭키세븐/더블스탭/다크사이트/헤이스트/드레인)
  · 어쌔신(크리티컬스로우/쉐도우스타/클로마스터리)
  · 시프(새비지블로우/스틸/메소가드), starterDecks.thief 추가
- cardframes classToFrame: thief/assassin/bandit → bandit 프레임(기존 RUID 재사용)
- CharacterSelectHud Thief 해금, BindMenuButtons ThiefButton, RenderCharacterSelect·
  StartNewGame·StartRun·JobLabel 도적 분기. 전사/법사 2차는 기존 완비 확인
- 산출물 재생성(생성기 검증 통과)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:38:52 +09:00
8f58a90746 feat(combat): 몬스터 랜덤 구성·랜덤 행동·덱 오염(AddCard)·map01 로스터 (P14-3)
- 구성 랜덤화: BuildMonsters를 그룹별 수집 후 노드 타입별 추첨
  (일반 1~3 / 엘리트 1+일반0~2 / 보스 1, MAX_MONSTERS=4 내)
- 행동 랜덤화: EnemyActStep 순차(round-robin) → 정의된 intent 중 math.random 선택
  (스폰 시 시작 intent도 랜덤, sim-balance.mjs 미러 동기화)
- StS2식 덱 오염: AddCard intent 신규 — 저주 카드(Wound/Burn)를 버린 더미에 추가
  · Wound=사용불가 사석, Burn=사용불가+턴종료 피해2(EndPlayerTurn 처리)
  · PlayCard unplayable 가드, CardPool class 필터로 보상/상점 자동 제외
  · luaIntentsArray(card/count)·luaCardsTable(unplayable/curse/endTurnDamage) 직렬화
- map01 인카운터: 일반 5종(주황/초록/빨강달팽이/파랑/돼지) + 엘리트1 + 보스1, 우측 포메이션
  · enemies.json red_snail/stump 신규, blue_mushroom/mushmom에 AddCard intent
  · gen-map-encounters 레이아웃 맵별 분기 + 풀 인덱싱 일반화
- 막 배율 0.6→0.45(5막 기준 완화). sim 테스트 35/35 통과(신규 3 포함). 산출물 재생성

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:33:49 +09:00
8fa1079548 feat(map-ui): 노드 맵 가로 진행 레이아웃(왼→오른쪽) (P14-2)
- nodeX=행(좌→우)·nodeY=열(분기)로 좌표축 스왑, 보스는 최우측 중앙
- 호출부(노드/도트/보스 간선) 인자 스왑. Lua 무수정(좌표는 JS 빌더 전담)
- 도트 보간·간선·라벨 좌표 함수 파생이라 자동 추종. 산출물 재생성

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:14:24 +09:00
9e16465218 feat(map): 맵 5막화·노드 depth 7·rest/shop/elite 연속 금지 (P14-1)
- ACT_COUNT/RUN_LENGTH 3→5, ACT_MAPS map01~map05 (반복 런 기반 확장)
- MAP_ROWS 7→6 (걷는 행 6 + 보스 = depth 최대 7), 막 배율 0.6→0.45 완화
- 노드 타입 인접 금지를 elite 단독 → rest/shop/elite 3종으로 일반화
  (Lua GenerateMap + rogue-map.mjs JS 미러 동시 수정, 테스트 9/9 통과)
- 맵 파일 생성기 카운트 11→5, map06~map11 삭제, SectorConfig 정리(stale 제거)
- 산출물 재생성(ui/codeblock/map01~05). 검증 헬퍼 tools/verify/count.mjs 추가

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:13:16 +09:00
f67471435e docs(p14): 반복 런·로비·영혼·도적·몬스터 랜덤성 설계 spec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:03:13 +09:00
31 changed files with 20330 additions and 54883 deletions

View File

@@ -23,13 +23,7 @@
"map://map02",
"map://map03",
"map://map04",
"map://map05",
"map://map06",
"map://map07",
"map://map08",
"map://map09",
"map://map10",
"map://map11"
"map://map05"
]
}
],

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,9 @@
},
"classToFrame": {
"warrior": "warrior", "fighter": "warrior", "page": "warrior", "spearman": "warrior",
"magician": "magician", "firepoison": "magician", "icelightning": "magician", "cleric": "magician"
"magician": "magician", "firepoison": "magician", "icelightning": "magician", "cleric": "magician",
"thief": "bandit", "assassin": "bandit", "bandit": "bandit",
"curse": "bandit"
},
"rewardWeights": { "normal": 70, "unique": 25, "legend": 5 }
}

View File

@@ -6,9 +6,10 @@
"kind": "Attack",
"damage": 6,
"desc": "피해 6",
"image": "a71b116807904ef2b38e1dc013e2f9a2",
"image": "e4acdf27d68549db8858d6082169c70c",
"class": "warrior",
"rarity": "normal"
"rarity": "normal",
"fx": "291b2298db88476f8ae3c6c78f53c9b7"
},
"Defend": {
"name": "아이언 바디",
@@ -16,7 +17,7 @@
"kind": "Skill",
"block": 5,
"desc": "방어도 5",
"image": "1ae9b6741c5947a8b528a0f515b50e3e",
"image": "7648c3b8e1ca44fc8ec353561207a670",
"class": "warrior",
"rarity": "normal"
},
@@ -26,9 +27,10 @@
"kind": "Attack",
"damage": 10,
"desc": "피해 10",
"image": "d5bc2953fcab4cfe9062af81c35aff86",
"image": "4cbbe8cfc3e840e4a76379498d8eb012",
"class": "warrior",
"rarity": "normal"
"rarity": "normal",
"fx": "863812c5c2f84132ac7465b50ec2283e"
},
"WarLeap": {
"name": "워 리프",
@@ -49,7 +51,8 @@
"desc": "피해 13",
"image": "21af4bccc5054a5dbc8245dfa7f08681",
"class": "warrior",
"rarity": "unique"
"rarity": "unique",
"fx": "e8a145a6c43d493f9ad50fab03b200aa"
},
"ChargedBlow": {
"name": "차지 블로우",
@@ -102,7 +105,8 @@
"hits": 2,
"desc": "피해 5 × 2회",
"image": "1bc3e52b330648faae9eafd5a205e37b",
"rarity": "unique"
"rarity": "unique",
"fx": "48754be05be344358cddd55aa8fe11f4"
},
"Berserk": {
"name": "버서크",
@@ -113,7 +117,7 @@
"value": 1,
"selfVuln": 1,
"desc": "매턴 에너지 +1, 취약 1 자가",
"image": "cef30ea340c74e768bcee4e2cbe0577a",
"image": "e2580523efc6457385114b78ad0d7cce",
"rarity": "legend"
},
"RisingAttack": {
@@ -123,8 +127,9 @@
"class": "fighter",
"damage": 12,
"desc": "피해 12",
"image": "3a3d4b8bb5bd4137847caf883e4bf38e",
"rarity": "unique"
"image": "115e309771604743853abad2d8d186bc",
"rarity": "unique",
"fx": "6f283d96d5804b4fb88009685a11c1f8"
},
"ThunderCharge": {
"name": "썬더 차지",
@@ -134,8 +139,9 @@
"damage": 7,
"weak": 1,
"desc": "피해 7, 약화 1",
"image": "f1b7e3041909411eb67af884b446e1e1",
"rarity": "unique"
"image": "b7030d8caedc4fbc9f38fe1e541d6e6b",
"rarity": "unique",
"fx": "997fa6999aa04dbb97a1dd99025fa2ba"
},
"BlizzardCharge": {
"name": "블리자드 차지",
@@ -145,8 +151,9 @@
"damage": 7,
"vuln": 1,
"desc": "피해 7, 취약 1",
"image": "7915c70952ad432f99519ad79bf929a4",
"rarity": "unique"
"image": "9aac955d159f49c1bc913ef96128e781",
"rarity": "unique",
"fx": "2799562e984c4a4da3b73e1f3431057c"
},
"PowerGuard": {
"name": "파워 가드",
@@ -166,8 +173,9 @@
"damage": 9,
"pierce": true,
"desc": "피해 9, 방어 무시",
"image": "e312e535a2bc4fed82d36f9c6027c9db",
"rarity": "unique"
"image": "251b6e12329048429490049a4f3cf564",
"rarity": "unique",
"fx": "1b0afc410a1a458598eb7ca2fb26e97d"
},
"IronWall": {
"name": "아이언 월",
@@ -197,8 +205,9 @@
"class": "magician",
"damage": 6,
"desc": "피해 6",
"image": "a1ee3069fce14498b92998542679ae40",
"rarity": "normal"
"image": "e84880eaf89442128d3af2be5c80a74f",
"rarity": "normal",
"fx": "1d5877e1120a42d0907f204c959888b1"
},
"MagicGuard": {
"name": "매직 가드",
@@ -218,8 +227,9 @@
"damage": 3,
"hits": 2,
"desc": "피해 3 × 2회",
"image": "d6e7c04c436f42f19e9806ac5b4401ae",
"rarity": "normal"
"image": "f3fcac2d460041b288cc1973caaaf30f",
"rarity": "normal",
"fx": "ba4ac7c8f24845b68b7e689b7effcc93"
},
"Teleport": {
"name": "텔레포트",
@@ -229,7 +239,7 @@
"block": 3,
"draw": 1,
"desc": "방어도 3, 드로 1",
"image": "80c98c8e032b4f6c8371a24b4e1d8f14",
"image": "7f70a9dc7e304433bb8121dd9c4df98b",
"rarity": "normal"
},
"Slow": {
@@ -239,7 +249,7 @@
"class": "magician",
"weak": 2,
"desc": "약화 2 부여",
"image": "16f79f571a964430bf1953edc9a14c73",
"image": "7224cd3f9b7e497d9dd65f32a50865e4",
"rarity": "normal"
},
"FireArrow": {
@@ -249,8 +259,9 @@
"class": "firepoison",
"damage": 8,
"desc": "피해 8",
"image": "78b9be4e711c440f84fc21e51e812bae",
"rarity": "unique"
"image": "6fa15fd3a0004b409ea516c11a67e533",
"rarity": "unique",
"fx": "4a937e208875468eb63d891806fba3cd"
},
"PoisonBreath": {
"name": "포이즌 브레스",
@@ -259,7 +270,7 @@
"class": "firepoison",
"poison": 4,
"desc": "독 4 부여",
"image": "b4e8bd7508b54d208e4f2ad7414f8c0a",
"image": "07200f3c74854022baa7ebbefdc4ad8c",
"rarity": "unique"
},
"ElementAmp": {
@@ -270,7 +281,7 @@
"powerEffect": "strengthPerTurn",
"value": 1,
"desc": "매 턴 힘 +1",
"image": "9859f3ab41b945f797d56cd83f95b25f",
"image": "06865473977849bebe79062dbd608944",
"rarity": "legend"
},
"ThunderBolt": {
@@ -282,7 +293,8 @@
"aoe": true,
"desc": "모든 적에게 피해 6",
"image": "c6685d33cb2641f09d11cfa2d5cc820c",
"rarity": "legend"
"rarity": "legend",
"fx": "7d52f5e389bd4d44a30cf7cc54538f8f"
},
"ColdBeam": {
"name": "콜드 빔",
@@ -302,7 +314,7 @@
"class": "icelightning",
"block": 8,
"desc": "방어도 8",
"image": "b2a7274d868241c78aa5780f2beecddf",
"image": "bef20873a68a4651a91d74be457c2cfc",
"rarity": "unique"
},
"Heal": {
@@ -312,7 +324,7 @@
"class": "cleric",
"heal": 10,
"desc": "HP 10 회복",
"image": "b4127c181e2942e38821d4a9a1f14596",
"image": "8b935b7d7066493cb462834bbe287c74",
"rarity": "unique"
},
"Bless": {
@@ -323,7 +335,7 @@
"strength": 1,
"block": 5,
"desc": "힘 +1, 방어도 5",
"image": "d45553db4a414011b67486dfa8a12fe5",
"image": "607fc5457c1c44a0993a5c2fe3fb0c68",
"rarity": "unique"
},
"HolyArrow": {
@@ -333,8 +345,153 @@
"class": "cleric",
"damage": 8,
"desc": "피해 8",
"image": "0265e103b4904f178b1c2bdcd54d5975",
"image": "a80127195bf7471f9545b70e491f4719",
"rarity": "unique",
"fx": "4faa7b78e09643cf86339b8b7cf2abac"
},
"LuckySeven": {
"name": "럭키 세븐",
"cost": 1,
"kind": "Attack",
"class": "thief",
"damage": 3,
"hits": 2,
"desc": "피해 3 × 2회",
"rarity": "normal",
"image": "0539ba559f8c413dac95c52992b436d9",
"fx": "aa499663a278414b914b8fb9b8382879"
},
"DoubleStab": {
"name": "더블 스탭",
"cost": 2,
"kind": "Attack",
"class": "thief",
"damage": 5,
"hits": 2,
"desc": "피해 5 × 2회",
"rarity": "normal",
"image": "92a5020c978c46bdabab910598118b86",
"fx": "a82d0aae7f5e4db6a19078537afbe80c"
},
"DarkSight": {
"name": "다크 사이트",
"cost": 1,
"kind": "Skill",
"class": "thief",
"block": 6,
"desc": "방어도 6",
"rarity": "normal",
"image": "0946f69d84464df29b24b94c744c868d"
},
"Haste": {
"name": "헤이스트",
"cost": 1,
"kind": "Skill",
"class": "thief",
"block": 3,
"draw": 1,
"desc": "방어도 3, 드로 1",
"rarity": "normal",
"image": "e65317856a914b8686f55e3351c3a24c"
},
"Drain": {
"name": "드레인",
"cost": 1,
"kind": "Attack",
"class": "thief",
"damage": 5,
"heal": 3,
"desc": "피해 5, HP 3 회복",
"rarity": "unique"
},
"CriticalThrow": {
"name": "크리티컬 스로우",
"cost": 2,
"kind": "Attack",
"class": "assassin",
"damage": 8,
"hits": 2,
"desc": "피해 8 × 2회",
"rarity": "unique",
"image": "1b0f2dc8abd0434990eee1befefcbe0d",
"fx": "23232336918d43f49fab19b888920f0c"
},
"ShadowStar": {
"name": "쉐도우 스타",
"cost": 1,
"kind": "Attack",
"class": "assassin",
"damage": 6,
"weak": 1,
"desc": "피해 6, 약화 1",
"rarity": "unique",
"image": "2d394e08d95841028d3dc95fca200756",
"fx": "ab45ee74d258419096e1e132af68aeca"
},
"ClawMastery": {
"name": "클로 마스터리",
"cost": 1,
"kind": "Power",
"class": "assassin",
"powerEffect": "strengthPerTurn",
"value": 1,
"desc": "매 턴 힘 +1",
"rarity": "legend",
"image": "aa09741ae1e145a28d1e1c19aeb9e83c"
},
"SavageBlow": {
"name": "새비지 블로우",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"damage": 3,
"hits": 3,
"desc": "피해 3 × 3회",
"rarity": "unique",
"image": "92a5020c978c46bdabab910598118b86",
"fx": "a82d0aae7f5e4db6a19078537afbe80c"
},
"Steal": {
"name": "스틸",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"block": 4,
"draw": 1,
"desc": "방어도 4, 드로 1",
"rarity": "unique",
"image": "c1e19219745e44c39ae6ac2f77e347d9"
},
"MesoGuard": {
"name": "메소 가드",
"cost": 1,
"kind": "Power",
"class": "bandit",
"powerEffect": "blockPerTurn",
"value": 3,
"desc": "매 턴 방어도 +3",
"rarity": "legend"
},
"Wound": {
"name": "상처",
"cost": 0,
"kind": "Status",
"desc": "사용할 수 없다. 손패를 막는 저주.",
"class": "curse",
"rarity": "normal",
"unplayable": true,
"curse": true
},
"Burn": {
"name": "화상",
"cost": 0,
"kind": "Status",
"desc": "사용 불가. 손패에 있으면 턴 종료 시 피해 2.",
"class": "curse",
"rarity": "normal",
"unplayable": true,
"curse": true,
"endTurnDamage": 2
}
},
"starterDecks": {
@@ -361,6 +518,18 @@
"MagicGuard",
"MagicGuard",
"MagicClaw"
],
"thief": [
"LuckySeven",
"LuckySeven",
"LuckySeven",
"LuckySeven",
"LuckySeven",
"DarkSight",
"DarkSight",
"DarkSight",
"DarkSight",
"DoubleStab"
]
}
}

View File

@@ -46,7 +46,8 @@
"intents": [
{ "kind": "Attack", "value": 4 },
{ "kind": "Attack", "value": 4 },
{ "kind": "Attack", "value": 10 }
{ "kind": "Attack", "value": 10 },
{ "kind": "AddCard", "card": "Wound", "count": 1 }
]
},
"pig": {
@@ -67,6 +68,24 @@
{ "kind": "Attack", "value": 9 }
]
},
"red_snail": {
"name": "빨간 달팽이",
"maxHp": 14,
"intents": [
{ "kind": "Attack", "value": 5 },
{ "kind": "Defend", "value": 6 },
{ "kind": "Attack", "value": 7 }
]
},
"stump": {
"name": "나무토막",
"maxHp": 19,
"intents": [
{ "kind": "Defend", "value": 5 },
{ "kind": "Attack", "value": 8 },
{ "kind": "Attack", "value": 6 }
]
},
"mushmom": {
"name": "머쉬맘",
"maxHp": 75,
@@ -75,7 +94,8 @@
{ "kind": "Debuff", "effect": "weak", "value": 2 },
{ "kind": "Attack", "value": 16 },
{ "kind": "Attack", "value": 9 },
{ "kind": "Defend", "value": 6 }
{ "kind": "Defend", "value": 6 },
{ "kind": "AddCard", "card": "Burn", "count": 1 }
]
},
"modified_snail": {

View File

@@ -3,7 +3,7 @@
"ironHeart": { "name": "강철 심장", "desc": "전투 시작 시 방어도 +6", "hook": "combatStart", "effect": "block", "value": 6, "icon": "e555b3a62f3c49dbb2c53784e6bd481f" },
"energyCore": { "name": "에너지 코어", "desc": "턴 시작 시 에너지 +1", "hook": "turnStart", "effect": "energy", "value": 1, "icon": "a41014f28b47434ab9f49ef104523862" },
"vampire": { "name": "흡혈 송곳니", "desc": "공격 카드 사용 시 HP +1", "hook": "cardPlayed", "effect": "healOnAttack", "value": 1, "icon": "ed64cde7e6c44b9e99502847e54f04e9" },
"goldIdol": { "name": "황금 우상", "desc": "전투 승리 시 골드 +10", "hook": "combatReward", "effect": "gold", "value": 10, "icon": "03bb05c92b8f45edb0f3dad2e118fd5a" },
"goldIdol": { "name": "황금 우상", "desc": "전투 승리 시 메소 +10", "hook": "combatReward", "effect": "gold", "value": 10, "icon": "03bb05c92b8f45edb0f3dad2e118fd5a" },
"potionBelt": { "name": "장인의 벨트", "desc": "물약 슬롯이 5칸으로 늘어난다", "hook": "passive", "effect": "potionSlots", "value": 5, "icon": "36725b4566ac40d4902e2ab2113c2096" },
"burningBlood": { "name": "자쿰의 투구", "desc": "전투 승리 시 HP 6 회복", "hook": "combatEnd", "effect": "healOnWin", "value": 6, "icon": "07f994825ce34131b419d43e890c878d" },
"vajra": { "name": "미스릴 해머", "desc": "전투 시작 시 힘 +1", "hook": "combatStart", "effect": "strength", "value": 1, "icon": "59d2579d46dc41d590a9e6b141ad458b" },

View File

@@ -0,0 +1,97 @@
# P14 — 반복 런 · 로비 · 영혼 · 도적 · 몬스터 랜덤성 설계
> 작성 2026-06-13. 사용자 자율 실행 지시(Phase별 커밋 → 최종 push/PR)에 따라 인터랙티브 승인 게이트 없이
> 설계 결정을 본 문서에 기록·커밋하고 순차 구현한다. 산출물(`ui/DefaultGroup.ui`·`*.codeblock`·`*.map`·
> `*.gamelogic`)은 생성기(`tools/`)·데이터(`data/`)에서 100% 생성되므로 본 작업은 전부 소스만 수정한다(RULES.md).
## 목표
기존 P1~P13 단발 런 구조를, **로비 허브를 중심으로 반복 수행하는 로그라이트 루프**로 재편하고
도적 직업·몬스터 랜덤성·영혼 메타 성장·카드 UX를 추가한다.
## 핵심 설계 결정 (요약)
1. **맵 5개 + 반복 루프**: 런은 map01~map05 5막. 최종 보스 클리어 시 무한 진행이 아니라 **로비로 복귀**해
영혼을 정산하고 다음 런을 준비. "반복 수행이 메인"을 *로비 기점 반복 런*으로 해석.
2. **로비 = 스크린 HUD**: 게임 전체가 이미 스크린 HUD(MapHud/ShopHud/RewardHud) 구동이고 물리 맵은 배경일 뿐이다.
로비도 동일하게 `LobbyHud`(스크린)로 구현하고, NPC는 클릭 가능한 스프라이트 버튼으로 배치한다.
NPC 4종: **도감(Codex)·상점(영혼 메타)·런 시작·게시판(채팅 대용)**. 첫 실행/패배/클리어 시 진입점.
3. **영혼(Soul)**: 승천(패널티 누적)과 역할 분리된 **영구 강화 메타 화폐**.
*2차 전직을 한 상태로* 맵 보스를 클리어할 때마다 누적. 로비 상점 NPC에서 해금 구매 → 다음 런에 이점.
저장은 승천 RPC 패턴 복제(`UserDataStorage`, key `soulPoints`/`soulUnlocks`).
4. **도적**: `bandit` 프레임이 이미 데이터에 준비됨. 도적 1차 + 2차(어쌔신/시프) 카드·스타터덱 신규.
5. **몬스터 랜덤성**: 구성(일반 1~3 / 엘리트 1+일반 0~2 / 보스 1)과 행동(정의된 intent 중 랜덤)을 런타임 추첨.
StS2식 "덱 오염" intent(`AddCard`)와 저주 카드 신규.
6. **메소**: 표면 문자열은 이미 메소. 잔존 `goldIdol.desc` 정정 + 메소 코인 아이콘 추가(내부 식별자 `Gold`는 유지).
## Phase 구성 (각 Phase = 1+ 커밋, 소스 수정 → 재생성 → 카운트/테스트 검증)
### Phase 1 — 맵 5막 · depth 7 · 노드 인접 규칙
- `gen-slaydeck.mjs`: `MAP_ROWS 7→6`(걷는행6+보스=총7), `ACT_COUNT 3→5`, `ACT_MAPS [map01..map05]`, `RUN_LENGTH 3→5`.
- 노드 타입 인접 금지 확장: 현재 elite만 부모-자식 연속 금지 → **rest·shop·elite** 3종 모두 금지.
`GenerateMap`(Lua 4333-4358) + `rogue-map.mjs`(67-72) 미러 + `rogue-map.test.mjs` 단언 추가.
- 막 배율 `1+(Floor-1)*0.6`(`:2776`)을 5막 기준 `1+(Floor-1)*0.45`로 완화.
- 맵 파일 11→5: 생성기 카운트(`gen-maps`/`gen-map-encounters`/`gen-combat-monster`/`freeze-turn-monsters`/
`gen-camera`/`gen-player-lock`) `[2..11]→[2..5]`, `length:11→5`. `git rm map/map06..11.map`.
`Global/SectorConfig.config`에서 map06~11 엔트리 제거(생성기가 재구성하도록 보정 또는 수동 정리).
### Phase 2 — 노드 가로 레이아웃(왼→오른쪽)
- `gen-slaydeck.mjs:1536-1538` 좌표 함수 row↔x·col↔y 스왑 + 호출부(1573/1600/1605) 인자 스왑. Lua 무수정.
보스는 최우측 중앙. 도트 보간·간선·라벨 자동 추종.
### Phase 3 — 몬스터 랜덤 구성 · 랜덤 행동 · AddCard · map01 배치
- `data/enemies.json`: 종별 `tier`(normal/elite/boss) 추가, 일부 종에 `AddCard` intent 추가.
map01용 일반 5종 + 엘리트 1종 보장(slime/orange/blue/green mushroom/pig + mushmom 등 기존 활용).
- `data/cards.json`: 저주/상태 카드 신규(`kind:"Status"`, `unplayable:true`, `curse:true`, `endTurnDamage?`).
- `gen-slaydeck.mjs`: `BuildMonsters`(2780) 노드타입별 랜덤 구성, `EnemyActStep`(3603) 랜덤 intent 선택
(예고=확정: 턴 종료 시 다음 행동 추첨 저장), `AddCard` intent 처리, `PlayCard` unplayable 가드,
카드 직렬화(`luaCardsTable`)에 신규 필드, intent 직렬화(`luaIntentsArray`)에 `card`/`count`.
- `sim-balance.mjs`+test: 랜덤 행동(rng)·AddCard 미러, 결정성 테스트 유지, 저주 unplayable 필터.
- `gen-map-encounters.mjs`: map01 편입 + 일반5/엘리트1 레이아웃(오른쪽 배치). 엘리트 맵에 일반 혼합용 변형 배치.
### Phase 4 — 도적 클래스 + 2차(어쌔신/시프)
- `data/cards.json`: 도적 1차(class `thief`) + 어쌔신(class `assassin`) + 시프(class `bandit`) 카드 + 스타터덱.
- `data/cardframes.json`: `classToFrame`에 thief/assassin/bandit → `bandit` 프레임 매핑.
- `gen-slaydeck.mjs`: `CLASSES.thief`(maxHp 75), `JOBS.thief`(어쌔신/시프), CharacterSelectHud Thief 해금,
`BindMenuButtons` ThiefButton, `RenderCharacterSelect`/`StartNewGame`/`StartRun`/`JobLabel` 도적 분기.
- 전사/법사 2차는 완비 확인됨(수정 불요).
### Phase 5 — 카드 스킬 아이콘 · 피격 이펙트 분리
- `data/cards.json`: 공격 카드에 `fx`(이펙트 RUID) 필드 추가, `image`는 스킬 아이콘 유지.
- `gen-slaydeck.mjs`: `luaCardsTable` fx 직렬화, `PlayCard`(3296-3298) FX 인자를 `c.fx or c.image`로.
- RUID는 MSW 공식 리소스 asset 검색으로 수급(계정 업로드 금지·RULES §5).
### Phase 6 — 카드 UX: 핸드 최대 10 · 마우스오버 확대/툴팁
- `gen-slaydeck.mjs`: CardHand 슬롯 5→10 확장, `RenderHand` 동적 간격(장수 비례), `DrawCards` 10장 상한
(초과 분 자동 버림), 카드 hover 이벤트 바인딩(enter→`ShowTooltip`+스케일업, exit→복귀), 드래그 중 가드.
- `sim-balance.mjs`+test: 드로우 상한 미러.
### Phase 7 — 메소 전환 + 메소 아이콘
- `data/relics.json`: `goldIdol.desc` "골드"→"메소".
- `gen-slaydeck.mjs`: TopBar·ShopHud 메소 텍스트 옆 코인 아이콘 sprite 추가(공식 RUID).
### Phase 8 — 로비 + NPC + 반복 루프
- `gen-slaydeck.mjs`: `LobbyHud` 섹션(배경 + NPC 4종 스프라이트 버튼), `ShowLobby`/`ShowState("lobby")`,
NPC 핸들러(Codex→CodexHud, Shop→영혼상점, RunStart→CharacterSelect, Board→게시판 패널).
`CodexHud`: 전 카드 도감(클래스별 그리드). `OnBeginPlay``ShowLobby`(메뉴 대체), `EndRun``ShowLobby`.
첫 실행/패배/클리어 모두 로비 기점.
### Phase 9 — 영혼(Soul) 시스템
- `gen-slaydeck.mjs`: `SoulPoints`/`SoulUnlocks` 프로퍼티, RPC `ReqLoadSouls`/`SaveSouls`/`RecvSouls`(ExecSpace 5/6),
보스 클리어 & `PlayerJob~=""`일 때 영혼 가산(`ContinueAfterBoss`/`CheckCombatEnd`), 로비 영혼 상점 UI·구매,
해금 효과를 `StartRun`에 적용(시작 메소/시작 유물/시작 HP/덱 강화 등 덱빌딩 이점).
### Final — 전체 재생성 · 테스트 · push · PR
- 전 생성기 재생성, `node --test`(rogue-map·sim-balance), 카운트 검증, 가능 시 메이커 플레이테스트.
- `tools/git/gitea-pr.mjs`로 UTF-8 spec JSON 작성 후 PR 생성(RULES §4).
## 검증 원칙 (RULES §2·§6)
- 산출물 본문 출력 금지 — `grep -c`/JSON parse/카운트만.
- 전투·맵 규칙 수정 시 Lua↔JS 미러 동시 수정 + `node --test` 통과.
- 커밋은 기능 단위, 산출물 재생성은 메시지에 명시.
## 알려진 제약 / 결정 근거
- **로비 "돌아다니기"**: 물리 맵 walkable 로비는 player 이동이 전역 freeze(턴제)라 위험·고비용 → 스크린 HUD
NPC 클릭으로 동일 기능 제공(아키텍처 정합). 추후 walkable 로비는 확장 슬롯.
- **카드 아이콘/이펙트**: 현재 `c.image`가 카드 아트 겸 피격 FX로 이중 사용 중 → `fx` 분리로 의도 달성.
- **영혼 vs 승천**: 승천=적 강화 패널티 토글, 영혼=플레이어 영구 강화 → 같은 저장소 다른 key로 공존.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@
{
"id": "00000bb8-0000-4000-8000-000000000bb8",
"path": "/maps/map03",
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.PlayerLock,script.MapCamera",
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock",
"jsonString": {
"name": "map03",
"path": "/maps/map03",
@@ -1105,11 +1105,11 @@
"Enable": true
},
{
"@type": "script.PlayerLock",
"@type": "script.MapCamera",
"Enable": true
},
{
"@type": "script.MapCamera",
"@type": "script.PlayerLock",
"Enable": true
}
],
@@ -6366,7 +6366,7 @@
{
"id": "00000dac-0000-4000-8000-000000000dac",
"path": "/maps/map03/combat_1",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "combat_1",
"path": "/maps/map03/combat_1",
@@ -6379,12 +6379,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00000dac-0000-4000-8000-000000000dac",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6425,38 +6425,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6469,26 +6437,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6507,10 +6482,33 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
"EnemyId": "green_mushroom",
"EnemyId": "pig",
"Group": "combat"
}
],
@@ -6520,7 +6518,7 @@
{
"id": "00000dad-0000-4000-8000-000000000dad",
"path": "/maps/map03/combat_2",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "combat_2",
"path": "/maps/map03/combat_2",
@@ -6533,12 +6531,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00000dad-0000-4000-8000-000000000dad",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6579,38 +6577,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6623,26 +6589,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6661,10 +6634,33 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
"EnemyId": "blue_mushroom",
"EnemyId": "red_snail",
"Group": "combat"
}
],
@@ -6674,7 +6670,7 @@
{
"id": "00000dae-0000-4000-8000-000000000dae",
"path": "/maps/map03/combat_3",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "combat_3",
"path": "/maps/map03/combat_3",
@@ -6687,12 +6683,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00000dae-0000-4000-8000-000000000dae",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6733,38 +6729,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6777,26 +6741,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6815,6 +6786,29 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
@@ -6828,7 +6822,7 @@
{
"id": "00000daf-0000-4000-8000-000000000daf",
"path": "/maps/map03/elite_4",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "elite_4",
"path": "/maps/map03/elite_4",
@@ -6841,12 +6835,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00000daf-0000-4000-8000-000000000daf",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6887,38 +6881,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6931,26 +6893,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6969,6 +6938,29 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
@@ -6982,7 +6974,7 @@
{
"id": "00000db0-0000-4000-8000-000000000db0",
"path": "/maps/map03/elite_5",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "elite_5",
"path": "/maps/map03/elite_5",
@@ -6995,12 +6987,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00000db0-0000-4000-8000-000000000db0",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -7041,38 +7033,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -7085,26 +7045,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -7123,6 +7090,29 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
@@ -7136,7 +7126,7 @@
{
"id": "00000db1-0000-4000-8000-000000000db1",
"path": "/maps/map03/boss_6",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "boss_6",
"path": "/maps/map03/boss_6",
@@ -7149,12 +7139,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00000db1-0000-4000-8000-000000000db1",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -7195,38 +7185,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -7239,26 +7197,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -7277,6 +7242,29 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,

View File

@@ -16,7 +16,7 @@
{
"id": "00000fa0-0000-4000-8000-000000000fa0",
"path": "/maps/map04",
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.PlayerLock,script.MapCamera",
"componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock",
"jsonString": {
"name": "map04",
"path": "/maps/map04",
@@ -1105,11 +1105,11 @@
"Enable": true
},
{
"@type": "script.PlayerLock",
"@type": "script.MapCamera",
"Enable": true
},
{
"@type": "script.MapCamera",
"@type": "script.PlayerLock",
"Enable": true
}
],
@@ -6366,7 +6366,7 @@
{
"id": "00001194-0000-4000-8000-000000001194",
"path": "/maps/map04/combat_1",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "combat_1",
"path": "/maps/map04/combat_1",
@@ -6379,12 +6379,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00001194-0000-4000-8000-000000001194",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6425,38 +6425,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6469,26 +6437,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6507,10 +6482,33 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
"EnemyId": "pig",
"EnemyId": "blue_mushroom",
"Group": "combat"
}
],
@@ -6520,7 +6518,7 @@
{
"id": "00001195-0000-4000-8000-000000001195",
"path": "/maps/map04/combat_2",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "combat_2",
"path": "/maps/map04/combat_2",
@@ -6533,12 +6531,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00001195-0000-4000-8000-000000001195",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6579,38 +6577,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6623,26 +6589,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6661,10 +6634,33 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
"EnemyId": "blue_mushroom",
"EnemyId": "stump",
"Group": "combat"
}
],
@@ -6674,7 +6670,7 @@
{
"id": "00001196-0000-4000-8000-000000001196",
"path": "/maps/map04/combat_3",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "combat_3",
"path": "/maps/map04/combat_3",
@@ -6687,12 +6683,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00001196-0000-4000-8000-000000001196",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6733,38 +6729,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6777,26 +6741,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6815,10 +6786,33 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
"EnemyId": "orange_mushroom",
"EnemyId": "green_mushroom",
"Group": "combat"
}
],
@@ -6828,7 +6822,7 @@
{
"id": "00001197-0000-4000-8000-000000001197",
"path": "/maps/map04/elite_4",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "elite_4",
"path": "/maps/map04/elite_4",
@@ -6841,12 +6835,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00001197-0000-4000-8000-000000001197",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -6887,38 +6881,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -6931,26 +6893,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -6969,6 +6938,29 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
@@ -6982,7 +6974,7 @@
{
"id": "00001198-0000-4000-8000-000000001198",
"path": "/maps/map04/elite_5",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "elite_5",
"path": "/maps/map04/elite_5",
@@ -6995,12 +6987,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00001198-0000-4000-8000-000000001198",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -7041,38 +7033,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -7085,26 +7045,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -7123,6 +7090,29 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,
@@ -7136,7 +7126,7 @@
{
"id": "00001199-0000-4000-8000-000000001199",
"path": "/maps/map04/boss_6",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
"jsonString": {
"name": "boss_6",
"path": "/maps/map04/boss_6",
@@ -7149,12 +7139,12 @@
"revision": 2,
"origin": {
"type": "Model",
"entry_id": "StaticMonster",
"entry_id": "ChaseMonster",
"sub_entity_id": null,
"root_entity_id": "00001199-0000-4000-8000-000000001199",
"replaced_model_id": null
},
"modelId": "staticmonster",
"modelId": "chasemonster",
"@components": [
{
"@type": "MOD.Core.TransformComponent",
@@ -7195,38 +7185,6 @@
"StartFrameIndex": 0,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.78,
"y": 0.86
},
"ColliderOffset": {
"x": 0.03999999,
"y": 0.43
},
"CollisionGroup": {
"Id": "8992acd1e8cd45838db6f10a7b41df09"
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.RigidbodyComponent",
"MoveVelocity": {
@@ -7239,26 +7197,33 @@
},
"Enable": true
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.MovementComponent",
"Enable": false,
"InputSpeed": 0
"InputSpeed": 0,
"JumpForce": 6,
"Enable": false
},
{
"@type": "MOD.Core.StateComponent",
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.HitComponent",
"BoxSize": {
"x": 0.63,
"y": 0.58
},
"ColliderOffset": {
"x": 0.0449999869,
"y": 0.29
},
"IsLegacy": false,
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSpawnerComponent",
"Enable": true
},
{
"@type": "script.Monster",
@@ -7277,6 +7242,29 @@
"y": 0
}
},
{
"@type": "MOD.Core.KinematicbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.SideviewbodyComponent",
"MoveVelocity": {
"x": 0,
"y": 0
},
"Enable": true
},
{
"@type": "MOD.Core.DamageSkinSettingComponent",
"DamageSkinId": {
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
},
"Enable": true
},
{
"@type": "script.CombatMonster",
"Enable": true,

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,7 @@ export function loadData() {
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
export function chooseAction(hand, cards, energy) {
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id].cost <= 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');
@@ -122,7 +122,9 @@ export function simulateCombat(data, rng, stats) {
for (let k = 0; k < n; k++) {
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
if (drawPile.length === 0) break;
hand.push(drawPile.pop());
const card = drawPile.pop();
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
if (hand.length >= 10) discard.push(card); else hand.push(card);
}
}
const aliveList = () => mob.filter((m) => m.alive);
@@ -202,7 +204,12 @@ export function simulateCombat(data, rng, stats) {
if (c.draw) draw(c.draw);
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
}
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
let burn = 0;
for (const hid of hand) { const hc = cards[hid]; if (hc && hc.endTurnDamage) burn += hc.endTurnDamage; }
if (burn > 0) { pHp -= burn; if (pHp < 0) pHp = 0; }
discard.push(...hand); hand = [];
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
// 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전)
if (pWeak > 0) pWeak--;
if (pVuln > 0) pVuln--;
@@ -215,7 +222,8 @@ export function simulateCombat(data, rng, stats) {
if (m.hp <= 0) { m.hp = 0; m.alive = false; continue; }
}
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
const it = m.intents[m.intentIdx];
// 정의된 intent 중 랜덤 선택 (Lua EnemyActStep 동기화 — 순차→랜덤)
const it = m.intents.length ? m.intents[Math.floor(rng() * m.intents.length)] : null;
if (it) {
if (it.kind === 'Attack') {
const atk = calcAttack(it.value, m.str, m.weak, pVuln);
@@ -224,9 +232,12 @@ export function simulateCombat(data, rng, stats) {
else if (it.kind === 'Debuff') {
if (it.effect === 'weak') pWeak += it.value;
else if (it.effect === 'vuln') pVuln += it.value;
} else if (it.kind === 'AddCard') {
// StS2식 덱 오염 — 저주 카드를 버린 더미에 추가 (Lua 동기화)
const cnt = it.count || 1;
for (let k = 0; k < cnt; k++) discard.push(it.card);
}
}
m.intentIdx = (m.intentIdx + 1) % m.intents.length;
// 적 디버프 감소 — Lua EnemyActStep 동기화 (자기 행동 후)
if (m.weak > 0) m.weak--;
if (m.vuln > 0) m.vuln--;

View File

@@ -345,3 +345,33 @@ test('simulateCombat: draw — 카드 드로로 손패 보충', () => {
assert.ok(r.turns <= 2, `seed ${s}: ${r.turns}`);
}
});
test('chooseAction: unplayable(저주) 카드는 건너뜀', () => {
const cards = { Strike: { cost: 1, kind: 'Attack', damage: 6 }, Wound: { cost: 0, kind: 'Status', unplayable: true } };
assert.equal(chooseAction(['Wound', 'Strike'], cards, 3), 1); // Strike 선택
assert.equal(chooseAction(['Wound'], cards, 3), -1); // 낼 카드 없음
});
test('simulateCombat: AddCard intent가 저주를 덱에 추가(오염)', () => {
const data = {
cards: { Hit: { name: '히트', cost: 1, kind: 'Attack', damage: 1 }, Wound: { name: '상처', cost: 0, kind: 'Status', unplayable: true } },
starterDeck: ['Hit', 'Hit', 'Hit', 'Hit', 'Hit'],
monsters: [{ name: '오염자', maxHp: 9999, intents: [{ kind: 'AddCard', card: 'Wound', count: 1 }] }],
};
// 적은 공격 안 하고 매 턴 저주만 추가 → 플레이어 무피해(승리 불가, 9999hp) → 무승부, 사망 아님
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.win, false);
assert.equal(r.draw, true);
});
test('simulateCombat: endTurnDamage(화상)이 턴 종료 시 누적 피해', () => {
const data = {
cards: { Skip: { name: '대기', cost: 3, kind: 'Skill', block: 0 }, Burn: { name: '화상', cost: 0, kind: 'Status', unplayable: true, endTurnDamage: 2 } },
starterDeck: ['Burn', 'Skip', 'Skip', 'Skip', 'Skip'],
monsters: [{ name: '무공격', maxHp: 9999, intents: [{ kind: 'Defend', value: 0 }] }],
};
// 적은 방어만(무피해). 손패의 Burn이 매 턴 -2 → 80hp 잠식 → MAX_TURNS 전 사망 → win false(draw 아님)
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.win, false);
assert.notEqual(r.draw, true);
});

View File

@@ -4,7 +4,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
// 새 CameraComponent를 만들지 않고(엔진 소유) 기존 카메라 속성만 런타임 설정한다.
// 플레이어 입력 차단·시선 고정은 tools/player/gen-player-lock.mjs(script.PlayerLock)로 분리됨.
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11
const MAP_NUMBERS = Array.from({ length: 5 }, (_, i) => i + 1); // map01~05
function prop(Type, Name, DefaultValue = 'nil') {
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };

File diff suppressed because it is too large Load Diff

View File

@@ -2,15 +2,24 @@ import { readFileSync, writeFileSync } from 'node:fs';
// map02~11에 노드 타입별 몬스터 그룹(combat3/elite2/boss1)을 맵별 테마로 자동 구성.
// 기존 몬스터 엔티티를 전부 제거하고 첫 몬스터를 템플릿으로 6마리 재생성(결정론).
const MAP_NUMBERS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const COMBAT_POOL = ['orange_mushroom', 'green_mushroom', 'pig', 'blue_mushroom'];
const MAP_NUMBERS = [1, 2, 3, 4, 5];
const COMBAT_POOL = ['orange_mushroom', 'green_mushroom', 'pig', 'blue_mushroom', 'red_snail', 'stump'];
const ELITE_POOL = ['mushmom', 'modified_snail'];
const BOSS_POOL = ['king_slime', 'slime_boss'];
const LAYOUT = [
// map01: StS2식 일반 5종 + 엘리트 1 + 보스 1(보스 노드용, 화면 우측 포메이션).
// 그 외 맵: 일반 3 + 엘리트 2 + 보스 1. 전투 시 BuildMonsters가 노드 타입별로 1~3마리 랜덤 추첨.
const LAYOUT_MAP01 = [
{ group: 'combat', x: 2.6 }, { group: 'combat', x: 3.6 }, { group: 'combat', x: 4.6 },
{ group: 'combat', x: 5.6 }, { group: 'combat', x: 6.6 },
{ group: 'elite', x: 4.6 },
{ group: 'boss', x: 4.6 },
];
const LAYOUT_DEFAULT = [
{ group: 'combat', x: 2.3 }, { group: 'combat', x: 3.8 }, { group: 'combat', x: 5.2 },
{ group: 'elite', x: 3.0 }, { group: 'elite', x: 5.0 },
{ group: 'boss', x: 4.0 },
];
const layoutFor = (nn) => (nn === 1 ? LAYOUT_MAP01 : LAYOUT_DEFAULT);
const MONSTER_VARIANTS = [
{ sprite: '96e955c1bf27415e84f96deea200a8f1', stand: '96e955c1bf27415e84f96deea200a8f1', hit: 'aec9504d5dc24aceb5646b79d30abad4', die: '65a2bfb039614f2e9e4ccc354340153d' },
{ sprite: 'f86992ba9c41487c8480fcb893fcbda6', stand: 'f86992ba9c41487c8480fcb893fcbda6', hit: 'd305b942b1704c8084548108ff3b7a6b', die: '5a563e5fd98c4132b61057dc6bb8aaf2' },
@@ -54,13 +63,17 @@ function patchMap(nn) {
const template = monsters[0];
map.ContentProto.Entities = ents.filter((e) => !isMonster(e));
const rand = rng(nn * 7919 + 17);
const combatIds = pickN(rand, COMBAT_POOL, 3);
const eliteIds = pickN(rand, ELITE_POOL, 2);
const layout = layoutFor(nn);
const nCombat = layout.filter((s) => s.group === 'combat').length;
const nElite = layout.filter((s) => s.group === 'elite').length;
const combatIds = pickN(rand, COMBAT_POOL, nCombat);
const eliteIds = pickN(rand, ELITE_POOL, nElite);
const bossId = pick(rand, BOSS_POOL);
const variants = pickN(rand, MONSTER_VARIANTS, 6);
LAYOUT.forEach((slot, idx) => {
const variants = pickN(rand, MONSTER_VARIANTS, layout.length);
let ci = 0, ei = 0;
layout.forEach((slot, idx) => {
const m = JSON.parse(JSON.stringify(template));
const enemyId = slot.group === 'combat' ? combatIds[idx] : slot.group === 'elite' ? eliteIds[idx - 3] : bossId;
const enemyId = slot.group === 'combat' ? combatIds[ci++] : slot.group === 'elite' ? eliteIds[ei++] : bossId;
const name = `${slot.group}_${idx + 1}`;
m.id = encGuid(nn, idx);
m.path = `/maps/map${tag}/${name}`;

View File

@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
const TEMPLATE = 'map/map01.map';
const SECTOR = 'Global/SectorConfig.config';
const MAP_NUMBERS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const MAP_NUMBERS = [2, 3, 4, 5];
// 공식 맵에서 수확한 Background-타입 RUID 풀 (맵마다 1개씩, 서로 다르게).
// 공식 MapleStory 맵을 import해 각 맵의 BackgroundComponent.TemplateRUID를 수집함.
@@ -139,14 +139,14 @@ const targets = arg ? [Number(arg)] : MAP_NUMBERS;
const made = targets.map(buildMap);
console.log('Generated:', made.join(', '));
// SectorConfig 등록 (전체 생성 시에만, 중복 방지)
// SectorConfig 등록 (전체 생성 시에만) — 유효 맵만 유지하고 삭제된 맵 엔트리는 제거
if (!arg) {
const sector = JSON.parse(readFileSync(SECTOR, 'utf8'));
const entries = sector.ContentProto.Json.Sectors[0].entries;
for (const nn of MAP_NUMBERS) {
const key = `map://map${String(nn).padStart(2, '0')}`;
if (!entries.includes(key)) entries.push(key);
}
const sec0 = sector.ContentProto.Json.Sectors[0];
const valid = ['map://map01', ...MAP_NUMBERS.map((nn) => `map://map${String(nn).padStart(2, '0')}`)];
// map06~ 등 더 이상 존재하지 않는 맵 엔트리 제거 + 누락분 추가
sec0.entries = sec0.entries.filter((k) => !/^map:\/\/map\d+$/.test(k) || valid.includes(k));
for (const key of valid) if (!sec0.entries.includes(key)) sec0.entries.push(key);
writeFileSync(SECTOR, JSON.stringify(sector, null, 2), 'utf8');
console.log('SectorConfig entries:', entries.length);
console.log('SectorConfig entries:', sec0.entries.length);
}

View File

@@ -2,7 +2,7 @@
// ⚠️ 전투 규칙과 마찬가지로 tools/deck/gen-slaydeck.mjs 의 Lua(GenerateMap)와 로직 동기화 유지할 것.
// (Lua는 math.random, 여기는 주입 rng — 수치 동일성이 아니라 구조 규칙 동일성이 대상)
export const ROWS = 7; // 걷는 행 1..7, 보스는 row 8
export const ROWS = 6; // 걷는 행 1..6, 보스는 row 7 (depth 최대 7)
export const COLS = 4;
export const PATHS = 4;
@@ -64,12 +64,13 @@ export function generateMap(rng) {
const id = nodeId(r, c);
const node = nodes[id];
if (!node) continue;
// elite 부모 검사 (연속 엘리트 방지)
let eliteParent = false;
// 부모 노드 타입 수집 (rest/shop/elite 부모와 같은 타입 연속 금지)
const parentTypes = new Set();
for (const pn of Object.values(nodes)) {
if (pn.row === r - 1 && pn.type === 'elite' && pn.next.includes(id)) eliteParent = true;
if (pn.row === r - 1 && pn.next.includes(id)) parentTypes.add(pn.type);
}
const w = rowWeights(r).map(([t, wt]) => [t, t === 'elite' && eliteParent ? 0 : wt]);
const NO_REPEAT = new Set(['rest', 'shop', 'elite']);
const w = rowWeights(r).map(([t, wt]) => [t, NO_REPEAT.has(t) && parentTypes.has(t) ? 0 : wt]);
const total = w.reduce((s, [, wt]) => s + wt, 0);
const roll = rng() * total;
let acc = 0;

View File

@@ -61,7 +61,7 @@ test('타입 규칙: 1~2행 combat만, elite·treasure는 4행부터, shop·rest
}
});
test('boss: row 8 단일 노드, 7행 노드는 전부 boss로 연결', () => {
test('boss: row 7 단일 노드, 마지막 걷는 행 노드는 전부 boss로 연결', () => {
for (let s = 1; s <= 30; s++) {
const { nodes } = gen(s);
const bosses = Object.entries(nodes).filter(([, n]) => n.type === 'boss');
@@ -90,19 +90,21 @@ test('간선 제약: row+1로만, 열 차이 1 이하 (boss 간선 제외)', ()
}
});
test('elite 연속 금지: elite 부모를 가진 노드는 elite 아님', () => {
test('연속 금지: rest/shop/elite 부모와 같은 타입을 자식으로 두지 않음', () => {
const NO_REPEAT = new Set(['rest', 'shop', 'elite']);
for (let s = 1; s <= 100; s++) {
const { nodes } = gen(s);
for (const [id, n] of Object.entries(nodes)) {
if (n.type !== 'elite') continue;
if (!NO_REPEAT.has(n.type)) continue;
for (const nid of n.next) {
assert.notEqual(nodes[nid].type, 'elite', `seed ${s}: ${id}(elite) → ${nid}(elite)`);
if (nid === 'boss') continue;
assert.notEqual(nodes[nid].type, n.type, `seed ${s}: ${id}(${n.type}) → ${nid}(${n.type})`);
}
}
}
});
test('그리드 범위: 행 1..8, 열 1..4 (boss 제외)', () => {
test('그리드 범위: 행 1..6, 열 1..4 (boss 제외)', () => {
const { nodes } = gen(7);
for (const [id, n] of Object.entries(nodes)) {
if (id === 'boss') continue;

View File

@@ -5,7 +5,7 @@ const AI_COMPONENTS = new Set([
'MOD.Core.AIChaseComponent',
]);
const mapFiles = Array.from({ length: 11 }, (_, i) => `map/map${String(i + 1).padStart(2, '0')}.map`);
const mapFiles = Array.from({ length: 5 }, (_, i) => `map/map${String(i + 1).padStart(2, '0')}.map`);
const modelFiles = [
'Global/MoveMonster.model',
'Global/ChaseMonster.model',

View File

@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
// 맵 몬스터에 적 타입(EnemyId)을 부여하고, BeginPlay 시 /common 컨트롤러에 자기등록하는 마커.
// 카드 전투 시 컨트롤러가 등록 목록으로 인카운터를 구성한다.
const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11
const MAP_NUMBERS = Array.from({ length: 5 }, (_, i) => i + 1); // map01~05
const NAME_TO_ENEMY = { '주황버섯': 'orange_mushroom', '파란버섯': 'blue_mushroom' };
const DEFAULT_ENEMY = 'orange_mushroom';

View File

@@ -7,7 +7,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
const LOOK_DIRECTION_X = 1; // 1 = 오른쪽(몬스터가 배치된 전투 포메이션 방향)
const FIXED_LOOK_AT = true; // 바라보는 방향 고정
const CONTROLLER_ENABLE = false; // 플레이어 입력 차단
const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11
const MAP_NUMBERS = Array.from({ length: 5 }, (_, i) => i + 1); // map01~05
function prop(Type, Name, DefaultValue = 'nil') {
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };

22
tools/verify/count.mjs Normal file
View File

@@ -0,0 +1,22 @@
import { readFileSync } from 'node:fs';
// 산출물 카운트 검증 헬퍼 (RULES §2: 내용 출력 금지·카운트만).
// 사용: node tools/verify/count.mjs <key> <regex> [<regex> ...]
// key: ui | cb | common (산출물 경로는 여기 내장 — Bash 명령에 산출물 경로를 노출하지 않아 deny 회피)
const FILES = {
ui: 'ui/DefaultGroup.ui',
cb: 'RootDesk/MyDesk/SlayDeckController.codeblock',
common: 'Global/common.gamelogic',
};
const key = process.argv[2];
const path = FILES[key];
if (!path) { console.error(`unknown key: ${key} (use ${Object.keys(FILES).join('|')})`); process.exit(1); }
const content = readFileSync(path, 'utf8');
// JSON 유효성도 함께 확인
let jsonOk = false;
try { JSON.parse(content); jsonOk = true; } catch { jsonOk = false; }
console.log(`${path} bytes=${content.length} jsonValid=${jsonOk}`);
for (const pat of process.argv.slice(3)) {
const m = content.match(new RegExp(pat, 'g'));
console.log(` /${pat}/ = ${m ? m.length : 0}`);
}

File diff suppressed because it is too large Load Diff