From 68c9333b59939c76bebc8ca7175388b1c142b5ad Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 9 Jun 2026 22:24:09 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(map-camera):=20=EB=A7=B5=EB=B3=84=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20=E2=80=94=20?= =?UTF-8?q?MapCamera=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A1=9C=2011?= =?UTF-8?q?=EB=A7=B5=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20framing=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 맵 로드 시 플레이어 CameraComponent를 data/camera.json 값(현재 map01: zoom100·screenOffset0.5/0.655·confine)으로 설정. - data/camera.json: 카메라 framing 단일 설정 - tools/gen-camera.mjs: MapCamera.codeblock 생성 + 11맵 루트에 script.MapCamera 부착(idempotent) - 새 CameraComponent 미생성(엔진 소유) — 기존 플레이어 카메라 속성만 런타임 설정 - OnBeginPlay(client, ExecSpace 6) + LocalPlayer 카메라 재시도 타이머 - 메이커 Play 검증: zoom 60 테스트로 적용 입증, 100으로 복원. idempotent·결정적 - 참고: 맵 루트 client 스크립트는 ExecSpace 6 필요(1은 미발동) Co-Authored-By: Claude Opus 4.8 (1M context) --- RootDesk/MyDesk/MapCamera.codeblock | 60 ++++++++++++++++++ data/camera.json | 6 ++ map/map01.map | 8 ++- map/map02.map | 8 ++- map/map03.map | 8 ++- map/map04.map | 8 ++- map/map05.map | 8 ++- map/map06.map | 8 ++- map/map07.map | 8 ++- map/map08.map | 8 ++- map/map09.map | 8 ++- map/map10.map | 8 ++- map/map11.map | 8 ++- tools/gen-camera.mjs | 98 +++++++++++++++++++++++++++++ 14 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 RootDesk/MyDesk/MapCamera.codeblock create mode 100644 data/camera.json create mode 100644 tools/gen-camera.mjs diff --git a/RootDesk/MyDesk/MapCamera.codeblock b/RootDesk/MyDesk/MapCamera.codeblock new file mode 100644 index 0000000..594d8e8 --- /dev/null +++ b/RootDesk/MyDesk/MapCamera.codeblock @@ -0,0 +1,60 @@ +{ + "Id": "", + "GameId": "", + "EntryKey": "codeblock://mapcamera", + "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": "MapCamera", + "Language": 1, + "Name": "MapCamera", + "Type": 1, + "Source": 0, + "Target": null, + "Properties": [ + { + "Type": "number", + "DefaultValue": "0", + "SyncDirection": 0, + "Attributes": [], + "Name": "CamTries" + } + ], + "Methods": [ + { + "Return": { + "Type": "void", + "DefaultValue": null, + "SyncDirection": 0, + "Attributes": [], + "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 = 100\n\t\tcam.ScreenOffset = Vector2(0.5, 0.655)\n\t\tcam.ConfineCameraArea = true\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": [], + "Name": "OnBeginPlay" + } + ], + "EntityEventHandlers": [] + } + } +} \ No newline at end of file diff --git a/data/camera.json b/data/camera.json new file mode 100644 index 0000000..5592b0a --- /dev/null +++ b/data/camera.json @@ -0,0 +1,6 @@ +{ + "zoomRatio": 100, + "screenOffsetX": 0.5, + "screenOffsetY": 0.655, + "confineCameraArea": true +} diff --git a/map/map01.map b/map/map01.map index e5fa7af..0bb0ea2 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map01", "path": "/maps/map01", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6946,4 +6950,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map02.map b/map/map02.map index 92ef160..03b2519 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map02", "path": "/maps/map02", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map03.map b/map/map03.map index 85879b5..c3ba5cd 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map03", "path": "/maps/map03", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map04.map b/map/map04.map index 8ac5533..505452e 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map04", "path": "/maps/map04", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map05.map b/map/map05.map index 9a64045..28eec51 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map05", "path": "/maps/map05", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map06.map b/map/map06.map index 34b50e3..d556799 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map06", "path": "/maps/map06", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map07.map b/map/map07.map index 59700a2..093d2c9 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map07", "path": "/maps/map07", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map08.map b/map/map08.map index ae92401..7f6699e 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map08", "path": "/maps/map08", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map09.map b/map/map09.map index 4cb304f..da59f4b 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map09", "path": "/maps/map09", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map10.map b/map/map10.map index 0e36c3f..140abdf 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map10", "path": "/maps/map10", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/map/map11.map b/map/map11.map index 032ab51..de3fb5d 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", + "componentNames": "MOD.Core.MapComponent,MOD.Core.FootholdComponent,script.MapCamera", "jsonString": { "name": "map11", "path": "/maps/map11", @@ -1103,6 +1103,10 @@ ] }, "Enable": true + }, + { + "@type": "script.MapCamera", + "Enable": true } ], "@version": 1 @@ -6653,4 +6657,4 @@ } ] } -} +} \ No newline at end of file diff --git a/tools/gen-camera.mjs b/tools/gen-camera.mjs new file mode 100644 index 0000000..d507a96 --- /dev/null +++ b/tools/gen-camera.mjs @@ -0,0 +1,98 @@ +import { readFileSync, writeFileSync } from 'node:fs'; + +// 맵별 고정 카메라: 맵 로드 시 플레이어 CameraComponent를 data/camera.json 값으로 설정. +// 새 CameraComponent를 만들지 않고(엔진 소유) 기존 카메라 속성만 런타임 설정한다. +const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8')); +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://mapcamera', + 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: 'MapCamera', + Language: 1, + Name: 'MapCamera', + Type: 1, + Source: 0, + Target: null, + Properties: [prop('number', 'CamTries', '0')], + Methods: [ + method('OnBeginPlay', `self.CamTries = 0 +local eventId = 0 +local function apply() + self.CamTries = self.CamTries + 1 + local cam = nil + local lp = _UserService.LocalPlayer + if lp ~= nil then + cam = lp.CameraComponent + end + if cam == nil then + cam = _CameraService:GetCurrentCameraComponent() + end + if cam ~= nil then + cam.ZoomRatio = ${CAM.zoomRatio} + cam.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY}) + cam.ConfineCameraArea = ${CAM.confineCameraArea} + _TimerService:ClearTimer(eventId) + elseif self.CamTries > 30 then + _TimerService:ClearTimer(eventId) + end +end +eventId = _TimerService:SetTimerRepeat(apply, 0.1)`), + ], + EntityEventHandlers: [], + }, + }, + }; + writeFileSync('RootDesk/MyDesk/MapCamera.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-camera] 맵 루트 없음: ${file}`); + // idempotent: 기존 script.MapCamera 제거 후 재추가 + root.jsonString['@components'] = root.jsonString['@components'].filter((c) => c['@type'] !== 'script.MapCamera'); + root.jsonString['@components'].push({ '@type': 'script.MapCamera', Enable: true }); + const names = (root.componentNames || '').split(',').filter((s) => s && s !== 'script.MapCamera'); + names.push('script.MapCamera'); + root.componentNames = names.join(','); + writeFileSync(file, JSON.stringify(map, null, 2), 'utf8'); + return `map${tag}`; +} + +writeCodeblock(); +const patched = MAP_NUMBERS.map(patchMap); +console.log('MapCamera codeblock written; patched maps:', patched.join(', ')); -- 2.49.1 From 9eeb12adf9d04f3c331f589628033a8bb3e630fe Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 9 Jun 2026 22:24:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs(map-camera):=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=EC=B9=B4=EB=A9=94=EB=9D=BC=20=EC=84=A4=EA=B3=84=C2=B7=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EA=B3=84=ED=9A=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-09-map-camera.md | 213 ++++++++++++++++++ .../specs/2026-06-09-map-camera-design.md | 50 ++++ 2 files changed, 263 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-map-camera.md create mode 100644 docs/superpowers/specs/2026-06-09-map-camera-design.md diff --git a/docs/superpowers/plans/2026-06-09-map-camera.md b/docs/superpowers/plans/2026-06-09-map-camera.md new file mode 100644 index 0000000..53c8c49 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-map-camera.md @@ -0,0 +1,213 @@ +# 맵별 고정 카메라 (런타임 설정) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 맵 로드 시 플레이어 카메라를 `data/camera.json`의 고정 framing(현재 map01 값)으로 설정하는 `MapCamera` 스크립트를 11맵에 부착. + +**Architecture:** 새 CameraComponent를 만들지 않고(엔진 소유), `MapCamera.codeblock`이 OnBeginPlay에서 플레이어의 기존 CameraComponent 속성(ZoomRatio·ScreenOffset·ConfineCameraArea)을 설정. `gen-camera.mjs`가 codeblock 생성 + 11맵 루트에 `script.MapCamera` 부착(idempotent). + +**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock/map JSON. 검증은 node --check+재생성+JSON유효+결정성+메이커 Play(카메라 적용 확인). + +--- + +## File Structure +- Create: `data/camera.json` — 카메라 framing 값. +- Create: `tools/gen-camera.mjs` — MapCamera.codeblock 생성 + 11맵 루트에 script.MapCamera 부착(idempotent). +- 생성물: `RootDesk/MyDesk/MapCamera.codeblock`, `map/map01.map`~`map11.map`(패치). + +검증: MSW Lua 단위테스트 불가 → 생성기 문법·JSON유효·결정성·idempotency·메이커 Play. + +--- + +### Task 1: data/camera.json + gen-camera.mjs (codeblock 생성) + +**Files:** Create `data/camera.json`, `tools/gen-camera.mjs` + +- [ ] **Step 1: `data/camera.json`** (현재 map01 추출값) + +```json +{ + "zoomRatio": 100, + "screenOffsetX": 0.5, + "screenOffsetY": 0.655, + "confineCameraArea": true +} +``` + +- [ ] **Step 2: `tools/gen-camera.mjs` 작성 — codeblock 생성 부분** + +```js +import { readFileSync, writeFileSync } from 'node:fs'; + +const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8')); +const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11 + +function method(Name, Code, Arguments = [], ExecSpace = 1) { + return { + Return: { Type: 'void', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null }, + Arguments, Code, Scope: 2, ExecSpace, Attributes: [], Name, + }; +} +function prop(Type, Name, DefaultValue = 'nil') { + return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name }; +} + +function writeCodeblock() { + const cb = { + Id: '', GameId: '', EntryKey: 'codeblock://mapcamera', 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: 'MapCamera', Language: 1, Name: 'MapCamera', Type: 1, Source: 0, Target: null, + Properties: [ prop('number', 'CamTries', '0') ], + Methods: [ + method('OnBeginPlay', `self.CamTries = 0 +local eventId = 0 +local function apply() + self.CamTries = self.CamTries + 1 + local cam = nil + local lp = _UserService.LocalPlayer + if lp ~= nil then + cam = lp.CameraComponent + end + if cam == nil then + cam = _CameraService:GetCurrentCameraComponent() + end + if cam ~= nil then + cam.ZoomRatio = ${CAM.zoomRatio} + cam.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY}) + cam.ConfineCameraArea = ${CAM.confineCameraArea} + _TimerService:ClearTimer(eventId) + elseif self.CamTries > 30 then + _TimerService:ClearTimer(eventId) + end +end +eventId = _TimerService:SetTimerRepeat(apply, 0.1)`), + ], + EntityEventHandlers: [], + } }, + }; + writeFileSync('RootDesk/MyDesk/MapCamera.codeblock', JSON.stringify(cb, null, 2), 'utf8'); +} +``` + +- [ ] **Step 3: 문법 검사 (맵 패치 전, codeblock만)** — 임시로 `writeCodeblock(); console.log('cb ok');` 호출 추가 후: + +Run: `node --check tools/gen-camera.mjs` +Expected: 오류 없음 + +- [ ] **Step 4: 커밋** + +```bash +git add data/camera.json tools/gen-camera.mjs +git commit -m "gen-camera(map-camera): camera.json + MapCamera.codeblock 생성기" +``` + +--- + +### Task 2: gen-camera.mjs — 11맵 루트에 script.MapCamera 부착 + +**Files:** Modify `tools/gen-camera.mjs` + +- [ ] **Step 1: 맵 패치 함수 + 실행부 추가** (Step 2의 writeCodeblock 임시 호출은 제거) + +```js +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-camera] 맵 루트 없음: ${file}`); + const comps = root.jsonString['@components']; + // idempotent: 기존 script.MapCamera 제거 후 재추가 + root.jsonString['@components'] = comps.filter((c) => c['@type'] !== 'script.MapCamera'); + root.jsonString['@components'].push({ '@type': 'script.MapCamera', Enable: true }); + const names = (root.componentNames || '').split(',').filter((s) => s && s !== 'script.MapCamera'); + names.push('script.MapCamera'); + root.componentNames = names.join(','); + writeFileSync(file, JSON.stringify(map, null, 2), 'utf8'); + return `map${tag}`; +} + +writeCodeblock(); +const patched = MAP_NUMBERS.map(patchMap); +console.log('MapCamera codeblock written; patched maps:', patched.join(', ')); +``` + +- [ ] **Step 2: 문법 검사** + +Run: `node --check tools/gen-camera.mjs` +Expected: 오류 없음 + +- [ ] **Step 3: 커밋** + +```bash +git add tools/gen-camera.mjs +git commit -m "gen-camera(map-camera): 11맵 루트에 script.MapCamera 부착(idempotent)" +``` + +--- + +### Task 3: 실행 + 정적 검증 + +**Files:** 생성물 + +- [ ] **Step 1: 생성기 실행** + +Run: `node tools/gen-camera.mjs` +Expected: `MapCamera codeblock written; patched maps: map01, ..., map11` + +- [ ] **Step 2: codeblock·부착 확인** + +Run: `node -e "const c=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/MapCamera.codeblock','utf8')); console.log(c.ContentProto.Json.Id==='MapCamera'&&/ScreenOffset = Vector2\(0.5, 0.655\)/.test(c.ContentProto.Json.Methods[0].Code)?'CB OK':'CB BAD'); for(const nn of [1,6,11]){const tag=String(nn).padStart(2,'0'); const m=JSON.parse(require('fs').readFileSync('map/map'+tag+'.map','utf8')); const r=m.ContentProto.Entities.find(e=>e.path==='/maps/map'+tag); const has=r.componentNames.includes('script.MapCamera')&&r.jsonString['@components'].some(x=>x['@type']==='script.MapCamera'); console.log('map'+tag, has?'ATTACHED':'MISSING');}"` +Expected: `CB OK`, `map01 ATTACHED`, `map06 ATTACHED`, `map11 ATTACHED` + +- [ ] **Step 3: idempotency (2회 실행 시 중복 부착 없음)** + +Run: `node tools/gen-camera.mjs >/dev/null && node -e "const m=JSON.parse(require('fs').readFileSync('map/map01.map','utf8')); const r=m.ContentProto.Entities.find(e=>e.path==='/maps/map01'); const n=(r.componentNames.match(/script.MapCamera/g)||[]).length; const c=r.jsonString['@components'].filter(x=>x['@type']==='script.MapCamera').length; console.log('componentNames 횟수='+n+' @components 횟수='+c+(n===1&&c===1?' IDEMPOTENT OK':' DUP!'))"` +Expected: `IDEMPOTENT OK` + +- [ ] **Step 4: JSON 유효 + 결정성** + +Run: `for f in map/map*.map; do node -e "JSON.parse(require('fs').readFileSync('$f','utf8'))" || echo "BAD $f"; done; node tools/gen-camera.mjs >/dev/null && sha1sum map/map01.map RootDesk/MyDesk/MapCamera.codeblock > /tmp/a.sha && node tools/gen-camera.mjs >/dev/null && sha1sum map/map01.map RootDesk/MyDesk/MapCamera.codeblock > /tmp/b.sha && diff /tmp/a.sha /tmp/b.sha && echo DETERMINISTIC` +Expected: BAD 없음, `DETERMINISTIC` + +- [ ] **Step 5: git status** + +Run: `git status --short` +Expected: `map/map01~11.map`, `RootDesk/MyDesk/MapCamera.codeblock`, `data/camera.json`, `tools/gen-camera.mjs` (+docs). map02~11은 freeze/카메라로 변경될 수 있음. + +- [ ] **Step 6: 생성물 커밋** + +```bash +git add map/*.map RootDesk/MyDesk/MapCamera.codeblock +git commit -m "재생성(map-camera): MapCamera codeblock + 11맵 부착 반영" +``` + +--- + +### Task 4: 메이커 Play 런타임 검증 (ExecSpace 확정) + +**Files:** 없음 (런타임 검증; 필요 시 ExecSpace 조정) + +- [ ] **Step 1: reload → Play → map01 카메라 적용 확인** + +메이커 reload 후 Play. `maker_execute_script`(client)로 현재 카메라 값 확인: +```lua +local cam = _CameraService:GetCurrentCameraComponent() +log("Zoom=" .. tostring(cam.ZoomRatio) .. " SO=(" .. tostring(cam.ScreenOffset.x) .. "," .. tostring(cam.ScreenOffset.y) .. ") Confine=" .. tostring(cam.ConfineCameraArea)) +``` +Expected: Zoom 100·SO(0.5,0.655)·Confine true (MapCamera가 적용한 값). + +- [ ] **Step 2: ExecSpace 검증/조정** — 만약 카메라 값이 적용 안 되면(스크립트가 클라에서 안 돎), `gen-camera.mjs`의 `method()` 기본 `ExecSpace`를 1↔6 등으로 바꿔 재생성·재확인. (gen-slaydeck는 6으로 클라 동작 확인됨, Monster는 클라 메서드 1.) 적용되는 값으로 확정. + +- [ ] **Step 3: (선택) framing 변경 반영 확인** — `data/camera.json`의 zoomRatio를 70으로 임시 변경 → 재생성 → Play에서 Zoom 70 확인 → 원복(`git checkout -- data/camera.json` + 재생성). + +--- + +## Self-Review +- **Spec coverage:** camera.json·codeblock(Task1), 11맵 부착(Task2), 정적검증·idempotency(Task3), 런타임·ExecSpace(Task4). 스펙 항목 매핑. +- **Placeholder scan:** 실제 코드/명령 포함. (Task4 ExecSpace 조정은 런타임 결과 의존 — 의도된 검증 분기.) +- **Type consistency:** `writeCodeblock`/`patchMap`/`method`/`prop` 일관. codeblock Id 'MapCamera' ↔ componentName `script.MapCamera` ↔ EntryKey `codeblock://mapcamera` 일치. camera.json 필드(zoomRatio/screenOffsetX/Y/confineCameraArea)가 codeblock 생성에서 사용됨. +- **리스크:** 맵 루트 스크립트의 client OnBeginPlay 실행/ExecSpace는 런타임 검증으로 확정(Task4). LocalPlayer.CameraComponent 타이밍은 재시도 타이머로 흡수. diff --git a/docs/superpowers/specs/2026-06-09-map-camera-design.md b/docs/superpowers/specs/2026-06-09-map-camera-design.md new file mode 100644 index 0000000..47724a1 --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-map-camera-design.md @@ -0,0 +1,50 @@ +# 맵별 고정 카메라 (Map Camera Anchor) — 설계 + +> 작성: 2026-06-09 / 상태: 승인됨 / 근거: MSW CameraService/CameraComponent API + map01 현재 카메라 런타임 추출. + +## 문제 + +맵별로 카메라 시점을 고정해 타일·배경·몬스터를 결정적 framing 안에 배치하고 싶다. 현재 카메라는 +플레이어(DefaultPlayer) 소유로 플레이어를 추적한다(플레이어 freeze로 사실상 고정이지만 플레이어/맵 의존). +맵별로 **명시적·데이터 제어 가능한 고정 시점**이 필요하다. + +## 추출한 현재(map01) 카메라 값 (런타임) +- ZoomRatio **100** (min 30 / max 500), CameraOffset (0,0), ScreenOffset **(0.5, 0.655)**, + ConfineCameraArea **true**, UseCustomBound false. +- 플레이어 스폰 ≈ **(-5.0, -0.04)**, 카메라 가둠 영역 LB(-8.73,-1.76)~RT(7.83,4.35). + +## 설계 + +### 구조 +- **맵별 `CameraAnchor` 엔티티**(정적): 각 맵(`/maps/mapNN/CameraAnchor`)에 추가. + - `TransformComponent`: 위치 = framing 중심(스폰 `(-5, -0.04)`). + - `CameraComponent`: ZoomRatio 100, ScreenOffset (0.5, 0.655), ConfineCameraArea true (= 현재 값). + - `script.MapCamera`: 맵 로드 시 이 카메라로 전환. + - 앵커가 움직이지 않으므로 시점 고정. 플레이어와 분리. +- **`RootDesk/MyDesk/MapCamera.codeblock`**(신규, 1개): + - `OnBeginPlay`(client): `_CameraService:SwitchCameraTo(self.Entity.CameraComponent)`. +- **`data/camera.json`**(신규): 단일 카메라 설정(zoom·screenOffset·confine·anchor pos). 맵 공통값(맵들이 map01 클론)이며, 추후 맵별 오버라이드 가능 구조. + +### 생성기 +- `tools/gen-maps.mjs`에 카메라 앵커 주입 추가: `data/camera.json` 읽어 11맵 각각에 CameraAnchor 엔티티 + (Transform+Camera+script.MapCamera) 추가. CameraComponent JSON 구조는 기존 `Global/DefaultPlayer.model`의 + CameraComponent를 복제해 값만 교체(정확한 필드 보존). +- `MapCamera.codeblock`은 `gen-maps.mjs`(또는 별도 소함수)에서 생성. ExecSpace는 클라이언트(OnBeginPlay client). + +### 데이터 흐름 +`data/camera.json` → `gen-maps.mjs`(앵커 주입) + `MapCamera.codeblock` 생성 → 맵 로드 시 +`OnBeginPlay`→`SwitchCameraTo` → 고정 시점. framing 안에 타일/배경/몬스터 배치(기존대로). + +### 조정 +- 앵커는 메이커 Explorer `/maps/mapNN/CameraAnchor`에 나타나며 Scene에서 카메라 기즈모로 표시·이동 가능. + 값은 Property Editor 또는 `data/camera.json` 수정→재생성으로. + +## 검증 (메이커 Play) +- map01 진입 시 카메라가 CameraAnchor로 전환돼 현재와 동일 framing 고정(플레이어 이동/위치 무관). +- (가능하면 map02 진입해 동일 framing 확인.) +- `node tools/gen-maps.mjs` 결정적. 맵 JSON 유효. 빌드 오류 없음. +- 앵커가 Explorer/Scene에 보이고 기즈모로 framing 확인 가능. + +## 범위 밖 (금지) +- 맵별 다른 줌/오프셋 튜닝(공통값부터), 카메라 연출(흔들림/줌인/블렌드), 노드별 맵 전환 로직, + 카드 UI/전투 로직 변경. -- 2.49.1 From fbf5cfe19f94976e1028d01da3873095f4f1374c Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 9 Jun 2026 23:22:52 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(map-camera):=20ScreenOffset=E2=86=92Cam?= =?UTF-8?q?eraOffset=EB=A1=9C=20=EC=8B=9C=EC=A0=90=20=EC=A1=B0=EC=A0=95=20?= =?UTF-8?q?+=20zoom=2090?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConfineCameraArea=true에서는 ScreenOffset이 무시됨(MSW 문서·실측 확인) → 시점 이동은 CameraOffset(월드 좌표)으로. - data/camera.json: zoomRatio 90, cameraOffsetX 1.5, cameraOffsetY -1 추가 (x+ 오른쪽 / y- 아래) - gen-camera: codeblock에 cam.CameraOffset = Vector2(...) 굽기 추가 - 메이커 Play 검증: 파이프라인(camera.json→gen-camera→reload)으로 zoom90·offset(1.5,-1) 적용, 시점이 우하단으로 이동 확인 - 참고: 시점 조정은 CameraOffset 사용(ScreenOffset은 confine=true에서 무효, 범위 0~1) Co-Authored-By: Claude Opus 4.8 (1M context) --- RootDesk/MyDesk/MapCamera.codeblock | 2 +- data/camera.json | 6 ++++-- tools/gen-camera.mjs | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/RootDesk/MyDesk/MapCamera.codeblock b/RootDesk/MyDesk/MapCamera.codeblock index 594d8e8..b9810fc 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 = 100\n\t\tcam.ScreenOffset = Vector2(0.5, 0.655)\n\t\tcam.ConfineCameraArea = true\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\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/data/camera.json b/data/camera.json index 5592b0a..62ac469 100644 --- a/data/camera.json +++ b/data/camera.json @@ -1,6 +1,8 @@ { - "zoomRatio": 100, + "zoomRatio": 90, "screenOffsetX": 0.5, "screenOffsetY": 0.655, - "confineCameraArea": true + "confineCameraArea": true, + "cameraOffsetX": 1.5, + "cameraOffsetY": -1 } diff --git a/tools/gen-camera.mjs b/tools/gen-camera.mjs index d507a96..5ac6790 100644 --- a/tools/gen-camera.mjs +++ b/tools/gen-camera.mjs @@ -63,6 +63,7 @@ local function apply() cam.ZoomRatio = ${CAM.zoomRatio} cam.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY}) cam.ConfineCameraArea = ${CAM.confineCameraArea} + cam.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY}) _TimerService:ClearTimer(eventId) elseif self.CamTries > 30 then _TimerService:ClearTimer(eventId) -- 2.49.1