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>
341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
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 };
|