Files
maplecontest/tools/deck/lib/ui-helpers.mjs
gahusb 420cce561c Merge origin/main into feature/gen-modularization (생성기 모듈화)
main의 5개 PR(#62 exhaust+tooltip, #66 dex/thorns, #67 캐릭터 덱버튼 제거,
#68 스크롤바, #69 표창카드)을 모듈화 브랜치에 병합.

충돌은 tools/deck/gen-slaydeck.mjs 한 파일 — main이 그 단일체를 콘텐츠 변경
(구조/emit/top-level 상수는 불변)한 반면 본 브랜치는 모듈로 재구조화.
해결: main 버전(theirs)을 취해 **콘텐츠 마커 기반으로 재모듈화**(라인인덱스 X,
이름 자동 파생) → lib/data·lib/ui-helpers + hud/*.mjs 16종 재생성.

검증(손실 0): 재모듈화 생성기 출력이 origin/main 산출물과 **바이트 동일**
(diffcheck: ui/DefaultGroup.ui·SlayDeckController.codeblock IDENTICAL).
common.gamelogic은 origin/main 그대로 채택(유일 차이는 main의 stale `.0` 정수표기
— origin/main 원본 생성기도 정수를 만듦을 확인). 미러 테스트 sim-balance·rogue-map 통과.
RULES.md는 §1(모듈구조)+§4/§7(main) 자동 병합.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 07:38:54 +09:00

341 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };