const UI_FILE = 'ui/DefaultGroup.ui'; const COMMON_FILE = 'Global/common.gamelogic'; const UI_ROOT = '/ui/DefaultGroup'; const GENERATED_UI_SECTIONS = [ 'DeckHud', 'DeckInspectHud', 'DeckAllHud', 'CombatHud', 'RewardHud', 'MapHud', 'ShopHud', 'RestHud', 'TreasureHud', 'JobChoiceHud', 'JobSelectHud', 'MainMenu', 'CharacterSelectHud', 'LobbyHud', 'BoardHud', 'SoulShopHud', ]; const UI_APPEND_ORDER = [ 'DeckHud', 'CombatHud', 'RewardHud', 'MapHud', 'ShopHud', 'RestHud', 'TreasureHud', 'JobChoiceHud', 'JobSelectHud', 'DeckInspectHud', 'DeckAllHud', 'MainMenu', 'CharacterSelectHud', 'LobbyHud', 'BoardHud', 'SoulShopHud', ]; const DISABLED_STOCK_CONTROLS = ['Button_Attack', 'Button_Jump', 'UIJoystick']; const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 }; const DARK = { r: 0.08, g: 0.09, b: 0.11, a: 0.92 }; const GOLD = { r: 0.94, g: 0.74, b: 0.26, a: 1 }; const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1 }; const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1 }; const SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 }; const DAMAGE_DIGIT_RUIDS = [ 'b94c19830538447f81617035d89bcc05', '01b023122a6f4a5789e1d4c61ff8f430', '57ff71d1b9eb471b9feb1c15348770c9', 'cab92837798a42ad9143c67e93f999e1', '366f271f9ca94a0684083aad9298efad', '5c7a6ad38491466aa84bf450e0fdcf25', '7d82a6838e1b4f4a8a0f7420db34c985', 'c0765bb1e47d46ffbe1df4ac19ea9b1b', '6ea0bfed61e149f88a9b3f22dd79774f', '82ad2acaae4e4b3fb87bf73635250d22', ]; const DAMAGE_POP_MAX_DIGITS = 5; const DAMAGE_POP_DIGIT_W = 22; const DAMAGE_POP_DIGIT_H = 32; const DAMAGE_POP_DIGIT_SPACING = -4; const MAX_MONSTERS = 4; const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산 const HP_BAR_W = 140; const WHITE = { r: 1, g: 1, b: 1, a: 1 }; const CARD_NAME_TEXT = { r: 1, g: 0.92, b: 0.62, a: 1 }; const CARD_DESC_TEXT = { r: 0.98, g: 0.96, b: 0.9, a: 1 }; // 카드 프레임(1054×1492 원본) 슬롯 레이아웃 — 픽셀 실측을 180×250 카드 좌표로 환산한 기준값을 폭 비례 스케일. // 실측(워리어·메이지·밴딧 공통): 육각 중심 (120,127)→(-70,104) · 배너 본체 y55..165, x215..1015→중심 (+15,+107) // · 설명 박스 y~1030..1480→중심 (0,-86) · 아트 영역 y260..1030→중심 (0,+17) function cardFaceLayout(W) { const s = W / 180; const r = (v) => Math.round(v * s); return { texts: [ ['Cost', { size: { x: r(40), y: r(40) }, pos: { x: r(-70), y: r(104) }, fontSize: r(24), bold: true, color: WHITE, dropShadow: false, outlineWidth: 2 }], ['Name', { size: { x: r(142), y: r(28) }, pos: { x: r(15), y: r(106) }, fontSize: r(17), bold: true, color: CARD_NAME_TEXT, dropShadow: false, outlineWidth: 2 }], ['Desc', { size: { x: r(158), y: r(78) }, pos: { x: 0, y: r(-82) }, fontSize: r(14), bold: true, color: CARD_DESC_TEXT, dropShadow: false, outlineWidth: 2 }], ], art: { size: { x: r(112), y: r(112) }, pos: { x: 0, y: r(17) } }, }; } const CARD_W = 180; const CARD_H = 250; const CARD_SPACING = 200; const CARD_XS = [-400, -200, 0, 200, 400]; const ALIGN_CENTER = 0; const ALIGN_BOTTOM_CENTER = 6; function guid(prefix, n) { // 유효한 8-4-4-4-12 hex GUID 생성. prefix는 충돌 방지용 네임스페이스 바이트로 매핑. const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : prefix === 'job' ? 0xe4 : prefix === 'ins2' ? 0xe5 : prefix === 'all2' ? 0xe6 : prefix === 'rwd2' ? 0xe7 : prefix === 'shp2' ? 0xe8 : prefix === 'lob' ? 0xe9 : prefix === 'brd' ? 0xea : prefix === 'soul' ? 0xeb : 0xfe; const v = (ns * 0x100000 + n) >>> 0; return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`; } 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 }; return { '@type': 'MOD.Core.UITransformComponent', ActivePlatform: 255, AlignmentOption: align, AnchorsMax: anchor, AnchorsMin: anchor, MobileOnly: false, OffsetMax: offMax, OffsetMin: offMin, Pivot: pivot, RectSize: size, UIMode: 1, UIScale: { x: 1, y: 1, z: 1 }, UIVersion: 2, anchoredPosition: pos, Position: { x: anchor.x * parentW - parentW / 2 + pos.x, y: anchor.y * parentH - parentH / 2 + pos.y, z: 0 }, QuaternionRotation: { x: 0, y: 0, z: 0, w: 1 }, Scale: { x: 1, y: 1, z: 1 }, Enable: true, }; } function sprite({ dataId = '', color = TRANSPARENT, type = 1, raycast = false }) { return { '@type': 'MOD.Core.SpriteGUIRendererComponent', AnimClipPlayType: 0, EndFrameIndex: 2147483647, ImageRUID: { DataId: dataId }, LocalPosition: { x: 0, y: 0 }, LocalScale: { x: 1, y: 1 }, OverrideSorting: false, PlayRate: 1, PreserveSprite: 0, StartFrameIndex: 0, Color: color, DropShadow: false, DropShadowAngle: 30, DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, DropShadowDistance: 32, FillAmount: 1, FillCenter: true, FillClockWise: true, FillMethod: 0, FillOrigin: 0, FlipX: false, FlipY: false, FrameColumn: 1, FrameRate: 0, FrameRow: 1, Outline: false, OutlineColor: { r: 0, g: 0, b: 0, a: 1 }, OutlineWidth: 3, RaycastTarget: raycast, Type: type, Enable: true, }; } function button({ enabled = true } = {}) { return { '@type': 'MOD.Core.ButtonComponent', Colors: { NormalColor: { r: 1, g: 1, b: 1, a: 1 }, HighlightedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, PressedColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 1 }, SelectedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, DisabledColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 0.5019608 }, ColorMultiplier: 1, FadeDuration: 0.1, }, ImageRUIDs: { HighlightedSprite: null, PressedSprite: null, SelectedSprite: null, DisabledSprite: null, }, KeyCode: 0, OverrideSorting: false, Transition: 1, Enable: enabled, }; } function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4, dropShadow = false, outlineWidth = 1 }) { return { '@type': 'MOD.Core.TextComponent', Alignment: alignment, Bold: bold, DropShadow: dropShadow, DropShadowAngle: 30, DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, DropShadowDistance: dropShadow ? 18 : 32, Font: 0, FontColor: color, FontSize: fontSize, MaxSize: fontSize, MinSize: 8, OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 }, OutlineDistance: { x: 1, y: -1 }, OutlineWidth: outlineWidth, Overflow: 0, OverrideSorting: false, Padding: { left: 0, right: 0, top: 0, bottom: 0 }, SizeFit: false, Text: value, UseOutLine: true, Enable: true, }; } function scrollLayoutGroup({ cellSize, spacing, columns }) { return { '@type': 'MOD.Core.ScrollLayoutGroupComponent', CellSize: cellSize, ChildAlignment: 0, Constraint: 1, ConstraintCount: columns, GridChildAlignment: 0, GridSpacing: spacing, HorizontalScrollBarDirection: 0, IgnoreMapLayerCheck: false, OrderInLayer: 0, OverrideSorting: false, Padding: { left: 16, right: 16, top: 16, bottom: 16 }, ReverseArrangement: false, ScrollBarBackgroundColor: { r: 1, g: 1, b: 1, a: 0.18 }, ScrollBarBgImageRUID: { DataId: '' }, ScrollBarHandleColor: { r: 0.94, g: 0.74, b: 0.26, a: 0.9 }, ScrollBarHandleImageRUID: { DataId: '' }, ScrollBarThickness: 12, ScrollBarVisible: 2, SortingLayer: 'UI', Spacing: 0, StartAxis: 0, StartCorner: 0, Type: 2, UseScroll: true, VerticalScrollBarDirection: 1, Enable: true, }; } function popupLayerFor(path) { if (path.startsWith('/ui/DefaultGroup/DeckAllHud')) return { root: '/ui/DefaultGroup/DeckAllHud', base: 4000 }; if (path.startsWith('/ui/DefaultGroup/DeckInspectHud')) return { root: '/ui/DefaultGroup/DeckInspectHud', base: 3000 }; return null; } function uiOrderFor(path, displayOrder) { const popup = popupLayerFor(path); if (popup != null) { const relative = path.slice(popup.root.length).split('/').filter(Boolean); return popup.base + relative.length * 100 + displayOrder; } return displayOrder; } function displayOrderFor(path, displayOrder) { return uiOrderFor(path, displayOrder); } function applySortingOverride(path, components, displayOrder) { if (popupLayerFor(path) == null) return components; const order = uiOrderFor(path, displayOrder); return components.map((component) => { if (component['@type'] !== 'MOD.Core.SpriteGUIRendererComponent' && component['@type'] !== 'MOD.Core.TextComponent') { return component; } return { ...component, OverrideSorting: true, SortingLayer: 'UI', OrderInLayer: order, }; }); } function entity({ id, path, modelId, entryId, componentNames, components, displayOrder }) { const parts = path.split('/'); const name = parts[parts.length - 1]; const sortedComponents = applySortingOverride(path, components, displayOrder); return { id, path, componentNames, jsonString: { name, path, nameEditable: true, enable: true, visible: true, localize: true, displayOrder: displayOrderFor(path, displayOrder), pathConstraints: '/'.repeat(parts.length - 1), revision: 1, origin: { type: 'Model', entry_id: entryId, sub_entity_id: null, root_entity_id: null, replaced_model_id: null, }, modelId, '@components': sortedComponents, '@version': 1, }, }; } function uiPath(...parts) { return [UI_ROOT, ...parts].join('/'); } function sectionRoot(section) { return uiPath(section); } function isGeneratedUiEntity(e) { return GENERATED_UI_SECTIONS.some((section) => e.path.startsWith(sectionRoot(section))); } function appendUiSection(ui, section, entities) { if (!GENERATED_UI_SECTIONS.includes(section)) { throw new Error(`[gen-slaydeck] unknown generated UI section: ${section}`); } const root = sectionRoot(section); for (const e of entities) { if (!e.path.startsWith(root)) { throw new Error(`[gen-slaydeck] ${section} section emitted unexpected path: ${e.path}`); } } ui.ContentProto.Entities.push(...entities); } export { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection };