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 }; 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: '734473d91cc6440491335c204a4de087' }, ]; 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.`);