맵별 고정 카메라 (MapCamera) — 11맵 카메라 framing 고정 #20
60
RootDesk/MyDesk/MapCamera.codeblock
Normal file
60
RootDesk/MyDesk/MapCamera.codeblock
Normal file
@@ -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 = 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": [],
|
||||
"Name": "OnBeginPlay"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
8
data/camera.json
Normal file
8
data/camera.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"zoomRatio": 90,
|
||||
"screenOffsetX": 0.5,
|
||||
"screenOffsetY": 0.655,
|
||||
"confineCameraArea": true,
|
||||
"cameraOffsetX": 1.5,
|
||||
"cameraOffsetY": -1
|
||||
}
|
||||
213
docs/superpowers/plans/2026-06-09-map-camera.md
Normal file
213
docs/superpowers/plans/2026-06-09-map-camera.md
Normal file
@@ -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 타이밍은 재시도 타이머로 흡수.
|
||||
50
docs/superpowers/specs/2026-06-09-map-camera-design.md
Normal file
50
docs/superpowers/specs/2026-06-09-map-camera-design.md
Normal file
@@ -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/전투 로직 변경.
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
99
tools/gen-camera.mjs
Normal file
99
tools/gen-camera.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
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}
|
||||
cam.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY})
|
||||
_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(', '));
|
||||
Reference in New Issue
Block a user