refactor(tools): .mjs를 주체별 폴더로 분류 + 카메라/플레이어 제어 분리

- tools/{player,monster,camera,map,deck,balance}/ 로 8개 스크립트 분류 (git mv 이력 보존)
- gen-camera의 플레이어 입력 차단·시선 고정을 tools/player/gen-player-lock.mjs(PlayerLock 코드블록)로 분리
- MapCamera 코드블록은 카메라 속성 전용으로 정리, 11개 맵 루트에 script.PlayerLock 부착
- README 및 스크립트 주석의 도구 경로 갱신

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:51:24 +09:00
parent f1d101f6a4
commit 124e49b938
23 changed files with 223 additions and 26 deletions

View File

@@ -76,7 +76,7 @@ slaymaple/
## 게임 프레임워크 현황
현재 전투는 `Global/common.gamelogic``/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작합니다. 모든 카드/덱/전투 관련 산출물(`ui/DefaultGroup.ui` · `RootDesk/MyDesk/SlayDeckController.codeblock` · `common.gamelogic`)은 **`tools/gen-slaydeck.mjs` 단일 소스에서 생성**됩니다(직접 편집 금지, 결정적 출력).
현재 전투는 `Global/common.gamelogic``/common` 엔티티에 부착된 **`SlayDeckController` 단일 컴포넌트**로 동작합니다. 모든 카드/덱/전투 관련 산출물(`ui/DefaultGroup.ui` · `RootDesk/MyDesk/SlayDeckController.codeblock` · `common.gamelogic`)은 **`tools/deck/gen-slaydeck.mjs` 단일 소스에서 생성**됩니다(직접 편집 금지, 결정적 출력).
| 컴포넌트 | 상태 | 역할 |
|---|---|---|
@@ -126,7 +126,7 @@ c:StartCombat() -- 전투 재시작(상태 초기화)
- [x] HP·방어도·에너지·적 의도·손패 카드를 렌더링하는 전투 UI **(완료 — `SlayDeckController` + CombatHud)**
- [x] 카드 사용이 실제 데미지/방어/적 의도/승패에 반영되는 단일 전투 루프 **(완료)**
- [ ] 카드/적 데이터를 `data/cards.json` · `data/enemies.json`로 외부화 (D)
- [ ] 전투를 N회 자동 시뮬레이션하는 밸런스 검증 도구 `tools/sim-balance.mjs` (F, D 선행)
- [ ] 전투를 N회 자동 시뮬레이션하는 밸런스 검증 도구 `tools/balance/sim-balance.mjs` (F, D 선행)
- [ ] 전투/엘리트/상점/휴식/이벤트/보스 노드를 가진 맵 노드 UI (E)
- [ ] `OnCombatStart` / `OnCardPlayed` / `OnTurnStart` / `OnCombatReward` 훅을 가진 유물 시스템 (E)
- [ ] 적 행동 패턴을 데이터로 정의 (현재 단순 결정적 의도 사이클 → 무브셋)

View File

@@ -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": [],

View File

@@ -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": []
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(', '));