Files
maplecontest/docs/superpowers/plans/2026-06-09-map-camera.md
2026-06-09 22:24:09 +09:00

9.7 KiB

맵별 고정 카메라 (런타임 설정) 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 추출값)
{
  "zoomRatio": 100,
  "screenOffsetX": 0.5,
  "screenOffsetY": 0.655,
  "confineCameraArea": true
}
  • Step 2: tools/gen-camera.mjs 작성 — codeblock 생성 부분
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: 커밋
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 임시 호출은 제거)
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: 커밋
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: 생성물 커밋
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)로 현재 카메라 값 확인:

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.mjsmethod() 기본 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 타이밍은 재시도 타이머로 흡수.