From af3480d8b62df5cde5a3a4a8b44b75a6eabc72ef Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 6 Jun 2026 01:38:38 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=98=EB=8B=A8=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=86=90=ED=8C=A8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/gen-cardhand.mjs | 249 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 tools/gen-cardhand.mjs diff --git a/tools/gen-cardhand.mjs b/tools/gen-cardhand.mjs new file mode 100644 index 0000000..1653e88 --- /dev/null +++ b/tools/gen-cardhand.mjs @@ -0,0 +1,249 @@ +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 }, +]; +const CARD_BG_RUID = 'cd0560c4fc7f3b14994b90a502f00a21'; // 기존 버튼 스프라이트 재사용 +const CARD_W = 180, CARD_H = 250; + +// ---- guid helper (deterministic, hex-safe) ---- +const guid = (n) => + `cad0${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 }) { + 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: 0, + 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 }, size: { x: 1020, y: 280 }, pos: { x: 0, y: 30 } }), + sprite({ color: TRANSPARENT, type: 1, raycast: false }), + ], +})); + +cards.forEach((c, i) => { + const cardPath = `/ui/DefaultGroup/CardHand/Card${i + 1}`; + // card background + 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: CARD_H }, pos: { x: (-2 + i) * 200, y: 0 } }), + sprite({ dataId: CARD_BG_RUID, color: c.tint, type: 0, raycast: true }), + ], + })); + // 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, y: 1 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 50, y: 50 }, pos: { x: 32, y: -32 } }), + 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: 1 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 50 }, pos: { x: 0, y: -70 } }), + 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 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 80 }, pos: { x: 0, y: 55 } }), + 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 matches = txt.match(/\n {4}\]/g); // Entities 닫는 대괄호(4-space indent)는 파일 내 유일 +if (!matches || matches.length !== 1) { + console.error(`Expected exactly one Entities closing bracket, found ${matches ? matches.length : 0}. Aborting.`); + process.exit(1); +} + +const blocks = ents + .map((e) => JSON.stringify(e, null, 2).split('\n').map((l) => ' ' + l).join('\n')) + .join(',\n'); + +txt = txt.replace('\n ]', ',\n' + blocks + '\n ]'); + +JSON.parse(txt); // 유효성 검증 (실패 시 throw) + +writeFileSync(FILE, txt, 'utf8'); +console.log(`Inserted ${ents.length} CardHand entities.`);