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:
261
tools/deck/gen-cardhand.mjs
Normal file
261
tools/deck/gen-cardhand.mjs
Normal file
@@ -0,0 +1,261 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const FILE = 'ui/DefaultGroup.ui';
|
||||
|
||||
// ---- card data ----
|
||||
const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1.0 };
|
||||
const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1.0 };
|
||||
// image RUID = Maker 로컬 워크스페이스로 임포트한 스프라이트 (RootDesk/MyDesk/invincible belief.sprite).
|
||||
// 클라우드 계정 리소스는 로컬 워크스페이스 플레이에서 로드되지 않아, 에디터 임포트로 만든 로컬 RUID를 사용한다.
|
||||
const cards = [
|
||||
{ name: '타격', cost: '1', desc: '피해 6', tint: ATTACK },
|
||||
{ name: '타격', cost: '1', desc: '피해 6', tint: ATTACK },
|
||||
{ name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND },
|
||||
{ name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND },
|
||||
{ name: '강타', cost: '2', desc: '피해 10', tint: ATTACK, image: 'eab37efa7f0d400f94259a2df836eb8a' },
|
||||
];
|
||||
const CARD_W = 180, CARD_H = 250, CARD_SPACING = 200;
|
||||
// AlignmentType enum: Center=0, TopLeft=4, BottomCenter=6 (MSW가 이 값으로 앵커를 결정)
|
||||
const ALIGN_CENTER = 0, ALIGN_BOTTOM_CENTER = 6;
|
||||
|
||||
// ---- guid helper (deterministic, hex-safe) ----
|
||||
const guid = (n) =>
|
||||
`cad000${n.toString(16).padStart(2, '0')}-0000-4000-8000-${n.toString(16).padStart(12, '0')}`;
|
||||
|
||||
// ---- component builders ----
|
||||
function transform({ parentW, parentH, anchor, pivot, size, pos, align = 0 }) {
|
||||
const offMin = { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y };
|
||||
const offMax = { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y };
|
||||
const position = {
|
||||
x: anchor.x * parentW - parentW / 2 + pos.x,
|
||||
y: anchor.y * parentH - parentH / 2 + pos.y,
|
||||
z: 0.0,
|
||||
};
|
||||
return {
|
||||
'@type': 'MOD.Core.UITransformComponent',
|
||||
ActivePlatform: 255,
|
||||
AlignmentOption: align,
|
||||
AnchorsMax: { x: anchor.x, y: anchor.y },
|
||||
AnchorsMin: { x: anchor.x, y: anchor.y },
|
||||
MobileOnly: false,
|
||||
OffsetMax: offMax,
|
||||
OffsetMin: offMin,
|
||||
Pivot: { x: pivot.x, y: pivot.y },
|
||||
RectSize: { x: size.x, y: size.y },
|
||||
UIMode: 1,
|
||||
UIScale: { x: 1.0, y: 1.0, z: 1.0 },
|
||||
UIVersion: 2,
|
||||
anchoredPosition: { x: pos.x, y: pos.y },
|
||||
Position: position,
|
||||
QuaternionRotation: { x: 0.0, y: 0.0, z: 0.0, w: 1.0 },
|
||||
Scale: { x: 1.0, y: 1.0, z: 1.0 },
|
||||
Enable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function sprite({ dataId = '', color, type = 1, raycast = true }) {
|
||||
return {
|
||||
'@type': 'MOD.Core.SpriteGUIRendererComponent',
|
||||
AnimClipPlayType: 0,
|
||||
EndFrameIndex: 2147483647,
|
||||
ImageRUID: { DataId: dataId },
|
||||
LocalPosition: { x: 0.0, y: 0.0 },
|
||||
LocalScale: { x: 1.0, y: 1.0 },
|
||||
OverrideSorting: false,
|
||||
PlayRate: 1.0,
|
||||
PreserveSprite: 0,
|
||||
StartFrameIndex: 0,
|
||||
Color: color,
|
||||
DropShadow: false,
|
||||
DropShadowAngle: 30.0,
|
||||
DropShadowColor: { r: 0.0, g: 0.0, b: 0.0, a: 0.72 },
|
||||
DropShadowDistance: 32.0,
|
||||
FillAmount: 1.0,
|
||||
FillCenter: true,
|
||||
FillClockWise: true,
|
||||
FillMethod: 0,
|
||||
FillOrigin: 0,
|
||||
FlipX: false,
|
||||
FlipY: false,
|
||||
FrameColumn: 1,
|
||||
FrameRate: 0,
|
||||
FrameRow: 1,
|
||||
Outline: false,
|
||||
OutlineColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
||||
OutlineWidth: 3.0,
|
||||
RaycastTarget: raycast,
|
||||
Type: type,
|
||||
Enable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function text({ value, fontSize, bold, alignment = 4 }) {
|
||||
return {
|
||||
'@type': 'MOD.Core.TextComponent',
|
||||
Alignment: alignment,
|
||||
Bold: bold,
|
||||
DropShadow: false,
|
||||
DropShadowAngle: 30.0,
|
||||
DropShadowColor: { r: 0.0, g: 0.0, b: 0.0, a: 0.72 },
|
||||
DropShadowDistance: 32.0,
|
||||
Font: 0,
|
||||
FontColor: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
|
||||
FontSize: fontSize,
|
||||
MaxSize: fontSize,
|
||||
MinSize: 8,
|
||||
OutlineColor: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 },
|
||||
OutlineDistance: { x: 1.0, y: -1.0 },
|
||||
OutlineWidth: 1.0,
|
||||
Overflow: 0,
|
||||
OverrideSorting: false,
|
||||
Padding: { left: 0, right: 0, top: 0, bottom: 0 },
|
||||
SizeFit: false,
|
||||
Text: value,
|
||||
UseOutLine: true,
|
||||
Enable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function entity({ id, path, modelId, entryId, componentNames, components, displayOrder }) {
|
||||
const parts = path.split('/');
|
||||
const name = parts[parts.length - 1];
|
||||
const slashes = '/'.repeat(parts.length - 1);
|
||||
return {
|
||||
id,
|
||||
path,
|
||||
componentNames,
|
||||
jsonString: {
|
||||
name,
|
||||
path,
|
||||
nameEditable: true,
|
||||
enable: true,
|
||||
visible: true,
|
||||
localize: true,
|
||||
displayOrder,
|
||||
pathConstraints: slashes,
|
||||
revision: 1,
|
||||
origin: {
|
||||
type: 'Model',
|
||||
entry_id: entryId,
|
||||
sub_entity_id: null,
|
||||
root_entity_id: null,
|
||||
replaced_model_id: null,
|
||||
},
|
||||
modelId,
|
||||
'@components': components,
|
||||
'@version': 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---- build entities ----
|
||||
const TRANSPARENT = { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
const ents = [];
|
||||
let g = 0;
|
||||
|
||||
// CardHand container
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: '/ui/DefaultGroup/CardHand',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1020, y: 280 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
],
|
||||
}));
|
||||
|
||||
cards.forEach((c, i) => {
|
||||
const cardPath = `/ui/DefaultGroup/CardHand/Card${i + 1}`;
|
||||
const cardH = c.image ? 270 : CARD_H;
|
||||
const cardSprite = c.image
|
||||
? sprite({ dataId: c.image, color: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, type: 0, raycast: true })
|
||||
: sprite({ color: c.tint, type: 1, raycast: true });
|
||||
// card background (or full image)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 1020, parentH: 280, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: cardH }, pos: { x: (i - (cards.length - 1) / 2) * CARD_SPACING, y: 0 }, align: ALIGN_CENTER }),
|
||||
cardSprite,
|
||||
],
|
||||
}));
|
||||
// 이미지 카드는 텍스트 오버레이를 만들지 않는다 (이미지에 이미 포함)
|
||||
if (c.image) return;
|
||||
// cost (top-left)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Cost`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 50, y: 50 }, pos: { x: -60, y: 95 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.cost, fontSize: 34, bold: true }),
|
||||
],
|
||||
}));
|
||||
// name (upper-center)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Name`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 50 }, pos: { x: 0, y: 50 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.name, fontSize: 28, bold: true }),
|
||||
],
|
||||
}));
|
||||
// desc (lower-center)
|
||||
ents.push(entity({
|
||||
id: guid(g++),
|
||||
path: `${cardPath}/Desc`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 80 }, pos: { x: 0, y: -80 } }),
|
||||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||||
text({ value: c.desc, fontSize: 22, bold: false }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
// ---- splice into file ----
|
||||
let txt = readFileSync(FILE, 'utf8');
|
||||
|
||||
if (txt.includes('/ui/DefaultGroup/CardHand')) {
|
||||
console.log('CardHand already present — no changes made.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const eol = txt.includes('\r\n') ? '\r\n' : '\n'; // 기존 파일의 줄바꿈 보존
|
||||
const splicePoint = `${eol} ]`; // Entities 닫는 대괄호(4-space indent)
|
||||
|
||||
const count = txt.split(splicePoint).length - 1;
|
||||
if (count !== 1) {
|
||||
console.error(`Expected exactly one Entities closing bracket, found ${count}. Aborting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const blocks = ents
|
||||
.map((e) => JSON.stringify(e, null, 2).split('\n').map((l) => ' ' + l).join(eol))
|
||||
.join(',' + eol);
|
||||
|
||||
txt = txt.replace(splicePoint, ',' + eol + blocks + eol + ' ]');
|
||||
|
||||
JSON.parse(txt); // 유효성 검증 (실패 시 throw)
|
||||
|
||||
writeFileSync(FILE, txt, 'utf8');
|
||||
console.log(`Inserted ${ents.length} CardHand entities.`);
|
||||
1668
tools/deck/gen-slaydeck.mjs
Normal file
1668
tools/deck/gen-slaydeck.mjs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user