diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index c583277..8282454 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -2,345 +2,7 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs'; -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: 1, - 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); -} - +import { 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 } from './lib/ui-helpers.mjs'; function upsertUi() { const ui = JSON.parse(readFileSync(UI_FILE, 'utf8')); const E = ui.ContentProto.Entities; diff --git a/tools/deck/lib/ui-helpers.mjs b/tools/deck/lib/ui-helpers.mjs new file mode 100644 index 0000000..27b7381 --- /dev/null +++ b/tools/deck/lib/ui-helpers.mjs @@ -0,0 +1,341 @@ +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: 1, + 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 };