diff --git a/README.md b/README.md index ea14446..a36437a 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ slaymaple/ │ └── RectTileData_Henesys.tileset ├── map/ │ └── map01.map ~ map11.map # 맵 11종 (공식 배경 + STS풍 우측 배치) -├── tools/ # 결정적 생성기·도구 (단일 소스) -│ ├── gen-slaydeck.mjs # ★ 게임 전체 생성: 카드/덱·맵·상점·유물·메인메뉴 UI + SlayDeckController + common -│ ├── gen-cardhand.mjs # 손패 카드 엔티티 초기 생성 -│ ├── gen-maps.mjs # 맵 생성 -│ ├── sim-balance.mjs # AI 전투 밸런스 시뮬레이터(몬테카를로) + sim-balance.test.mjs -│ ├── freeze-turn-monsters.mjs # 턴전투용 필드 몬스터 AI/이동 정지 패치 -│ └── freeze-turn-player.mjs # 턴전투용 플레이어 이동 정지 패치 +├── tools/ # 결정적 생성기·도구 (주체별 폴더, 단일 소스) +│ ├── deck/ # gen-slaydeck.mjs(★게임 전체 생성: 카드/덱·맵·상점·유물·메인메뉴 UI+SlayDeckController+common) · gen-cardhand.mjs(손패 초기 생성) +│ ├── map/ # gen-maps.mjs(맵 생성) +│ ├── camera/ # gen-camera.mjs(맵별 고정 카메라 codeblock) +│ ├── player/ # freeze-turn-player.mjs(이동 정지) · gen-player-lock.mjs(입력 차단·시선 고정 codeblock) +│ ├── monster/ # freeze-turn-monsters.mjs(필드 몬스터 AI/이동 정지) +│ └── balance/ # sim-balance.mjs(밸런스 시뮬·몬테카를로) · sim-balance.test.mjs ├── ui/ # UI 그룹 (Default / Popup / Toast) ├── docs/ │ ├── slaymaple_basic_framework.md # 전투 프레임워크 설계 문서 @@ -82,7 +82,7 @@ slaymaple/ ## 게임 프레임워크 현황 -**STS풍 덱빌더 런이 end-to-end로 완성**됐습니다 — 메인 메뉴 → 분기 맵 → 전투/엘리트/상점/휴식 → 카드 보상·덱 성장 → 유물 → 보스 → 다음 막 → 런 클리어. 게임 전체는 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작하며, 모든 산출물(`ui/DefaultGroup.ui` · `SlayDeckController.codeblock` · `common.gamelogic`)은 **`tools/gen-slaydeck.mjs` 단일 소스에서 생성**됩니다(직접 편집 금지, 결정적 출력). 게임 데이터는 **`data/*.json`** 가 단일 소스. +**STS풍 덱빌더 런이 end-to-end로 완성**됐습니다 — 메인 메뉴 → 분기 맵 → 전투/엘리트/상점/휴식 → 카드 보상·덱 성장 → 유물 → 보스 → 다음 막 → 런 클리어. 게임 전체는 `/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작하며, 모든 산출물(`ui/DefaultGroup.ui` · `SlayDeckController.codeblock` · `common.gamelogic`)은 **`tools/deck/gen-slaydeck.mjs` 단일 소스에서 생성**됩니다(직접 편집 금지, 결정적 출력). 게임 데이터는 **`data/*.json`** 가 단일 소스. ### 구현된 기능 @@ -96,7 +96,7 @@ slaymaple/ | **상점/휴식** | 상점=골드로 카드·유물 구매. 휴식=HP 회복 | | **유물** | 훅 패시브(`combatStart`/`turnStart`/`cardPlayed`/`combatReward`). 획득 3경로(시작·엘리트 승리·상점). 유물 4종 | | **멀티 act** | 보스 클리어→다음 막(적 스케일 `1+(막-1)*0.6`), 최종 막 보스에서 런 클리어. 막 수 3 | -| **밸런스 시뮬** | `tools/sim-balance.mjs` — 몬테카를로 N회 전투로 승률·턴·OP 카드 리포트 | +| **밸런스 시뮬** | `tools/balance/sim-balance.mjs` — 몬테카를로 N회 전투로 승률·턴·OP 카드 리포트 | | **턴전투 freeze** | 카드 전투 중 필드 몬스터/플레이어 이동·AI 정지(`freeze-turn-*.mjs`) | > ⚠️ 플레이어 HP(80)·적 수치·골드/카드값(15/30/유물60)·막 배율 등은 **밸런싱 미조정 placeholder**입니다. 추후 카드·적은 **메이플스토리 IP**에 맞춰 디벨롭 예정이며, 밸런싱은 `sim-balance.mjs`로 검증합니다. @@ -113,7 +113,7 @@ c:PickReward(1) -- 보상 카드 1택(0=건너뛰기) c:BuyCard(1) / c:BuyRelic() -- 상점 구매 ``` -밸런스 검증: `node tools/sim-balance.mjs [N] [--seed S]` · 테스트: `node --test tools/sim-balance.test.mjs`. +밸런스 검증: `node tools/balance/sim-balance.mjs [N] [--seed S]` · 테스트: `node --test tools/balance/sim-balance.test.mjs`. 상세 설계는 [`docs/slaymaple_basic_framework.md`](docs/slaymaple_basic_framework.md) 및 `docs/superpowers/specs/` 참조. --- diff --git a/RootDesk/MyDesk/MapCamera.codeblock b/RootDesk/MyDesk/MapCamera.codeblock index 0f0fe49..1d0744d 100644 --- a/RootDesk/MyDesk/MapCamera.codeblock +++ b/RootDesk/MyDesk/MapCamera.codeblock @@ -47,7 +47,7 @@ "Name": null }, "Arguments": [], - "Code": "self.CamTries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.CamTries = self.CamTries + 1\n\tlocal cam = nil\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil then\n\t\tcam = lp.CameraComponent\n\tend\n\tif cam == nil then\n\t\tcam = _CameraService:GetCurrentCameraComponent()\n\tend\n\tif cam ~= nil then\n\t\tcam.ZoomRatio = 90\n\t\tcam.ScreenOffset = Vector2(0.5, 0.655)\n\t\tcam.ConfineCameraArea = true\n\t\tcam.CameraOffset = Vector2(1.5, -1)\n\tend\n\tlocal pc = nil\n\tif lp ~= nil then\n\t\tpc = lp.PlayerControllerComponent\n\t\tif pc ~= nil then\n\t\t\tpc.LookDirectionX = 1\n\t\t\tpc.FixedLookAt = true\n\t\t\tpc.Enable = false\n\t\tend\n\tend\n\tif cam ~= nil and pc ~= nil then\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.CamTries > 30 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)", + "Code": "self.CamTries = 0\nlocal eventId = 0\nlocal function apply()\n\tself.CamTries = self.CamTries + 1\n\tlocal cam = nil\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil then\n\t\tcam = lp.CameraComponent\n\tend\n\tif cam == nil then\n\t\tcam = _CameraService:GetCurrentCameraComponent()\n\tend\n\tif cam ~= nil then\n\t\tcam.ZoomRatio = 90\n\t\tcam.ScreenOffset = Vector2(0.5, 0.655)\n\t\tcam.ConfineCameraArea = true\n\t\tcam.CameraOffset = Vector2(1.5, -1)\n\tend\n\tif cam ~= nil then\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.CamTries > 30 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(apply, 0.1)", "Scope": 2, "ExecSpace": 6, "Attributes": [], diff --git a/RootDesk/MyDesk/PlayerLock.codeblock b/RootDesk/MyDesk/PlayerLock.codeblock new file mode 100644 index 0000000..bc566d5 --- /dev/null +++ b/RootDesk/MyDesk/PlayerLock.codeblock @@ -0,0 +1,60 @@ +{ + "Id": "", + "GameId": "", + "EntryKey": "codeblock://playerlock", + "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": "PlayerLock", + "Language": 1, + "Name": "PlayerLock", + "Type": 1, + "Source": 0, + "Target": null, + "Properties": [ + { + "Type": "number", + "DefaultValue": "0", + "SyncDirection": 0, + "Attributes": [], + "Name": "LockTries" + } + ], + "Methods": [ + { + "Return": { + "Type": "void", + "DefaultValue": null, + "SyncDirection": 0, + "Attributes": [], + "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)", + "Scope": 2, + "ExecSpace": 6, + "Attributes": [], + "Name": "OnBeginPlay" + } + ], + "EntityEventHandlers": [] + } + } +} \ No newline at end of file diff --git a/map/map01.map b/map/map01.map index 01f5e66..504f38c 100644 --- a/map/map01.map +++ b/map/map01.map @@ -16,7 +16,7 @@ { "id": "bdadf19a-cc27-4a45-99c6-7a439c858a1b", "path": "/maps/map01", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map01", "path": "/maps/map01", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map02.map b/map/map02.map index 03b2519..3c65a85 100644 --- a/map/map02.map +++ b/map/map02.map @@ -16,7 +16,7 @@ { "id": "000007d0-0000-4000-8000-0000000007d0", "path": "/maps/map02", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map02", "path": "/maps/map02", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map03.map b/map/map03.map index c3ba5cd..c246154 100644 --- a/map/map03.map +++ b/map/map03.map @@ -16,7 +16,7 @@ { "id": "00000bb8-0000-4000-8000-000000000bb8", "path": "/maps/map03", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map03", "path": "/maps/map03", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map04.map b/map/map04.map index 505452e..e5a5efc 100644 --- a/map/map04.map +++ b/map/map04.map @@ -16,7 +16,7 @@ { "id": "00000fa0-0000-4000-8000-000000000fa0", "path": "/maps/map04", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map04", "path": "/maps/map04", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map05.map b/map/map05.map index 28eec51..8dd6249 100644 --- a/map/map05.map +++ b/map/map05.map @@ -16,7 +16,7 @@ { "id": "00001388-0000-4000-8000-000000001388", "path": "/maps/map05", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map05", "path": "/maps/map05", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map06.map b/map/map06.map index d556799..aebd0ad 100644 --- a/map/map06.map +++ b/map/map06.map @@ -16,7 +16,7 @@ { "id": "00001770-0000-4000-8000-000000001770", "path": "/maps/map06", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map06", "path": "/maps/map06", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map07.map b/map/map07.map index 093d2c9..670b28a 100644 --- a/map/map07.map +++ b/map/map07.map @@ -16,7 +16,7 @@ { "id": "00001b58-0000-4000-8000-000000001b58", "path": "/maps/map07", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map07", "path": "/maps/map07", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map08.map b/map/map08.map index 7f6699e..3d63dd9 100644 --- a/map/map08.map +++ b/map/map08.map @@ -16,7 +16,7 @@ { "id": "00001f40-0000-4000-8000-000000001f40", "path": "/maps/map08", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map08", "path": "/maps/map08", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map09.map b/map/map09.map index da59f4b..dbcbeb0 100644 --- a/map/map09.map +++ b/map/map09.map @@ -16,7 +16,7 @@ { "id": "00002328-0000-4000-8000-000000002328", "path": "/maps/map09", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map09", "path": "/maps/map09", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map10.map b/map/map10.map index 140abdf..e4d0a3e 100644 --- a/map/map10.map +++ b/map/map10.map @@ -16,7 +16,7 @@ { "id": "00002710-0000-4000-8000-000000002710", "path": "/maps/map10", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map10", "path": "/maps/map10", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/map/map11.map b/map/map11.map index de3fb5d..4cecfcf 100644 --- a/map/map11.map +++ b/map/map11.map @@ -16,7 +16,7 @@ { "id": "00002af8-0000-4000-8000-000000002af8", "path": "/maps/map11", - "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera,script.PlayerLock", "jsonString": { "name": "map11", "path": "/maps/map11", @@ -1107,6 +1107,10 @@ { "@type": "script.MapCamera", "Enable": true + }, + { + "@type": "script.PlayerLock", + "Enable": true } ], "@version": 1 diff --git a/tools/sim-balance.mjs b/tools/balance/sim-balance.mjs similarity index 98% rename from tools/sim-balance.mjs rename to tools/balance/sim-balance.mjs index 6dba43f..afb81d2 100644 --- a/tools/sim-balance.mjs +++ b/tools/balance/sim-balance.mjs @@ -1,5 +1,5 @@ // AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로. -// ⚠️ 전투 규칙은 tools/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것. +// ⚠️ 전투 규칙은 tools/deck/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것. // (데이터는 data/*.json 공유, 규칙 로직은 JS로 중복 재현) import { readFileSync } from 'node:fs'; diff --git a/tools/sim-balance.test.mjs b/tools/balance/sim-balance.test.mjs similarity index 100% rename from tools/sim-balance.test.mjs rename to tools/balance/sim-balance.test.mjs diff --git a/tools/gen-camera.mjs b/tools/camera/gen-camera.mjs similarity index 94% rename from tools/gen-camera.mjs rename to tools/camera/gen-camera.mjs index d28df39..734c0e4 100644 --- a/tools/gen-camera.mjs +++ b/tools/camera/gen-camera.mjs @@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs'; // 맵별 고정 카메라: 맵 로드 시 플레이어 CameraComponent를 data/camera.json 값으로 설정. // 새 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 @@ -65,16 +66,7 @@ local function apply() cam.ConfineCameraArea = ${CAM.confineCameraArea} cam.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY}) end - local pc = nil - if lp ~= nil then - pc = lp.PlayerControllerComponent - if pc ~= nil then - pc.LookDirectionX = 1 - pc.FixedLookAt = true - pc.Enable = false - end - end - if cam ~= nil and pc ~= nil then + if cam ~= nil then _TimerService:ClearTimer(eventId) elseif self.CamTries > 30 then _TimerService:ClearTimer(eventId) diff --git a/tools/gen-cardhand.mjs b/tools/deck/gen-cardhand.mjs similarity index 100% rename from tools/gen-cardhand.mjs rename to tools/deck/gen-cardhand.mjs diff --git a/tools/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs similarity index 100% rename from tools/gen-slaydeck.mjs rename to tools/deck/gen-slaydeck.mjs diff --git a/tools/gen-maps.mjs b/tools/map/gen-maps.mjs similarity index 99% rename from tools/gen-maps.mjs rename to tools/map/gen-maps.mjs index c8ff8de..1eb88e2 100644 --- a/tools/gen-maps.mjs +++ b/tools/map/gen-maps.mjs @@ -133,7 +133,7 @@ function buildMap(nn) { return `map${tag}`; } -// 인자: 생성할 맵 번호(미지정 시 전체). 예: node tools/gen-maps.mjs 2 +// 인자: 생성할 맵 번호(미지정 시 전체). 예: node tools/map/gen-maps.mjs 2 const arg = process.argv[2]; const targets = arg ? [Number(arg)] : MAP_NUMBERS; const made = targets.map(buildMap); diff --git a/tools/freeze-turn-monsters.mjs b/tools/monster/freeze-turn-monsters.mjs similarity index 100% rename from tools/freeze-turn-monsters.mjs rename to tools/monster/freeze-turn-monsters.mjs diff --git a/tools/freeze-turn-player.mjs b/tools/player/freeze-turn-player.mjs similarity index 100% rename from tools/freeze-turn-player.mjs rename to tools/player/freeze-turn-player.mjs diff --git a/tools/player/gen-player-lock.mjs b/tools/player/gen-player-lock.mjs new file mode 100644 index 0000000..90ebb16 --- /dev/null +++ b/tools/player/gen-player-lock.mjs @@ -0,0 +1,101 @@ +import { readFileSync, writeFileSync } from 'node:fs'; + +// 플레이어 입력 잠금: 맵 로드 시 LocalPlayer의 PlayerControllerComponent를 런타임 설정. +// (gen-camera.mjs에서 분리 — 카메라 제어와 플레이어 제어를 주체별로 나눔) +// · 입력 차단(턴제 전투) · 시선을 오른쪽(전투 포메이션 방향)으로 고정 +// 정적 이동 차단은 freeze-turn-player.mjs(Global/DefaultPlayer.model)가 담당하며, 이 스크립트는 런타임 컨트롤러를 제어한다. +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 + +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() { + const cb = { + Id: '', + GameId: '', + EntryKey: 'codeblock://playerlock', + 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: 'PlayerLock', + Language: 1, + Name: 'PlayerLock', + Type: 1, + Source: 0, + Target: null, + Properties: [prop('number', 'LockTries', '0')], + Methods: [ + method('OnBeginPlay', `self.LockTries = 0 +local eventId = 0 +local function apply() + self.LockTries = self.LockTries + 1 + local pc = nil + local lp = _UserService.LocalPlayer + if lp ~= nil then + pc = lp.PlayerControllerComponent + end + if pc ~= nil then + pc.LookDirectionX = ${LOOK_DIRECTION_X} + pc.FixedLookAt = ${FIXED_LOOK_AT} + pc.Enable = ${CONTROLLER_ENABLE} + end + if pc ~= nil then + _TimerService:ClearTimer(eventId) + elseif self.LockTries > 30 then + _TimerService:ClearTimer(eventId) + end +end +eventId = _TimerService:SetTimerRepeat(apply, 0.1)`), + ], + EntityEventHandlers: [], + }, + }, + }; + writeFileSync('RootDesk/MyDesk/PlayerLock.codeblock', JSON.stringify(cb, null, 2), 'utf8'); +} + +function patchMap(nn) { + const tag = String(nn).padStart(2, '0'); + const file = `map/map${tag}.map`; + const map = JSON.parse(readFileSync(file, 'utf8')); + const root = map.ContentProto.Entities.find((e) => e.path === `/maps/map${tag}`); + if (!root) throw new Error(`[gen-player-lock] 맵 루트 없음: ${file}`); + // idempotent: 기존 script.PlayerLock 제거 후 재추가 + root.jsonString['@components'] = root.jsonString['@components'].filter((c) => c['@type'] !== 'script.PlayerLock'); + root.jsonString['@components'].push({ '@type': 'script.PlayerLock', Enable: true }); + const names = (root.componentNames || '').split(',').filter((s) => s && s !== 'script.PlayerLock'); + names.push('script.PlayerLock'); + root.componentNames = names.join(','); + writeFileSync(file, JSON.stringify(map, null, 2), 'utf8'); + return `map${tag}`; +} + +writeCodeblock(); +const patched = MAP_NUMBERS.map(patchMap); +console.log('PlayerLock codeblock written; patched maps:', patched.join(', '));