refactor(gen): lib/ui-helpers.mjs로 UI 헬퍼·상수 추출 (출력 바이트 동일)
UI_FILE~appendUiSection(상수 30 + 헬퍼 15, 총 45)을 tools/deck/lib/ui-helpers.mjs로 이동, import로 연결. 산출물 무변경(diffcheck IDENTICAL). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
341
tools/deck/lib/ui-helpers.mjs
Normal file
341
tools/deck/lib/ui-helpers.mjs
Normal file
@@ -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 };
|
||||
Reference in New Issue
Block a user