23 Commits

Author SHA1 Message Date
1e87be2cd6 merge main into bandit silent deck 2026-06-14 17:48:59 +09:00
6cc008e894 Merge pull request 'feat: P15 — 로비를 전용 맵 + 월드 NPC로 (근접·클릭 상호작용, 로비 한정 이동·공격)' (#54) from feature/p15-lobby-map-npc into main
Reviewed-on: #54
2026-06-14 13:04:17 +09:00
760b856576 fix(lobby): 로비 이동·점프를 기본값으로 — InputSpeed 5→1.4, JumpForce 5→1.23 (P15)
기존 5/5는 보행 ~9u/s·점프 상승 14u로 과함. freeze가 안 건드린 intact RigidbodyComponent.WalkSpeed(1.4)/WalkJump(1.23) 기본값에 맞춤. 실측: 보행 2.5u/s, 점프 상승 1.79u로 정상화.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:02:30 +09:00
91bbe7d200 docs(p15): 계획에 메이커 정찰 실측 결과 반영
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:51:34 +09:00
989e3fe000 fix(lobby): 플레이테스트 — 이동 복원에 InputSpeed 필수 + FixedLookAt int 타입 (P15)
메이커 실측으로 확인:
- 이동에는 RigidbodyComponent.WalkAcceleration과 MovementComponent.InputSpeed가 둘 다 양수여야 함(WalkAccel만으론 안 걸림). LobbyMobility에 InputSpeed=5·JumpForce=5 추가.
- pc.FixedLookAt은 boolean이 아니라 int32 → false→0 (빌드 에러 해소).
- PlayerLock에 InputSpeed/JumpForce=0 대칭 재잠금 추가(전투맵 누설 방어).
- NPC 베이스 모델 inheritance 경고는 비치명적이라 proven-good(모델 유지) 결정 주석화.

검증: 로비 이동·점프, NpcCodex 근접(d=1.10<1.2)·↑키→카드 도감, 런 시작→map01 텔레포트+이동 잠금(InputSpeed=0), 로비 복귀→이동 재해제 전부 정상.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:50:51 +09:00
0e064cc1e9 feat(lobby): 로비 맵 흐름 통합 — 텔레포트·NPC 디스패치·StartRun map01·LobbyHud 슬림화 (P15)
- OnBeginPlay: 공격 키(Ctrl, 로비 한정) 바인딩
- ShowLobby→GoLobbyMap: 월드 시작·런 종료 시 로비 맵 텔레포트
- OnLobbyNpcInteract(id): 월드 NPC→기존 기능 패널 디스패치
- StartRun: 1막 진입 시 map01 물리 텔레포트(BuildMonsters CurrentMapName 필터 대응)
- LobbyHud: 버튼-행 제거, 투명·비레이캐스트화(맵 노출·클릭 통과), 영혼/승천 미니 정보바만 유지
- BindLobbyButtons: NPC 바인딩 제거

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:36:13 +09:00
de1e69de7d fix(lobby): 전투맵 PlayerLock에 WalkAcceleration=0 런타임 재잠금 (P15)
로비에서 푼 이동(WalkAcceleration)이 텔레포트 후 전투맵에 누설돼도 PlayerLock이 맵 로드 시 0으로 재설정해 확실히 잠금. 정찰로 WalkAcceleration이 실제 이동 레버임을 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:31:50 +09:00
a683f186d4 feat(lobby): LobbyNpc(근접·클릭 상호작용)·LobbyMobility(이동 해제) codeblock (P15)
LobbyNpc: 거리 폴링→머리위 마크 토글, TouchEvent/UpArrow→Interact→컨트롤러 OnLobbyNpcInteract. LobbyMobility: 진입 시 pc.Enable·WalkAcceleration=0.7 복원(정찰 확정). 공격 키는 컨트롤러에서 처리.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:29:44 +09:00
a309da2a99 feat(lobby): 로비 전용 맵 + NPC 4종 월드 엔티티 생성기 (P15)
map01 클론→lobby.map(헤네시스 배경), 몬스터 제거, NPC 4종(공식 메이플 NPC 스프라이트)+머리위 마크 배치. 각 NPC에 TouchReceiveComponent+script.LobbyNpc, 루트에 script.LobbyMobility(PlayerLock 제거). SectorConfig map://lobby 등록.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:27:44 +09:00
82bf22d4cc docs(p15): 로비 맵 + 월드 NPC 구현 계획
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:15:44 +09:00
f36bc0d14e docs(p15): 로비 맵 + 월드 NPC 설계 spec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:55:54 +09:00
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
38 changed files with 19382 additions and 47897 deletions

View File

@@ -24,12 +24,7 @@
"map://map03",
"map://map04",
"map://map05",
"map://map06",
"map://map07",
"map://map08",
"map://map09",
"map://map10",
"map://map11"
"map://lobby"
]
}
],

View File

@@ -0,0 +1,60 @@
{
"Id": "",
"GameId": "",
"EntryKey": "codeblock://lobbymobility",
"ContentType": "x-mod/codeblock",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"CoreVersion": {
"Major": 0,
"Minor": 2
},
"ScriptVersion": {
"Major": 1,
"Minor": 0
},
"Description": "",
"Id": "LobbyMobility",
"Language": 1,
"Name": "LobbyMobility",
"Type": 1,
"Source": 0,
"Target": null,
"Properties": [
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "Tries"
}
],
"Methods": [
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self.Tries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.Tries = self.Tries + 1\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil and lp.PlayerControllerComponent ~= nil then\n\t\tlocal pc = lp.PlayerControllerComponent\n\t\tpc.Enable = true\n\t\tpc.FixedLookAt = 0\n\t\tlocal rb = lp.RigidbodyComponent\n\t\tif rb ~= nil then rb.WalkAcceleration = 0.7 end\n\t\tlocal mv = lp.MovementComponent\n\t\tif mv ~= nil then\n\t\t\tmv.InputSpeed = 1.4\n\t\t\tmv.JumpForce = 1.23\n\t\tend\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.Tries > 50 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "OnBeginPlay"
}
],
"EntityEventHandlers": []
}
}
}

View File

@@ -0,0 +1,89 @@
{
"Id": "",
"GameId": "",
"EntryKey": "codeblock://lobbynpc",
"ContentType": "x-mod/codeblock",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Json",
"Json": {
"CoreVersion": {
"Major": 0,
"Minor": 2
},
"ScriptVersion": {
"Major": 1,
"Minor": 0
},
"Description": "",
"Id": "LobbyNpc",
"Language": 1,
"Name": "LobbyNpc",
"Type": 1,
"Source": 0,
"Target": null,
"Properties": [
{
"Type": "string",
"DefaultValue": "\"\"",
"SyncDirection": 0,
"Attributes": [],
"Name": "NpcId"
},
{
"Type": "string",
"DefaultValue": "\"\"",
"SyncDirection": 0,
"Attributes": [],
"Name": "MarkName"
},
{
"Type": "boolean",
"DefaultValue": "false",
"SyncDirection": 0,
"Attributes": [],
"Name": "InRange"
}
],
"Methods": [
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self.InRange = false\nlocal mark = _EntityService:GetEntityByPath(\"/maps/lobby/\" .. self.MarkName)\nif mark ~= nil then mark:SetVisible(false) end\nself.Entity:ConnectEvent(TouchEvent, function(e)\n\tself:Interact()\nend)\n_InputService:ConnectEvent(KeyDownEvent, function(e)\n\tif self.InRange and e.key == KeyboardKey.UpArrow then\n\t\tself:Interact()\n\tend\nend)\nlocal eventId = 0\nlocal function tick()\n\tlocal lp = _UserService.LocalPlayer\n\tif lp == nil then return end\n\tif mark == nil then mark = _EntityService:GetEntityByPath(\"/maps/lobby/\" .. self.MarkName) end\n\tlocal a = lp.TransformComponent.WorldPosition\n\tlocal b = self.Entity.TransformComponent.WorldPosition\n\tlocal d = Vector2.Distance(Vector2(a.x, a.y), Vector2(b.x, b.y))\n\tlocal near = d < 1.2\n\tif near ~= self.InRange then\n\t\tself.InRange = near\n\t\tif mark ~= nil then mark:SetVisible(near) end\n\tend\nend\neventId = _TimerService:SetTimerRepeat(tick, 0.15)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "OnBeginPlay"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "local c = _EntityService:GetEntityByPath(\"/common\")\nif c ~= nil and c.SlayDeckController ~= nil then\n\tc.SlayDeckController:OnLobbyNpcInteract(self.NpcId)\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "Interact"
}
],
"EntityEventHandlers": []
}
}
}

View File

@@ -47,7 +47,7 @@
"Name": null
},
"Arguments": [],
"Code": "self.LockTries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.LockTries = self.LockTries + 1\n\tlocal pc = nil\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil then\n\t\tpc = lp.PlayerControllerComponent\n\tend\n\tif pc ~= nil then\n\t\tpc.LookDirectionX = 1\n\t\tpc.FixedLookAt = true\n\t\tpc.Enable = false\n\tend\n\tif pc ~= nil then\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.LockTries > 30 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)",
"Code": "self.LockTries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.LockTries = self.LockTries + 1\n\tlocal pc = nil\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil then\n\t\tpc = lp.PlayerControllerComponent\n\tend\n\tif pc ~= nil then\n\t\tpc.LookDirectionX = 1\n\t\tpc.FixedLookAt = true\n\t\tpc.Enable = false\n\tend\n\tif lp ~= nil then\n\t\tif lp.RigidbodyComponent ~= nil then lp.RigidbodyComponent.WalkAcceleration = 0 end\n\t\tif lp.MovementComponent ~= nil then lp.MovementComponent.InputSpeed = 0; lp.MovementComponent.JumpForce = 0 end\n\tend\n\tif pc ~= nil then\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.LockTries > 30 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,41 @@
{
"frames": {
"warrior": { "normal": "4bb57ef88ef449fdaf958f6cf37fe44b", "unique": "4f71c124c8bc4e13b5e9fad392995f68", "legend": "6d741a60c60743cb98ee740a1e2dbfed" },
"magician": { "normal": "d788d09f6f50467ebc67f01dec45f9e2", "unique": "f5def2e8022b4e59a17d3c16414034fe", "legend": "cff71f2e472041ce80c6fbd296f42e2d" },
"bandit": { "normal": "9487b06867bc46269ed1d855420f457f", "unique": "b3081fb2fb1445fa90b12b01481a78ef", "legend": "c357d2daf31a489d95b8fa47e50dd879" }
"warrior": {
"normal": "4bb57ef88ef449fdaf958f6cf37fe44b",
"unique": "4f71c124c8bc4e13b5e9fad392995f68",
"legend": "6d741a60c60743cb98ee740a1e2dbfed"
},
"magician": {
"normal": "d788d09f6f50467ebc67f01dec45f9e2",
"unique": "f5def2e8022b4e59a17d3c16414034fe",
"legend": "cff71f2e472041ce80c6fbd296f42e2d"
},
"bandit": {
"normal": "9487b06867bc46269ed1d855420f457f",
"unique": "b3081fb2fb1445fa90b12b01481a78ef",
"legend": "c357d2daf31a489d95b8fa47e50dd879"
}
},
"classToFrame": {
"warrior": "warrior", "fighter": "warrior", "page": "warrior", "spearman": "warrior",
"magician": "magician", "firepoison": "magician", "icelightning": "magician", "cleric": "magician",
"bandit": "bandit", "shiv": "bandit", "poisoner": "bandit", "trickster": "bandit"
"warrior": "warrior",
"fighter": "warrior",
"page": "warrior",
"spearman": "warrior",
"magician": "magician",
"firepoison": "magician",
"icelightning": "magician",
"cleric": "magician",
"thief": "bandit",
"assassin": "bandit",
"bandit": "bandit",
"curse": "bandit",
"shiv": "bandit",
"poisoner": "bandit",
"trickster": "bandit"
},
"rewardWeights": { "normal": 70, "unique": 25, "legend": 5 }
"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,9 +345,154 @@
"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
},
"SilentStrike": {
"name": "타격",
"cost": 1,
@@ -1086,6 +1243,18 @@
"MagicGuard",
"MagicClaw"
],
"thief": [
"LuckySeven",
"LuckySeven",
"LuckySeven",
"LuckySeven",
"LuckySeven",
"DarkSight",
"DarkSight",
"DarkSight",
"DarkSight",
"DoubleStab"
],
"bandit": [
"SilentStrike",
"SilentStrike",

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,524 @@
# P15 — 로비 맵 + 월드 NPC 구현 계획
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. 설계: `docs/superpowers/specs/2026-06-14-lobby-map-npc-design.md`. 산출물(`map/*.map`,`ui/DefaultGroup.ui`,`*.codeblock`,`Global/*`)은 Read/Edit 금지 — 생성기 소스(`tools/`)만 수정 후 재생성. 검증은 `grep -c`(카운트)와 메이커 플레이테스트.
**Goal:** UI 패널 로비를 폐기하고, 전용 물리 맵 `lobby`에 공식 메이플 NPC 4종을 월드 엔티티로 배치해 근접(↑키)·클릭으로 기능을 열며, 이동·공격 모션은 로비 맵에서만 풀린다.
**Architecture:** 단일 소스(`tools/*` 생성기 + `data/*.json`) → 산출물 재생성. 신규 생성기 2개(`gen-lobby-map.mjs`=맵+NPC 엔티티, `gen-lobby-npc.mjs`=LobbyNpc+LobbyMobility codeblock) + `gen-slaydeck.mjs`(흐름·UI) + `gen-player-lock.mjs`(전투맵 이동 재잠금 보강) 수정. 기존 기능 패널(CharacterSelect/Codex/SoulShop/Board)·전투 흐름 재사용.
**Tech Stack:** Node.js ESM 생성기, MSW Lua(codeblock), MSW MCP(플레이테스트·asset).
**확정 사실(조사):**
- gen-slaydeck 편집 지점: OnBeginPlay `2830-2840`, ShowLobby `2986-2993`, LobbyHud npcs배열 `2469-2474`+버튼루프 `2475-2524`, lobTexts `2433-2439`, Asc버튼 `2454-2468`, BindLobbyButtons `2997-3014`, ShowState `2906-2922`, StartRun `3199-3232`, EndRun `4391-4403`, TeleportToActMap `4373-4385`, PlayerAttackMotion `4491-4500`, guid prefix `244-245`, ACT_MAPS `2745`.
- **1막 텔레포트 공백**: StartRun(`3199-3232`)에 map01 텔레포트가 없음 → `self:TeleportToActMap()` 추가 필요(`RenderPotions` 다음, `ShowMap` 직전). `TeleportToActMap``maps[self.Floor]` 사용 + 가드 `if lp.CurrentMapName==target then return`(멱등).
- **NPC 공식 RUID**(maplestory, 흰박스 위험 없음): 모험가 `122095fd155c4633867b0da4f375bc3c`, 사서 `4c264be6a64f4ac3970b2e6818d04e40`, 상인 `69987ccdc486423f8bedd786bd6cb5d9`, 안내원 `8a99bd87d667482cb1f3b2193f8a19c1`.
- **MSW API**: 월드 클릭 = 엔티티에 `TouchReceiveComponent` + `self.Entity:ConnectEvent(TouchEvent, fn)`. 키 = `_InputService:ConnectEvent(KeyDownEvent, fn)` + `KeyboardKey.UpArrow`(273)/`Space`(32)/`LeftControl`. 거리 = `Vector2.Distance(Vector2(a.x,a.y),Vector2(b.x,b.y))`. 이동복원 = `pc.Enable=true; pc.FixedLookAt=false; mv.InputSpeed=<V>; mv.JumpForce=<J>`(client 공간). 표시토글 = `entity:SetVisible(bool)`.
- **맵 생성 패턴**(gen-maps.mjs): `JSON.parse(readFileSync('map/map01.map'))` → deep clone → 경로 `/maps/map01``/maps/lobby` 치환 → GUID 재발급(+origin fixup) → `compOf(e,'MOD.Core.X')`로 컴포넌트 접근 → `writeFileSync('map/lobby.map', JSON.stringify(map,null,2))`. 배경=`/Background``BackgroundComponent.TemplateRUID`, 타일=`/TileMap``TileMapComponent.TileSetRUID={DataId}`. 컴포넌트 부착=`@components` push + `componentNames` CSV 둘 다. SectorConfig=`Sectors[0].entries``map://lobby` push.
- **codeblock 패턴**(gen-combat-monster.mjs): `prop()/method()` 팩토리 + 봉투(`CoreVersion:'26.5.0.0'`, `EntryKey:'codeblock://x'`) → `writeFileSync('RootDesk/MyDesk/X.codeblock', JSON.stringify(cb,null,2))`. 컨트롤러 호출=`_EntityService:GetEntityByPath("/common").SlayDeckController:Method(...)`. 폴 idiom=`_TimerService:SetTimerRepeat(fn,0.1)`+try카운트 가드+`:ClearTimer(id)`.
---
### Task 0: 메이커 사전 정찰 (이동값·키·바디 컴포넌트·스폰좌표 확정)
**목적:** LobbyMobility의 이동 복원 수치·공격 키·바디 컴포넌트 종류·로비 스폰 좌표를 추측이 아니라 실측으로 확정. 산출물 작성 전 선행.
- [ ] **Step 1:** 메이커가 켜져 있는지 확인하고 현재 빌드 플레이. `mcp__msw-maker-mcp__maker_play``maker_screenshot`로 현재 화면(UI 로비) 확인.
- [ ] **Step 2:** execute_script로 LocalPlayer 컴포넌트·이동값·바디 종류 덤프:
```lua
local lp = _UserService.LocalPlayer
local s = "pc="..tostring(lp.PlayerControllerComponent ~= nil)
local mv = lp.MovementComponent
if mv ~= nil then s = s.." InputSpeed="..tostring(mv.InputSpeed).." JumpForce="..tostring(mv.JumpForce) end
s = s.." Rigidbody="..tostring(lp.RigidbodyComponent ~= nil)
s = s.." Sideviewbody="..tostring(lp.SideviewbodyComponent ~= nil)
local p = lp.TransformComponent.WorldPosition
s = s.." pos=("..tostring(p.x)..","..tostring(p.y)..","..tostring(p.z)..")"
s = s.." map="..tostring(lp.CurrentMapName)
log(s)
return s
```
Run via `maker_execute_script`. 기대: 현재 InputSpeed/JumpForce(0일 것), 어떤 바디 컴포넌트가 존재하는지(Rigidbody vs Sideviewbody), 현재 맵 이름·좌표.
- [ ] **Step 3:** 이동 복원값 실측 — execute_script로 직접 켜 보고 걸어지는지 확인:
```lua
local lp = _UserService.LocalPlayer
lp.PlayerControllerComponent.Enable = true
lp.PlayerControllerComponent.FixedLookAt = false
lp.MovementComponent.InputSpeed = 5
lp.MovementComponent.JumpForce = 5
return "applied: try walking with arrow keys"
```
`maker_keyboard_input`로 방향키를 눌러 실제 이동 여부 확인(screenshot 비교). 걸으면 InputSpeed 값 후보 = 5. 안 걸으면 RigidbodyComponent.WalkSpeed/WalkJump 등도 set해보고(아래) 동작하는 최소 set을 기록.
```lua
local rb = _UserService.LocalPlayer.RigidbodyComponent
if rb ~= nil then rb.Enable = true end
```
- [ ] **Step 4:** 공격 키 enum 확정 — `mlua_api_retriever`(이미 검증됨: UpArrow=273, Space=32)에서 공격용 키 `LeftControl`의 정확한 enum 멤버명 확인(예: `KeyboardKey.LeftControl`). 확인 안 되면 공격 키를 `KeyboardKey.Space`로 폴백(이동 점프는 MSW 기본 Alt 가정).
- [ ] **Step 5:** 결정 기록 — 이 plan 파일 하단 "정찰 결과" 섹션에 확정값 적기:
- `WALK_SPEED` = (Step3에서 걸어진 InputSpeed), `JUMP_FORCE` = (걸어진 JumpForce), `BODY_KIND` = Rigidbody|Sideviewbody|none, 추가 바디 set 필요 여부, `ATTACK_KEY` = LeftControl|Space, `LOBBY_SPAWN` = 적당한 지면 좌표(현재 map 좌표 참고, 예 `Vector3(0, 0.03, 0)`).
- 이후 Task에서 이 값을 JS 상수로 사용.
- [ ] **Step 6:** `maker_stop`으로 플레이 종료(상태 churn 방지).
---
### Task 1: `gen-lobby-map.mjs` — 로비 맵 + NPC 엔티티 생성
**Files:**
- Create: `tools/map/gen-lobby-map.mjs`
- Output(산출물, 직접 편집 금지): `map/lobby.map`, `Global/SectorConfig.config`(갱신)
NPC 4종 + `!` 마크 4종을 월드 엔티티로 배치. 마크는 자식이 아니라 **형제 엔티티**(NPC 위 고정 위치, 정적이라 무방). 각 NPC에 `TouchReceiveComponent` + `script.LobbyNpc`(NpcId), 맵 루트에 `script.LobbyMobility` 부착.
- [ ] **Step 1:** `tools/map/gen-maps.mjs`를 참고 헤더로 새 파일 생성. 상수:
```js
import { readFileSync, writeFileSync } from 'node:fs';
const TEMPLATE = 'map/map01.map';
const OUT = 'map/lobby.map';
const SECTOR = 'Global/SectorConfig.config';
const TOWN_BG = '<gen-maps.mjs BACKGROUNDS 풀에서 타운(헤네시스 등) RUID 1개 복사>'; // Task1 Step2에서 확정
const NPCS = [
{ name: 'NpcRun', id: 'run', x: -4.5, ruid: '122095fd155c4633867b0da4f375bc3c' },
{ name: 'NpcCodex', id: 'codex', x: -1.5, ruid: '4c264be6a64f4ac3970b2e6818d04e40' },
{ name: 'NpcShop', id: 'shop', x: 1.5, ruid: '69987ccdc486423f8bedd786bd6cb5d9' },
{ name: 'NpcBoard', id: 'board', x: 4.5, ruid: '8a99bd87d667482cb1f3b2193f8a19c1' },
];
const MARK_RUID = '<Task1 Step2: asset_search로 "!" 말풍선/느낌표 공식 스프라이트 RUID, 못찾으면 NPC와 구분되는 작은 공식 스프라이트>';
const NPC_Y = 0.0; // 지면 (Task0 좌표 참고로 조정)
const MARK_DY = 1.6; // NPC 머리 위 오프셋
function compOf(e, type) { return e.jsonString['@components'].find((c) => c['@type'] === type); }
function lobbyGuid(idx) {
const n = (900000 + idx) >>> 0; // 기존 생성기와 충돌 없는 고유 오프셋
return `${n.toString(16).padStart(8,'0')}-0000-4000-8000-${n.toString(16).padStart(12,'0')}`;
}
```
- [ ] **Step 2:** TOWN_BG·MARK_RUID 확정 — `gen-maps.mjs`를 열어 `BACKGROUNDS` 배열에서 타운 느낌 RUID 하나 골라 `TOWN_BG`에 박는다. MARK_RUID는 메이커 MCP `asset_search_resources`(source=maplestory, query "느낌표"/"balloon"/"emotion")로 1개 확정(못 찾으면 `!` 대신 작은 화살표/별 공식 스프라이트, 최후엔 NPC RUID 재사용+tint).
- [ ] **Step 3:** 맵 로드·클론·정리(몬스터 제거)·배경:
```js
const map = JSON.parse(JSON.stringify(JSON.parse(readFileSync(TEMPLATE, 'utf8'))));
map.EntryKey = 'map://lobby';
let ents = map.ContentProto.Entities;
const isMonster = (e) => typeof e.componentNames === 'string' && (e.componentNames.includes('script.Monster') || e.componentNames.includes('script.CombatMonster'));
// 경로/이름 치환
for (const e of ents) {
if (typeof e.path === 'string') e.path = e.path.replace('/maps/map01', '/maps/lobby');
if (e.jsonString) {
if (typeof e.jsonString.path === 'string') e.jsonString.path = e.jsonString.path.replace('/maps/map01', '/maps/lobby');
if (e.jsonString.name === 'map01') e.jsonString.name = 'lobby';
}
if ((e.path || '').endsWith('/Background')) { const bg = compOf(e, 'MOD.Core.BackgroundComponent'); if (bg) bg.TemplateRUID = TOWN_BG; }
}
// 몬스터 엔티티 제거 + PlayerLock/MapCamera는 유지(로비엔 PlayerLock 불필요하니 루트에서 제거)
ents = ents.filter((e) => !isMonster(e));
const root = ents.find((e) => e.path === '/maps/lobby');
if (!root) throw new Error('[gen-lobby-map] 맵 루트 없음');
// 로비엔 PlayerLock 컴포넌트가 있으면 제거(이동 잠금 방지)
root.jsonString['@components'] = root.jsonString['@components'].filter((c) => c['@type'] !== 'script.PlayerLock');
{ const names = (root.componentNames || '').split(',').filter((s) => s && s !== 'script.PlayerLock'); root.componentNames = names.join(','); }
```
- [ ] **Step 4:** NPC 엔티티 + 마크 엔티티 생성(몬스터 템플릿을 클론해 몬스터 컴포넌트 제거 후 재사용). 몬스터 템플릿은 클론 전에 원본 ents(`map.ContentProto.Entities`)에서 확보:
```js
const orig = JSON.parse(readFileSync(TEMPLATE, 'utf8')).ContentProto.Entities;
const tmpl = orig.find((e) => typeof e.componentNames === 'string' && e.componentNames.includes('script.Monster'));
if (!tmpl) throw new Error('[gen-lobby-map] 몬스터 템플릿(스프라이트 엔티티) 없음');
let gi = 1;
function makeSpriteEntity(name, x, y, ruid, extraComps, extraNames, visible) {
const m = JSON.parse(JSON.stringify(tmpl));
m.id = lobbyGuid(gi++);
m.path = `/maps/lobby/${name}`;
m.jsonString.path = m.path;
m.jsonString.name = name;
const o = m.jsonString.origin; if (o) { if (o.root_entity_id) o.root_entity_id = m.id; if (o.sub_entity_id) o.sub_entity_id = m.id; }
const tr = compOf(m, 'MOD.Core.TransformComponent'); if (tr) { tr.Position.x = x; tr.Position.y = y; }
const sp = compOf(m, 'MOD.Core.SpriteRendererComponent'); if (sp) sp.SpriteRUID = ruid;
// 몬스터/전투 컴포넌트 전부 제거
m.jsonString['@components'] = m.jsonString['@components'].filter((c) => !['script.Monster','script.CombatMonster'].includes(c['@type']));
let names = (m.componentNames || '').split(',').filter((s) => s && !['script.Monster','script.CombatMonster'].includes(s));
// StateAnimationComponent가 있으면 die/hit 시트 제거(정적 stand)
for (const [comp, props] of extraComps) { m.jsonString['@components'].push({ '@type': comp, Enable: true, ...props }); names.push(comp); }
names = names.concat(extraNames).filter(Boolean);
m.componentNames = names.join(',');
// 마크 숨김은 Enable=false 금지(SetVisible가 안 먹음). codeblock OnBeginPlay가 SetVisible(false)로 숨기므로
// 여기선 별도 처리 안 함. (한 프레임 깜빡임 우려 시 SpriteRendererComponent.Visible=false 시도 — 필드 확인 후.)
void visible;
return m;
}
const added = [];
for (const npc of NPCS) {
// NPC: TouchReceiveComponent(자동맞춤) + script.LobbyNpc(NpcId)
added.push(makeSpriteEntity(npc.name, npc.x, NPC_Y, npc.ruid,
[['MOD.Core.TouchReceiveComponent', { AutoFitToSize: true }], ['script.LobbyNpc', { NpcId: npc.id, Tries: 0, InRange: false, MarkName: npc.name + 'Mark' }]],
['MOD.Core.TouchReceiveComponent', 'script.LobbyNpc'], true));
// 마크: NPC 위, 기본 숨김
added.push(makeSpriteEntity(npc.name + 'Mark', npc.x, NPC_Y + MARK_DY, MARK_RUID, [], [], false));
}
ents = ents.concat(added);
```
> 주: `script.LobbyNpc` props(NpcId/MarkName 등)는 Task2의 codeblock 속성 정의와 **이름이 정확히 일치**해야 한다.
- [ ] **Step 5:** 맵 루트에 `script.LobbyMobility` 부착 + 쓰기 + SectorConfig 등록:
```js
root.jsonString['@components'] = root.jsonString['@components'].filter((c) => c['@type'] !== 'script.LobbyMobility');
root.jsonString['@components'].push({ '@type': 'script.LobbyMobility', Enable: true, Tries: 0 });
{ const names = (root.componentNames || '').split(',').filter((s) => s && s !== 'script.LobbyMobility'); names.push('script.LobbyMobility'); root.componentNames = names.join(','); }
map.ContentProto.Entities = ents;
writeFileSync(OUT, JSON.stringify(map, null, 2), 'utf8');
// SectorConfig: map://lobby 등록(멱등) + 시작 섹터를 lobby로
const sector = JSON.parse(readFileSync(SECTOR, 'utf8'));
const sec0 = sector.ContentProto.Json.Sectors[0];
if (!sec0.entries.includes('map://lobby')) sec0.entries.push('map://lobby');
writeFileSync(SECTOR, JSON.stringify(sector, null, 2), 'utf8');
console.log('[gen-lobby-map] lobby.map 생성 + SectorConfig 등록 완료');
```
- [ ] **Step 6:** 실행 + 카운트 검증(내용 출력 금지):
```bash
node tools/map/gen-lobby-map.mjs
grep -c "script.LobbyNpc" map/lobby.map # 4 기대
grep -c "script.LobbyMobility" map/lobby.map # 1 기대
grep -c "TouchReceiveComponent" map/lobby.map # 4(+ 템플릿 잔존 가능) 기대
grep -lc "map://lobby" Global/SectorConfig.config
node tools/verify/count.mjs 2>/dev/null || true
```
기대: LobbyNpc=4, LobbyMobility=1. 어긋나면 생성기 수정.
- [ ] **Step 7:** 커밋:
```bash
git add tools/map/gen-lobby-map.mjs map/lobby.map Global/SectorConfig.config
git commit -m "feat(lobby): 로비 전용 맵 + NPC 4종 월드 엔티티 생성기 (P15)"
```
---
### Task 2: `gen-lobby-npc.mjs` — LobbyNpc + LobbyMobility codeblock
**Files:**
- Create: `tools/player/gen-lobby-npc.mjs`
- Output(산출물): `RootDesk/MyDesk/LobbyNpc.codeblock`, `RootDesk/MyDesk/LobbyMobility.codeblock`
`gen-combat-monster.mjs``prop()/method()`/봉투 패턴을 그대로 복사. **Lua 문자열은 실제 탭 들여쓰기 사용**(RULES.md 메모리: 실탭↔`\t` 혼재 금지 — 템플릿 리터럴 안 실제 탭).
- [ ] **Step 1:** 헤더·팩토리(gen-combat-monster.mjs:9-17 복사) + 봉투 함수:
```js
import { writeFileSync } from 'node:fs';
const WALK_SPEED = /* Task0 정찰값 */ 5;
const JUMP_FORCE = /* Task0 정찰값 */ 5;
const ATTACK_KEY = /* Task0: 'LeftControl' 또는 'Space' */ 'LeftControl';
function prop(Type, Name, DefaultValue = 'nil') { return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name }; }
function method(Name, Code, Arguments = [], ExecSpace = 6) {
return { Return: { Type: 'void', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null }, Arguments, Code, Scope: 2, ExecSpace, Attributes: [], Name };
}
function writeCodeblock(id, name, properties, methods) {
const cb = { Id: '', GameId: '', EntryKey: `codeblock://${id.toLowerCase()}`, ContentType: 'x-mod/codeblock', Content: '', Usage: 0, UsePublish: 1, UseService: 0, CoreVersion: '26.5.0.0', StudioVersion: '', DynamicLoading: 0,
ContentProto: { Use: 'Json', Json: { CoreVersion: { Major: 0, Minor: 2 }, ScriptVersion: { Major: 1, Minor: 0 }, Description: '', Id: name, Language: 1, Name: name, Type: 1, Source: 0, Target: null, Properties: properties, Methods: methods, EntityEventHandlers: [] } } };
writeFileSync(`RootDesk/MyDesk/${name}.codeblock`, JSON.stringify(cb, null, 2), 'utf8');
}
```
- [ ] **Step 2:** LobbyNpc codeblock — 근접 폴링 + 마크 토글 + Touch/Key → Interact. (아래 Lua의 들여쓰기는 실제 탭으로 입력)
```js
const npcInteract = method('Interact', `local c = _EntityService:GetEntityByPath("/common")
if c ~= nil and c.SlayDeckController ~= nil then
c.SlayDeckController:OnLobbyNpcInteract(self.NpcId)
end`);
const npcBegin = method('OnBeginPlay', `self.Tries = 0
self.InRange = false
local mark = _EntityService:GetEntityByPath("/maps/lobby/" .. self.MarkName)
if mark ~= nil then mark:SetVisible(false) end
self.Entity:ConnectEvent(TouchEvent, function(e) self:Interact() end)
_InputService:ConnectEvent(KeyDownEvent, function(e)
if self.InRange and e.key == KeyboardKey.UpArrow then self:Interact() end
end)
local eventId = 0
local function tick()
local lp = _UserService.LocalPlayer
if lp == nil then return end
local a = lp.TransformComponent.WorldPosition
local b = self.Entity.TransformComponent.WorldPosition
local d = Vector2.Distance(Vector2(a.x, a.y), Vector2(b.x, b.y))
local near = d < 1.8
if near ~= self.InRange then
self.InRange = near
if mark ~= nil then mark:SetVisible(near) end
end
end
eventId = _TimerService:SetTimerRepeat(tick, 0.15)`);
writeCodeblock('LobbyNpc', 'LobbyNpc', [
prop('string', 'NpcId', '""'),
prop('string', 'MarkName', '""'),
prop('boolean', 'InRange', 'false'),
prop('number', 'Tries', '0'),
], [npcBegin, npcInteract]);
```
- [ ] **Step 3:** LobbyMobility codeblock — 이동 복원 + 공격 키. (들여쓰기 실제 탭)
```js
const mobBegin = method('OnBeginPlay', `self.Tries = 0
local eventId = 0
local function apply()
self.Tries = self.Tries + 1
local lp = _UserService.LocalPlayer
if lp ~= nil and lp.PlayerControllerComponent ~= nil then
local pc = lp.PlayerControllerComponent
pc.Enable = true
pc.FixedLookAt = false
local mv = lp.MovementComponent
if mv ~= nil then
mv.InputSpeed = ${WALK_SPEED}
mv.JumpForce = ${JUMP_FORCE}
end
local rb = lp.RigidbodyComponent
if rb ~= nil then rb.Enable = true end
_TimerService:ClearTimer(eventId)
elseif self.Tries > 50 then
_TimerService:ClearTimer(eventId)
end
end
eventId = _TimerService:SetTimerRepeat(apply, 0.1)
_InputService:ConnectEvent(KeyDownEvent, function(e)
if e.key == KeyboardKey.${ATTACK_KEY} then
local c = _EntityService:GetEntityByPath("/common")
if c ~= nil and c.SlayDeckController ~= nil then
c.SlayDeckController:PlayerAttackMotion()
end
end
end)`);
writeCodeblock('LobbyMobility', 'LobbyMobility', [prop('number', 'Tries', '0')], [mobBegin]);
console.log('[gen-lobby-npc] LobbyNpc/LobbyMobility codeblock 생성 완료');
```
- [ ] **Step 4:** 실행 + 카운트 검증:
```bash
node tools/player/gen-lobby-npc.mjs
grep -c "OnLobbyNpcInteract" RootDesk/MyDesk/LobbyNpc.codeblock # >=1
grep -c "PlayerAttackMotion" RootDesk/MyDesk/LobbyMobility.codeblock # >=1
ls -la RootDesk/MyDesk/LobbyNpc.codeblock RootDesk/MyDesk/LobbyMobility.codeblock
```
- [ ] **Step 5:** 커밋:
```bash
git add tools/player/gen-lobby-npc.mjs RootDesk/MyDesk/LobbyNpc.codeblock RootDesk/MyDesk/LobbyMobility.codeblock
git commit -m "feat(lobby): LobbyNpc(근접·클릭 상호작용)·LobbyMobility(이동·공격 해제) codeblock (P15)"
```
---
### Task 3: `gen-player-lock.mjs` — 전투맵 이동 재잠금 보강 (방어)
**Files:** Modify `tools/player/gen-player-lock.mjs`
로비에서 푼 이동이 텔레포트 후 전투맵에 누설돼도, 전투맵 PlayerLock이 런타임으로 MovementComponent를 0으로 재설정해 확실히 잠그도록 보강.
- [ ] **Step 1:** `gen-player-lock.mjs`의 PlayerLock Lua에서 `pc.Enable = false` 직후 라인을 추가(생성기 내 해당 Lua 템플릿 리터럴, 실제 탭 들여쓰기):
```lua
pc.Enable = false
local mv = lp.MovementComponent
if mv ~= nil then mv.InputSpeed = 0; mv.JumpForce = 0 end
```
(정확한 삽입 지점은 `gen-player-lock.mjs`에서 `pc.Enable`가 들어간 Lua 문자열. `LocalPlayer.PlayerControllerComponent``lp`로 잡는 변수명이 기존 코드와 일치하는지 확인 — 다르면 기존 변수명 사용.)
- [ ] **Step 2:** 재생성 + 카운트:
```bash
node tools/player/gen-player-lock.mjs
grep -c "InputSpeed = 0" RootDesk/MyDesk/PlayerLock.codeblock # >=1 기대(파일명은 생성기 출력명 확인)
```
- [ ] **Step 3:** 커밋:
```bash
git add tools/player/gen-player-lock.mjs RootDesk/MyDesk/PlayerLock.codeblock map/map0*.map
git commit -m "fix(lobby): 전투맵 PlayerLock에 이동값 런타임 0 재설정 보강 (P15)"
```
---
### Task 4: `gen-slaydeck.mjs` — 흐름·UI 통합
**Files:** Modify `tools/deck/gen-slaydeck.mjs`
- [ ] **Step 1:** guid prefix 등록(`244-245`) — 신규 prefix 불필요(LobbyHud 슬림화만, 기존 `lob` 재사용). 확인만.
- [ ] **Step 2:** ACT_MAPS 아래(`2745`)에 로비 상수 추가:
```js
const ACT_MAPS = ['map01', 'map02', 'map03', 'map04', 'map05'];
const LOBBY_MAP = 'lobby';
const LOBBY_SPAWN = 'Vector3(0, 0.03, 0)'; // Task0 정찰 좌표로 조정
```
- [ ] **Step 3:** LobbyHud 슬림화 — `npcs` 배열(`2469-2474`)과 버튼 생성 루프(`2475-2524`) **삭제**. `lobTexts`(`2433-2439`)는 SoulLabel/AscLabel + 안내문(Hint)만 남기고 Title/Subtitle은 "마을" 정도로 축소 or 제거. AscMinus/AscPlus(`2454-2468`)는 유지. → LobbyHud가 상단 정보바(영혼/승천)만 남음.
- [ ] **Step 4:** BindLobbyButtons(`2997-3014`) — NPC 4개 `bindClick` 라인 **삭제**(NpcRun/NpcCodex/NpcShop/NpcBoard). AscMinus/AscPlus/BoardHud.Close/SoulShopHud.Close bindClick은 유지.
- [ ] **Step 5:** ShowLobby(`2986-2993`) — 끝에 로비 맵 텔레포트 추가:
```js
method('ShowLobby', `self.SelectedClass = ""
self:RenderAscension()
self:RenderSoulLabel()
self:ShowState("lobby")
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)
self:BindLobbyButtons()
self:BindMenuButtons()
self:GoLobbyMap()`),
```
- [ ] **Step 6:** 신규 method `GoLobbyMap`(ShowLobby 근처에 추가, ExecSpace 기본):
```js
method('GoLobbyMap', `self.LobbyTpTries = 0
local eventId = 0
local function go()
self.LobbyTpTries = self.LobbyTpTries + 1
local lp = _UserService.LocalPlayer
if lp ~= nil then
if lp.CurrentMapName ~= "${LOBBY_MAP}" then
_TeleportService:TeleportToMapPosition(lp, ${LOBBY_SPAWN}, "${LOBBY_MAP}")
end
_TimerService:ClearTimer(eventId)
elseif self.LobbyTpTries > 50 then
_TimerService:ClearTimer(eventId)
end
end
eventId = _TimerService:SetTimerRepeat(go, 0.1)`),
```
- [ ] **Step 7:** 신규 method `OnLobbyNpcInteract`(인자 id) — NPC codeblock이 호출:
```js
method('OnLobbyNpcInteract', `if self.RunActive == true then return end
if id == "run" then
self:ShowCharacterSelect()
elseif id == "codex" then
self:ShowCodex()
elseif id == "shop" then
self:ShowSoulShop()
elseif id == "board" then
self:ShowBoard()
end`, [{ Type: 'string', DefaultValue: '""', SyncDirection: 0, Attributes: [], Name: 'id' }]),
```
(인자 객체 형태는 기존 `EndRun``text` 인자/`ShowState``state` 인자 정의를 참고해 동일 구조로.)
- [ ] **Step 8:** StartRun(`3199-3232`) — `RenderPotions()` 다음, `ShowMap()` 직전에 1막 텔레포트 추가:
```js
// ... self:RenderPotions() (기존) 다음 줄에
self:TeleportToActMap()
// ... self:ShowMap() (기존)
```
(StartRun의 Lua 문자열 내부에 `self:TeleportToActMap()` 한 줄 삽입. Floor=1이 이미 세팅돼 map01 타깃.)
- [ ] **Step 9:** EndRun(`4391-4403`) 복귀 — 기존 타이머 `self:ShowLobby()`가 GoLobbyMap을 호출하므로 **별도 변경 불필요**(ShowLobby가 로비 맵 텔레포트 포함). 확인만.
- [ ] **Step 10:** 재생성 + 카운트 검증:
```bash
node tools/deck/gen-slaydeck.mjs
grep -c "OnLobbyNpcInteract" RootDesk/MyDesk/SlayDeckController.codeblock # >=1 (이 파일엔 정의만; 호출은 LobbyNpc.codeblock)
grep -c "GoLobbyMap" RootDesk/MyDesk/SlayDeckController.codeblock # >=2 (정의+ShowLobby 호출)
grep -c "TeleportToActMap" RootDesk/MyDesk/SlayDeckController.codeblock # >=3 (정의+ContinueAfterBoss+StartRun)
grep -c "NpcRun" ui/DefaultGroup.ui # 0 기대(버튼-행 제거됨)
```
- [ ] **Step 11:** 커밋:
```bash
git add tools/deck/gen-slaydeck.mjs ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock Global/common.gamelogic map/lobby.map map/map0*.map
git commit -m "feat(lobby): 로비 맵 흐름 통합 — OnBeginPlay/EndRun 텔레포트·NPC 상호작용 디스패치·StartRun map01 텔레포트·LobbyHud 슬림화 (P15)"
```
---
### Task 5: 미러/회귀 테스트
전투 규칙·맵 그래프 알고리즘 미변경 → 미러 동기화 불필요. 기존 테스트 회귀만 확인.
- [ ] **Step 1:** 기존 테스트 실행:
```bash
node --test tools/balance/sim-balance.test.mjs
node --test tools/map/rogue-map.test.mjs
```
기대: 전부 PASS(이번 변경은 전투/맵그래프 무관이라 회귀 없어야 함).
- [ ] **Step 2:** `git status --short`로 의도치 않은 산출물 변경 없는지 확인(산출물 diff는 보지 않음).
---
### Task 6: 메이커 플레이테스트 검증
- [ ] **Step 1:** git 상태 정리 후 메이커에서 **로컬 워크스페이스 refresh**(RULES.md §5 — 안 하면 stale 상태가 디스크 덮어씀). `maker_refresh_workspace` → 빌드 콘솔 0 에러 확인(`maker_logs`).
- [ ] **Step 2:** `maker_play``maker_screenshot`. 검증 시나리오(스크린샷·로그로):
1. 월드 시작 → **로비 맵에 스폰**(타운 배경, NPC 4명 보임), 방향키로 **이동됨**, 공격 키로 **공격 모션** 나옴.
2. NPC 근접 → 머리 위 `!` 표시 → `↑`키로 기능 패널 오픈. NPC `maker_mouse_input` 클릭으로도 오픈(버튼 클릭 불가 메모리 주의 — 월드 엔티티 TouchEvent라 mouse_input 좌표 클릭 시도, 안 되면 ↑키 경로로 검증).
3. 모험가→직업선택→런 시작 → **map01로 텔레포트**, 이동/공격 **잠김**. 1막 전투 몬스터 정상 등장(CurrentMapName 필터 통과).
4. 사서→도감, 상인→영혼상점, 안내원→게시판 각각 오픈/닫기.
5. 런 종료(빠른 패배 유도: execute_script로 `c.Combat.PlayerHp=0` 등 or 정상 진행) → 4초 후 **로비 맵 복귀**, 이동/공격 재해제.
6. 상단 미니 HUD에 영혼/승천 표시 정상.
- [ ] **Step 2b:** 실패 시 디버깅 — 이동 안 됨→Task0 값 재확認/RigidbodyComponent 추가 set, 클릭 안 됨→TouchReceiveComponent 필드/근접↑키 폴백, 몬스터 안 나옴→StartRun 텔레포트·spawn 좌표 확인. 생성기 수정→재생성→refresh→재플레이.
- [ ] **Step 3:** `maker_stop`. 스크린샷을 사용자에게 공유.
---
### Task 7: PR
- [ ] **Step 1:** push:
```bash
git push -u origin feature/p15-lobby-map-npc
```
- [ ] **Step 2:** PR spec JSON(UTF-8) 작성 후 `node tools/git/gitea-pr.mjs create <spec.json>` (RULES.md §4 — 인라인 curl 한글 금지). 제목 예: "feat: P15 — 로비 맵 + 월드 NPC(근접·클릭) + 로비 전용 이동·공격". 본문에 변경 요약·검증 결과·스크린샷 언급.
- [ ] **Step 3:** 사용자에게 PR 번호 보고 + 머지 여부 확인.
---
## 정찰 결과 (Task0 실측 완료)
- **이동 레버 = `RigidbodyComponent.WalkAcceleration` (freeze가 0으로 만든 값). 복원값 0.7로 이동·점프 정상 확인** (InputSpeed/JumpForce는 무관 — WalkSpeed=1.4·WalkJump=1.23는 freeze가 안 건드림).
- 이동 해제 = `pc.Enable=true; pc.FixedLookAt=false; rb.WalkAcceleration=0.7` (rb.Enable는 이미 true).
- BODY_KIND = Rigidbody가 구동(Sideviewbody도 존재하나 WalkSpeed=nil). 추가 바디 set 불필요.
- ATTACK_KEY = `LeftControl` (KeyboardKey.LeftControl 유효, PlayerAttackMotion() 호출 정상).
- 상호작용 키 = `UpArrow` 유효. 클릭 = TouchReceiveComponent+TouchEvent.
- 현재 플레이어 위치 map01 (-5,-0.039,0) → LOBBY_SPAWN = `Vector3(-5, 0.03, 0)`. NPC x = -3 / -0.5 / 2 / 4.5, 근접 임계 1.2.
- TOWN_BG = Task1에서 gen-maps BACKGROUNDS 풀에서 선택, MARK_RUID = Task1 asset 검색.

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로 공존.

View File

@@ -0,0 +1,104 @@
# 로비 맵 + 월드 NPC 설계 (P15)
작성일: 2026-06-14
브랜치: `feature/p15-lobby-map-npc`
## 목표
기존 **UI 패널 로비**(P14-8 `LobbyHud` — 색칠된 `UIButton` 4개 행)를 폐기하고,
**전용 로비 맵**에 **월드 NPC 엔티티 4종**을 배치한다. NPC를 누르면 각 기능이 실행되며,
플레이어 **이동·공격 모션은 로비 맵에서만** 풀린다(전투/런 맵에서는 기존대로 잠김 유지).
요청 원문: "로비를 UI로 만들지 말고, map을 추가해서 로비에 각각 기능을 할 수 있는 npc를 추가하고,
그 npc를 누르면 각 기능을 할 수 있도록 추가. 플레이어는 반드시 로비맵에서만 이동과 공격 모션을 풀어줘."
## 확정된 결정 (브레인스토밍)
| 항목 | 결정 |
|---|---|
| NPC 상호작용 | **근접 프롬프트+키(Up/Space) AND 직접 클릭** 둘 다 지원 |
| NPC 기능 | **기존 4종 유지** + 외형을 **정식 maplestory NPC 스프라이트(공식 RUID)** 로 교체 |
| 로비 맵 | **새 전용 로비 맵 추가**(`map/lobby.map`), 런 맵(map01~) 인덱스 불변 |
## 현재 상태 (조사 결과)
- **로비 = UI 패널** `LobbyHud`. NPC 4종은 색칠 `UIButton`, 전부 `tools/deck/gen-slaydeck.mjs`에 하드코딩.
- `NpcRun`(모험가→런 시작), `NpcCodex`(사서→카드 도감), `NpcShop`(상인→영혼 상점), `NpcBoard`(안내원→게시판).
- 정의 ~`gen-slaydeck.mjs:2469-2487`, 클릭 바인딩 `BindLobbyButtons()` ~`2997-3014`.
- 기능 진입 메서드: `ShowCharacterSelect()`(~3147), `ShowCodex()`, `ShowSoulShop()`, `ShowBoard()`**재사용 대상**.
- **이동/공격 이중 잠금**:
- `tools/player/freeze-turn-player.mjs``Global/DefaultPlayer.model` speed/jump/accel = 0 (**전역**).
- `tools/player/gen-player-lock.mjs``PlayerLock` codeblock(`pc.Enable=false`, `FixedLookAt=true`)를 map01~05에 주입.
- 공격은 순수 모션 `PlayerAttackMotion()`(StateComponent ATTACK→IDLE), 필드 몬스터와 무관(전투는 데이터 배열 UI 전투).
- 플레이어 스폰: `TeleportToActMap()``Vector3(-6, 0.03, 0)`, Floor별 map 선택.
- **맵 파이프라인**: map01 = 저작 템플릿, map02~05 = 복제 생성. 포털 없이 `TeleportToMapPosition` 전환.
- `Global/SectorConfig.config`의 valid 목록에 맵 등록(현재 gen-maps.mjs가 갱신).
- 컴포넌트 부착 패턴: `gen-combat-monster.mjs`가 맵 몬스터에 `script.CombatMonster`를 붙이고, 해당 codeblock이 OnBeginPlay에서 `/common` 컨트롤러에 자가등록.
- **흐름**: `OnBeginPlay``ShowLobby()`(UI). `EndRun(text)`→4초 후 `ShowLobby()`.
## 접근법
**A. 새 로비 맵 + 월드 NPC 엔티티 (채택)** — 맵 템플릿 복제 재사용, NPC를 월드 엔티티로 배치하고
각 NPC의 codeblock이 근접+클릭을 감지해 **기존 기능 패널**을 띄움. 이동/공격 해제는 로비 맵 전용 codeblock.
전투맵은 손대지 않아 잠금 유지. (B: 몬스터 배치기 재활용 → 로직 혼재로 비채택. C: 화면고정 UI 버튼 → 거부된 "UI 로비"라 제외.)
## 상세 설계
### 1) 로비 맵 — `tools/map/gen-lobby-map.mjs` → `map/lobby.map`
- map01 템플릿 deep clone → 경로 `/maps/lobby`로 치환, GUID 재발급(결정론 시드).
- 마을/타운 배경 RUID + 타일셋 적용. **`script.Monster`/`script.CombatMonster` 엔티티 전부 제거**(전투 없음).
- NPC 4종을 x축으로 벌려 월드 엔티티로 배치. 각 NPC:
- 스프라이트 = 공식 maplestory NPC RUID(계정 업로드 금지 — 흰 박스). 구현 단계에서 asset 검색으로 4개 확정.
- `script.LobbyNpc` 컴포넌트 + `NpcId`(`run`/`codex`/`shop`/`board`) + 머리 위 이름/`!` 프롬프트용 텍스트 노드.
- 플레이어 스폰 지점(맵 중앙-좌측).
- `Global/SectorConfig.config` valid 목록에 `map://lobby` 추가 — **SectorConfig 단일 소유자는 gen-maps.mjs**로 유지하고 lobby 항목을 그 상수에 포함(두 생성기 충돌 방지).
### 2) NPC 상호작용 — `tools/player/gen-lobby-npc.mjs` → `LobbyNpc` codeblock
- **근접+키**: 매 틱(타이머) 로컬 플레이어와의 x거리 측정 → 임계 거리 내면 `!` 프롬프트 노드 활성 + `Up`/`Space` 입력 시 트리거.
- **직접 클릭**: NPC 엔티티 클릭/터치 → 동일 트리거. (MSW 월드 엔티티 클릭 API는 구현 시 `mlua_api_retriever`로 확정: 엔티티 TouchEvent vs 스크린 오버레이 버튼 중 검증된 방식.)
- 트리거 시 `_EntityService:GetEntityByPath("/common").SlayDeckController:OnLobbyNpcInteract(NpcId)` 호출(경로 기반 크로스 codeblock — CombatMonster 자가등록과 동일 패턴).
- 한 번에 하나만 상호작용(다른 패널 열려 있으면 무시).
### 3) 이동·공격 잠금 해제 (로비 맵 한정) — `LobbyMobility` codeblock
- **`map/lobby.map`에만** 주입(전투맵 PlayerLock/전역 freeze는 불변).
- OnBeginPlay 런타임 복원: 로컬 플레이어 `MovementComponent`(InputSpeed/JumpForce) 정상값, `PlayerController.Enable=true`, `FixedLookAt=false`.
- 공격 입력(키/클릭) → 기존 `PlayerAttackMotion()`(코스메틱) 바인딩. **필드 타격 없음**.
- 전투맵 텔레포트 시 모델 기본값(speed=0)+PlayerLock 재적용 → **"로비맵에서만"을 구조적으로 보장**.
- 런타임 이동/공격 복원 정확한 API는 구현 단계에서 `mlua_api_retriever`로 확정.
- 생성기 배치는 `gen-lobby-npc.mjs`에 함께 둘지 별도 `gen-lobby-unlock.mjs`로 분리할지는 계획에서 결정(둘 다 lobby 맵 전용 codeblock).
### 4) 흐름 통합 — `tools/deck/gen-slaydeck.mjs`
- **OnBeginPlay**: `ShowLobby()`(UI) → **로비 맵 텔레포트** + 경량 "lobby" 상태(전투/상점/맵 HUD 숨김).
- **EndRun**: 4초 후 `ShowLobby()`**로비 맵 텔레포트 복귀**.
- **OnLobbyNpcInteract(id)** 신규: `run``ShowCharacterSelect()`, `codex``ShowCodex()`, `shop``ShowSoulShop()`, `board``ShowBoard()`(전부 기존 메서드 재사용, 패널은 로비 맵 위 팝업).
- **제거**: `LobbyHud` 버튼-행 허브 패널 + `BindLobbyButtons`.
- **유지**: 영혼 포인트·승천 표시는 화면 모서리 **미니 HUD**(정보 표시 필요). 기능 패널 4종은 NPC 트리거.
- 런 시작(`StartRun`/`TeleportToActMap`)·전투 흐름은 불변.
### 5) 미러/테스트 영향
- 이동/공격 해제·NPC 배치는 **전투 규칙도 맵 그래프 생성 알고리즘도 아님**`sim-balance.mjs`/`rogue-map.mjs` JS 미러 동기화 **불필요**(RULES.md §6은 그 둘만 요구).
- 검증(카운트만): `lobby.map` 내 NPC 엔티티 수, 산출물의 `LobbyNpc`/`LobbyMobility`/`OnLobbyNpcInteract` 개수, SectorConfig `map://lobby` 존재. 내용 출력 금지.
- 동작 검증: 메이커 플레이테스트.
## 검증 시나리오 (메이커)
1. 월드 시작 → **로비 맵에 스폰**, 이동 가능, 공격 모션 가능.
2. NPC 근접 → `!` 프롬프트 → `Up/Space`로 기능 패널 오픈. 직접 클릭으로도 오픈.
3. 4종 각각: 모험가→직업선택→런 시작, 사서→도감, 상인→영혼상점, 안내원→게시판.
4. 런 시작 → map01 텔레포트, **이동/공격 잠김**.
5. 런 종료(클리어/패배) → **로비 맵 복귀**, 이동/공격 재해제.
6. 미니 HUD에 영혼/승천 표시 정상.
## 리스크
- MSW 런타임 이동 재활성 API 가용성 → 계획 단계 `mlua_api_retriever` 검증.
- MSW 월드 엔티티 클릭 감지 방식 → 동일 검증(불가 시 근접+키만으로 폴백, 직접 클릭은 스크린 오버레이 버튼으로 구현).
- 텔레포트 복귀 좌표/스폰 위치 정합.
- 메이커 stale 상태 — git pull 후 로컬 워크스페이스 reload 필수(RULES.md §5).
## 생성기/파일 변경 요약
| 파일 | 변경 |
|---|---|
| `tools/map/gen-lobby-map.mjs` | **신규** — lobby.map(배경/타일/NPC 엔티티/스폰), SectorConfig 조율 |
| `tools/player/gen-lobby-npc.mjs` | **신규** — LobbyNpc 상호작용 codeblock(+LobbyMobility 또는 분리) |
| `tools/deck/gen-slaydeck.mjs` | OnBeginPlay/EndRun 로비맵 전환, OnLobbyNpcInteract, 버튼-행 허브 제거, 미니 HUD |
| `Global/SectorConfig.config` | map://lobby 등록(생성 산출물) |
| `map/lobby.map`, `ui/DefaultGroup.ui`, `*.codeblock` | 재생성 산출물 |

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

@@ -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

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

141
tools/map/gen-lobby-map.mjs Normal file
View File

@@ -0,0 +1,141 @@
import { readFileSync, writeFileSync } from 'node:fs';
// 로비 전용 맵 생성기 — map01 템플릿을 클론해 마을(타운) 배경의 로비 맵을 만든다.
// · 몬스터 엔티티 전부 제거(전투 없음)
// · NPC 4종(모험가/사서/상인/안내원) 월드 엔티티 배치 + 머리 위 마크(근접 시 표시)
// · 각 NPC: TouchReceiveComponent(클릭) + script.LobbyNpc(NpcId)
// · 맵 루트: script.PlayerLock 제거(이동 허용) + script.LobbyMobility 추가(이동·공격 해제)
// · SectorConfig에 map://lobby 등록
// codeblock 로직(LobbyNpc/LobbyMobility)은 tools/player/gen-lobby-npc.mjs가 emit한다.
const TEMPLATE = 'map/map01.map';
const OUT = 'map/lobby.map';
const SECTOR = 'Global/SectorConfig.config';
const TOWN_BG = '65c4167ea7484196b890022354e5a4a4'; // Henesys (gen-maps.mjs BACKGROUNDS 풀)
const MARK_RUID = 'bd4afdde295f40318fceb4166978ebaa'; // 공식 maplestory balloon (근접 마크)
// NPC 4종: x좌표는 정찰 기준 walkable 범위[-5,6.6], 근접 임계 1.2와 분리되게 배치
const NPCS = [
{ name: 'NpcRun', id: 'run', x: -3.0, ruid: '122095fd155c4633867b0da4f375bc3c' }, // 모험가
{ name: 'NpcCodex', id: 'codex', x: -0.5, ruid: '4c264be6a64f4ac3970b2e6818d04e40' }, // 사서
{ name: 'NpcShop', id: 'shop', x: 2.0, ruid: '69987ccdc486423f8bedd786bd6cb5d9' }, // 상인
{ name: 'NpcBoard', id: 'board', x: 4.5, ruid: '8a99bd87d667482cb1f3b2193f8a19c1' }, // 안내원
];
const MARK_DY = 1.6; // NPC 머리 위 오프셋
// NPC/마크는 정적 스프라이트로 만든다 — 몬스터 AI·물리(중력/충돌)·히트 컴포넌트 전부 제거.
// (마크는 물리가 있으면 머리 위에서 떨어지고, NPC는 충돌이 있으면 플레이어 통행을 막음)
const STRIP = new Set([
'script.Monster', 'script.MonsterAttack', 'script.CombatMonster',
'MOD.Core.RigidbodyComponent', 'MOD.Core.MovementComponent', 'MOD.Core.KinematicbodyComponent',
'MOD.Core.SideviewbodyComponent', 'MOD.Core.HitComponent',
'MOD.Core.DamageSkinSpawnerComponent', 'MOD.Core.DamageSkinSettingComponent',
]);
const compOf = (e, type) => e.jsonString['@components'].find((c) => c['@type'] === type);
const isMonster = (e) => (e.componentNames || '').includes('script.Monster');
// 결정론 GUID — 기존 생성기(map: nn*1000+idx, enc: +500)와 충돌 없는 고유 오프셋
function lobbyGuid(idx) {
const n = (900000 + idx) >>> 0;
return `${n.toString(16).padStart(8, '0')}-0000-4000-8000-${n.toString(16).padStart(12, '0')}`;
}
// 몬스터 템플릿 엔티티를 클론해 스프라이트 엔티티(NPC/마크)로 변환
function makeSpriteEntity(base, name, x, y, ruid, withInteract, npcId) {
const m = JSON.parse(JSON.stringify(base));
m.jsonString.name = name;
m.path = `/maps/lobby/${name}`;
m.jsonString.path = m.path;
// NOTE: 베이스 모델(chasemonster)이 script.Monster/MonsterAttack를 inheritance로 끌고 와서
// DuplicateComponent(LobbyNpc와 공존) 경고 + MonsterAttack.OnBeginPlay AnimationClip 에러가 뜬다.
// 둘 다 비치명적(로비엔 전투 컨텍스트가 없어 Monster는 휴면, 에러는 전투맵 몬스터와 공유되는 기존 lint).
// modelId를 비우면 fresh load에서 렌더가 깨질 위험이 있어 proven-good(모델 유지)로 둔다.
const tr = compOf(m, 'MOD.Core.TransformComponent');
if (tr) { tr.Position.x = x; tr.Position.y = y; }
const sp = compOf(m, 'MOD.Core.SpriteRendererComponent');
if (sp) sp.SpriteRUID = ruid;
const sa = compOf(m, 'MOD.Core.StateAnimationComponent');
if (sa) sa.ActionSheet = { stand: ruid, hit: ruid, die: ruid }; // 항상 stand 스프라이트 고정
// 몬스터 AI·물리·히트 컴포넌트 제거 → 정적 비충돌 스프라이트
m.jsonString['@components'] = m.jsonString['@components'].filter((c) => !STRIP.has(c['@type']));
let names = (m.componentNames || '').split(',').filter((s) => s && !STRIP.has(s));
if (withInteract) {
m.jsonString['@components'].push({ '@type': 'MOD.Core.TouchReceiveComponent', Enable: true, AutoFitToSize: true });
m.jsonString['@components'].push({ '@type': 'script.LobbyNpc', Enable: true, NpcId: npcId, MarkName: name + 'Mark' });
names.push('MOD.Core.TouchReceiveComponent', 'script.LobbyNpc');
}
m.componentNames = names.join(',');
return m;
}
const template = JSON.parse(readFileSync(TEMPLATE, 'utf8'));
const monsterTemplates = template.ContentProto.Entities.filter(isMonster);
if (monsterTemplates.length === 0) throw new Error('[gen-lobby-map] 몬스터 템플릿(스프라이트 엔티티) 없음');
const base = monsterTemplates.find((e) => (e.path || '').includes('Static')) || monsterTemplates[0];
const baseY = (() => {
const tr = compOf(base, 'MOD.Core.TransformComponent');
return tr ? tr.Position.y : 0;
})();
const map = JSON.parse(JSON.stringify(template)); // deep clone
map.EntryKey = 'map://lobby';
let ents = map.ContentProto.Entities.filter((e) => !isMonster(e));
// 경로/이름 치환 + 배경 + 루트 컴포넌트 조정
for (const e of ents) {
if (typeof e.path === 'string') e.path = e.path.replace('/maps/map01', '/maps/lobby');
if (e.jsonString) {
if (typeof e.jsonString.path === 'string') e.jsonString.path = e.jsonString.path.replace('/maps/map01', '/maps/lobby');
if (e.jsonString.name === 'map01') e.jsonString.name = 'lobby';
}
if ((e.path || '').endsWith('/Background')) {
const bg = compOf(e, 'MOD.Core.BackgroundComponent');
if (bg) bg.TemplateRUID = TOWN_BG;
}
}
const root = ents.find((e) => e.path === '/maps/lobby');
if (!root) throw new Error('[gen-lobby-map] 맵 루트 없음');
// 로비엔 PlayerLock 제거(이동 허용) + LobbyMobility 추가(이동·공격 해제). MapCamera는 유지.
root.jsonString['@components'] = root.jsonString['@components'].filter(
(c) => !['script.PlayerLock', 'script.LobbyMobility'].includes(c['@type']),
);
root.jsonString['@components'].push({ '@type': 'script.LobbyMobility', Enable: true });
{
const names = (root.componentNames || '')
.split(',')
.filter((s) => s && !['script.PlayerLock', 'script.LobbyMobility'].includes(s));
names.push('script.LobbyMobility');
root.componentNames = names.join(',');
}
// NPC + 마크 엔티티 생성
for (const npc of NPCS) {
ents.push(makeSpriteEntity(base, npc.name, npc.x, baseY, npc.ruid, true, npc.id));
ents.push(makeSpriteEntity(base, npc.name + 'Mark', npc.x, baseY + MARK_DY, MARK_RUID, false, ''));
}
// GUID 전부 재발급 (map01과 충돌 방지 + 자기참조 origin 보정)
ents.forEach((e, idx) => {
const oldId = e.id;
const newId = lobbyGuid(idx);
e.id = newId;
const o = e.jsonString && e.jsonString.origin;
if (o) {
if (o.root_entity_id === oldId) o.root_entity_id = newId;
if (o.sub_entity_id === oldId) o.sub_entity_id = newId;
}
});
map.ContentProto.Entities = ents;
writeFileSync(OUT, JSON.stringify(map, null, 2), 'utf8');
// SectorConfig 등록 (멱등)
const sector = JSON.parse(readFileSync(SECTOR, 'utf8'));
const sec0 = sector.ContentProto.Json.Sectors[0];
if (!sec0.entries.includes('map://lobby')) sec0.entries.push('map://lobby');
writeFileSync(SECTOR, JSON.stringify(sector, null, 2), 'utf8');
const npcCount = ents.filter((e) => (e.componentNames || '').includes('script.LobbyNpc')).length;
console.log(`[gen-lobby-map] lobby.map 생성: NPC ${npcCount}종 + 마크, SectorConfig entries ${sec0.entries.length}`);

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

@@ -0,0 +1,103 @@
import { writeFileSync } from 'node:fs';
// 로비 codeblock 2종 emit (맵/엔티티 부착은 tools/map/gen-lobby-map.mjs 소관):
// · LobbyNpc — NPC 엔티티에 부착. 근접 폴링→머리위 마크 토글, TouchEvent(클릭)/UpArrow(근접)→Interact→컨트롤러 호출.
// · LobbyMobility — 로비 맵 루트에 부착. 진입 시 플레이어 이동 잠금 해제(정찰 확정: WalkAcceleration이 진짜 레버).
// 공격 키(LeftControl) 바인딩은 SlayDeckController(/common, 1회 등록·로비 가드)에서 처리 — 여기 두지 않음.
// 정찰: 이동에는 RigidbodyComponent.WalkAcceleration(가속)과 MovementComponent.InputSpeed(속도)가
// 둘 다 양수여야 함 — freeze-turn-player가 둘 다 0으로 만들었으므로 둘 다 복원해야 걷는다(실측 확인).
// 값은 freeze가 건드리지 않은 intact RigidbodyComponent.WalkSpeed(1.4)/WalkJump(1.23) = 기본값에 맞춤.
// (실측: InputSpeed 1.4→보행 ~2.5u/s, JumpForce 1.23→점프 상승 1.79u. 이전 5/5는 ~9u/s·상승 14u로 과함.)
const WALK_ACCEL = 0.7;
const WALK_SPEED = 1.4;
const JUMP_FORCE = 1.23;
const PROX = 1.2; // 근접 임계(맵 NPC 간격 2.5와 분리)
function prop(Type, Name, DefaultValue = 'nil') {
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };
}
function method(Name, Code, Arguments = [], ExecSpace = 6) {
return {
Return: { Type: 'void', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null },
Arguments, Code, Scope: 2, ExecSpace, Attributes: [], Name,
};
}
function writeCodeblock(name, properties, methods) {
const cb = {
Id: '', GameId: '', EntryKey: `codeblock://${name.toLowerCase()}`, ContentType: 'x-mod/codeblock',
Content: '', Usage: 0, UsePublish: 1, UseService: 0, CoreVersion: '26.5.0.0', StudioVersion: '', DynamicLoading: 0,
ContentProto: { Use: 'Json', Json: {
CoreVersion: { Major: 0, Minor: 2 }, ScriptVersion: { Major: 1, Minor: 0 },
Description: '', Id: name, Language: 1, Name: name, Type: 1, Source: 0, Target: null,
Properties: properties, Methods: methods, EntityEventHandlers: [],
} },
};
writeFileSync(`RootDesk/MyDesk/${name}.codeblock`, JSON.stringify(cb, null, 2) + '\n', 'utf8');
}
// ── LobbyNpc ──────────────────────────────────────────────────────────────
const npcInteract = method('Interact', `local c = _EntityService:GetEntityByPath("/common")
if c ~= nil and c.SlayDeckController ~= nil then
c.SlayDeckController:OnLobbyNpcInteract(self.NpcId)
end`);
const npcBegin = method('OnBeginPlay', `self.InRange = false
local mark = _EntityService:GetEntityByPath("/maps/lobby/" .. self.MarkName)
if mark ~= nil then mark:SetVisible(false) end
self.Entity:ConnectEvent(TouchEvent, function(e)
self:Interact()
end)
_InputService:ConnectEvent(KeyDownEvent, function(e)
if self.InRange and e.key == KeyboardKey.UpArrow then
self:Interact()
end
end)
local eventId = 0
local function tick()
local lp = _UserService.LocalPlayer
if lp == nil then return end
if mark == nil then mark = _EntityService:GetEntityByPath("/maps/lobby/" .. self.MarkName) end
local a = lp.TransformComponent.WorldPosition
local b = self.Entity.TransformComponent.WorldPosition
local d = Vector2.Distance(Vector2(a.x, a.y), Vector2(b.x, b.y))
local near = d < ${PROX}
if near ~= self.InRange then
self.InRange = near
if mark ~= nil then mark:SetVisible(near) end
end
end
eventId = _TimerService:SetTimerRepeat(tick, 0.15)`);
writeCodeblock('LobbyNpc', [
prop('string', 'NpcId', '""'),
prop('string', 'MarkName', '""'),
prop('boolean', 'InRange', 'false'),
], [npcBegin, npcInteract]);
// ── LobbyMobility ─────────────────────────────────────────────────────────
const mobBegin = method('OnBeginPlay', `self.Tries = 0
local eventId = 0
local function apply()
self.Tries = self.Tries + 1
local lp = _UserService.LocalPlayer
if lp ~= nil and lp.PlayerControllerComponent ~= nil then
local pc = lp.PlayerControllerComponent
pc.Enable = true
pc.FixedLookAt = 0
local rb = lp.RigidbodyComponent
if rb ~= nil then rb.WalkAcceleration = ${WALK_ACCEL} end
local mv = lp.MovementComponent
if mv ~= nil then
mv.InputSpeed = ${WALK_SPEED}
mv.JumpForce = ${JUMP_FORCE}
end
_TimerService:ClearTimer(eventId)
elseif self.Tries > 50 then
_TimerService:ClearTimer(eventId)
end
end
eventId = _TimerService:SetTimerRepeat(apply, 0.1)`);
writeCodeblock('LobbyMobility', [prop('number', 'Tries', '0')], [mobBegin]);
console.log('[gen-lobby-npc] LobbyNpc / LobbyMobility codeblock 생성 완료');

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 };
@@ -65,6 +65,10 @@ local function apply()
pc.FixedLookAt = ${FIXED_LOOK_AT}
pc.Enable = ${CONTROLLER_ENABLE}
end
if lp ~= nil then
if lp.RigidbodyComponent ~= nil then lp.RigidbodyComponent.WalkAcceleration = 0 end
if lp.MovementComponent ~= nil then lp.MovementComponent.InputSpeed = 0; lp.MovementComponent.JumpForce = 0 end
end
if pc ~= nil then
_TimerService:ClearTimer(eventId)
elseif self.LockTries > 30 then

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