import { readFileSync, writeFileSync } from 'node:fs'; const CARDS = JSON.parse(readFileSync('data/cards.json', 'utf8')); const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8')); // 검증 (fail-fast): 잘못된 데이터면 생성 중단 const CLASSES = { warrior: { label: '전사', maxHp: 80 }, magician: { label: '마법사', maxHp: 70 }, }; for (const cls of Object.keys(CLASSES)) { if (!CARDS.starterDecks?.[cls]) throw new Error(`[gen-slaydeck] starterDecks.${cls} 없음`); for (const id of CARDS.starterDecks[cls]) { if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`); } } // 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드) const JOBS = { warrior: [ { id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' }, { id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' }, { id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' }, ], magician: [ { id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' }, { id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' }, { id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' }, ], }; for (const [cls, jobs] of Object.entries(JOBS)) { for (const j of jobs) { if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`); } } if (!ENEMIES.enemies[ENEMIES.activeEnemy]) { throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`); } // 카드 프레임 (사용자 제작 이미지 — 로컬 임포트 .sprite RUID, 직업 3종 × 등급 3종) const CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json', 'utf8')); const RARITIES = ['normal', 'unique', 'legend']; for (const [fid, fr] of Object.entries(CARDFRAMES.frames)) { for (const r of RARITIES) { if (!fr[r]) throw new Error(`[gen-slaydeck] cardframes.frames.${fid}.${r} RUID 없음`); } } for (const [id, c] of Object.entries(CARDS.cards)) { if (!RARITIES.includes(c.rarity)) throw new Error(`[gen-slaydeck] 카드 ${id} rarity 누락/오류: ${c.rarity}`); const fc = CARDFRAMES.classToFrame[c.class]; if (!fc || !CARDFRAMES.frames[fc]) throw new Error(`[gen-slaydeck] 카드 ${id} class ${c.class} → 프레임 매핑 없음`); } function frameRuid(card) { return CARDFRAMES.frames[CARDFRAMES.classToFrame[card.class]][card.rarity]; } function luaFramesTable() { const frames = Object.entries(CARDFRAMES.frames).map(([fid, fr]) => `\t${fid} = { normal = ${luaStr(fr.normal)}, unique = ${luaStr(fr.unique)}, legend = ${luaStr(fr.legend)} },`).join('\n'); const cls = Object.entries(CARDFRAMES.classToFrame).map(([c, f]) => `\t${c} = ${luaStr(f)},`).join('\n'); return `self.CardFrames = {\n${frames}\n}\nself.ClassToFrame = {\n${cls}\n}`; } // 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨. const MAP_ROWS = 7; // 걷는 행 1..7, 보스 row 8 const MAP_COLS = 4; // 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별) const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71'; const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759'; const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8')); if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`); for (const id of RELICS.relicPool) { if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`); } function luaRelicsTable(relics) { const lines = Object.entries(relics).map(([id, r]) => `\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`); return `self.Relics = {\n${lines.join('\n')}\n}`; } const POTIONS = JSON.parse(readFileSync('data/potions.json', 'utf8')); for (const [pid, p] of Object.entries(POTIONS.potions)) { if (!p.name || !p.effect || p.value == null) throw new Error(`[gen-slaydeck] potion 필드 누락: ${pid}`); } function luaPotionsTable(potions) { const lines = Object.entries(potions).map(([id, p]) => `\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`); return `self.Potions = {\n${lines.join('\n')}\n}`; } function luaIntentsArray(intents) { return '{ ' + intents.map((it) => { const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value}`]; if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`); return `{ ${fields.join(', ')} }`; }).join(', ') + ' }'; } function luaEnemiesTable(enemies) { const lines = Object.entries(enemies).map(([id, e]) => `\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`); return `self.Enemies = {\n${lines.join('\n')}\n}`; } // Lua 직렬화 헬퍼 function luaStr(s) { return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"'; } function luaJobsTable(jobs) { const cls = Object.entries(jobs).map(([clsId, list]) => { const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n'); return `\t${clsId} = {\n${items}\n\t},`; }).join('\n'); return `self.Jobs = {\n${cls}\n}`; } function luaCardsTable(cards) { const lines = Object.entries(cards).map(([id, c]) => { const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`]; if (c.damage != null) fields.push(`damage = ${c.damage}`); if (c.block != null) fields.push(`block = ${c.block}`); if (c.strength != null) fields.push(`strength = ${c.strength}`); if (c.weak != null) fields.push(`weak = ${c.weak}`); if (c.vuln != null) fields.push(`vuln = ${c.vuln}`); if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`); if (c.value != null) fields.push(`value = ${c.value}`); if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`); fields.push(`class = ${luaStr(c.class)}`); fields.push(`rarity = ${luaStr(c.rarity)}`); if (c.hits != null) fields.push(`hits = ${c.hits}`); if (c.pierce === true) fields.push('pierce = true'); if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`); if (c.draw != null) fields.push(`draw = ${c.draw}`); if (c.heal != null) fields.push(`heal = ${c.heal}`); if (c.poison != null) fields.push(`poison = ${c.poison}`); if (c.aoe === true) fields.push('aoe = true'); if (c.image != null) fields.push(`image = ${luaStr(c.image)}`); return `\t${id} = { ${fields.join(', ')} },`; }); return `self.Cards = {\n${lines.join('\n')}\n}`; } function luaDeckTable(deck) { return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`; } 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', ]; const UI_APPEND_ORDER = [ 'DeckHud', 'CombatHud', 'RewardHud', 'MapHud', 'ShopHud', 'RestHud', 'TreasureHud', 'JobChoiceHud', 'JobSelectHud', 'DeckInspectHud', 'DeckAllHud', 'MainMenu', 'CharacterSelectHud', ]; 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 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 INK = { r: 0.13, g: 0.11, b: 0.09, a: 1 }; // 밝은 배너·설명 박스 위 먹색 글자 // 카드 프레임(263×366 원본, 0.72 비율) 슬롯 레이아웃 — 180×250 기준값을 폭 비례 스케일 function cardFaceLayout(W) { const s = W / 180; const r = (v) => Math.round(v * s); return { texts: [ ['Cost', { size: { x: r(44), y: r(44) }, pos: { x: r(-68), y: r(103) }, fontSize: r(26), bold: true, color: WHITE }], ['Name', { size: { x: r(150), y: r(26) }, pos: { x: r(4), y: r(97) }, fontSize: r(18), bold: true, color: INK }], ['Desc', { size: { x: r(152), y: r(64) }, pos: { x: 0, y: r(-85) }, fontSize: r(16), bold: false, color: INK }], ], art: { size: { x: r(110), y: r(110) }, pos: { x: 0, y: r(16) } }, }; } 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 : 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 }) { return { '@type': 'MOD.Core.TextComponent', Alignment: alignment, Bold: bold, DropShadow: false, DropShadowAngle: 30, DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, DropShadowDistance: 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: 1, 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); } function upsertUi() { const ui = JSON.parse(readFileSync(UI_FILE, 'utf8')); const E = ui.ContentProto.Entities; // CardHand는 스톡 섹션이라 과거 생성된 단색판(NamePlate/CostPlate)이 잔존 → 프레임 이미지 도입으로 제거 const obsoletePlate = /^\/ui\/DefaultGroup\/CardHand\/Card\d+\/(NamePlate|CostPlate)$/; ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e) && !obsoletePlate.test(e.path)); const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e])); const uiSections = new Map(); const emit = (section, entities) => { if (uiSections.has(section)) { throw new Error(`[gen-slaydeck] duplicate generated UI section: ${section}`); } uiSections.set(section, entities); }; for (const path of DISABLED_STOCK_CONTROLS.map((name) => uiPath(name))) { const e = byPath.get(path); if (e != null) { e.jsonString.enable = false; e.jsonString.visible = false; for (const component of e.jsonString['@components'] || []) { component.Enable = false; if (component.RaycastTarget != null) component.RaycastTarget = false; } } } // 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시 const previewIds = Object.keys(CARDS.cards); const cards = Array.from({ length: 5 }, (_, i) => { const c = CARDS.cards[previewIds[i % previewIds.length]]; return { name: c.name, cost: String(c.cost), desc: c.desc, frame: frameRuid(c) }; }); for (let i = 1; i <= 5; i++) { const card = byPath.get(`/ui/DefaultGroup/CardHand/Card${i}`); if (!card) continue; const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent'); const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent'); tr.RectSize = { x: CARD_W, y: CARD_H }; tr.anchoredPosition = { x: CARD_XS[i - 1], y: 0 }; tr.OffsetMin = { x: CARD_XS[i - 1] - CARD_W / 2, y: -CARD_H / 2 }; tr.OffsetMax = { x: CARD_XS[i - 1] + CARD_W / 2, y: CARD_H / 2 }; sp.ImageRUID = { DataId: cards[i - 1].frame }; sp.Type = 0; sp.Color = WHITE; sp.RaycastTarget = true; const comps = card.jsonString['@components']; if (!comps.some((c) => c['@type'] === 'MOD.Core.ButtonComponent')) { comps.push(button()); } if (!card.componentNames.includes('MOD.Core.ButtonComponent')) { card.componentNames += ',MOD.Core.ButtonComponent'; } if (!comps.some((c) => c['@type'] === 'MOD.Core.UITouchReceiveComponent')) { comps.push({ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }); } if (!card.componentNames.includes('MOD.Core.UITouchReceiveComponent')) { card.componentNames += ',MOD.Core.UITouchReceiveComponent'; } card.jsonString.enable = true; card.jsonString.visible = true; const handLayout = cardFaceLayout(CARD_W); const previewValues = { Cost: cards[i - 1].cost, Name: cards[i - 1].name, Desc: cards[i - 1].desc }; const children = handLayout.texts.map(([suffix, cfg]) => [suffix, { ...cfg, value: previewValues[suffix] }]); for (const [suffix, cfg] of children) { const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`; const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; let child = byPath.get(path); if (!child) { child = entity({ id: guid('dck', i * 10 + children.findIndex(([s]) => s === suffix)), path, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: dOrder, components: [ transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), sprite({ color: TRANSPARENT }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), ], }); ui.ContentProto.Entities.push(child); byPath.set(path, child); } else { child.id = guid('dck', i * 10 + children.findIndex(([s]) => s === suffix)); child.jsonString.enable = true; child.jsonString.visible = true; child.jsonString.displayOrder = dOrder; const ctr = child.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent'); if (ctr) { const pivot = { x: 0.5, y: 0.5 }; ctr.RectSize = cfg.size; ctr.anchoredPosition = cfg.pos; ctr.OffsetMin = { x: cfg.pos.x - pivot.x * cfg.size.x, y: cfg.pos.y - pivot.y * cfg.size.y }; ctr.OffsetMax = { x: cfg.pos.x + (1 - pivot.x) * cfg.size.x, y: cfg.pos.y + (1 - pivot.y) * cfg.size.y }; } child.jsonString['@components'][2].Text = cfg.value; child.jsonString['@components'][2].FontSize = cfg.fontSize; child.jsonString['@components'][2].MaxSize = cfg.fontSize; child.jsonString['@components'][2].FontColor = cfg.color; } } // 프레임 이미지가 이름판·코스트판을 내장하므로 Art만 유지 (잔존 NamePlate/CostPlate는 upsertUi 초입에서 제거) const frameKids = [ ['Art', 5, handLayout.art, WHITE, 0], ]; for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) { const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`; let fe = byPath.get(fPath); if (!fe) { fe = entity({ id: guid('dck', 200 + i * 10 + dOrder), path: fPath, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: dOrder, components: [ transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), sprite({ color, type: spriteType, raycast: false }), ], }); ui.ContentProto.Entities.push(fe); byPath.set(fPath, fe); } else { const ftr = fe.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent'); if (ftr) { ftr.RectSize = cfg.size; ftr.anchoredPosition = cfg.pos; ftr.OffsetMin = { x: cfg.pos.x - cfg.size.x / 2, y: cfg.pos.y - cfg.size.y / 2 }; ftr.OffsetMax = { x: cfg.pos.x + cfg.size.x / 2, y: cfg.pos.y + cfg.size.y / 2 }; } } } } const hud = []; const add = (e) => hud.push(e); add(entity({ id: guid('hud', 0), path: '/ui/DefaultGroup/DeckHud', modelId: 'uiempty', entryId: 'UIEmpty', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 5, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }), sprite({ color: TRANSPARENT }), ], })); for (const pile of [ { key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.20, b: 0.25, a: 1 } }, { key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } }, ]) { add(entity({ id: guid('hud', hud.length), path: `/ui/DefaultGroup/DeckHud/${pile.key}`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: pile.key === 'DrawPile' ? 0 : 1, components: [ transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }), sprite({ color: pile.color, type: 1, raycast: true }), button(), ], })); add(entity({ id: guid('hud', hud.length), path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }), sprite({ color: TRANSPARENT }), text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }), ], })); add(entity({ id: guid('hud', hud.length), path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }), sprite({ color: TRANSPARENT }), text({ value: pile.count, fontSize: 42, bold: true }), ], })); } add(entity({ id: guid('hud', hud.length), path: '/ui/DefaultGroup/DeckHud/EndTurnButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 160 }, align: ALIGN_CENTER }), sprite({ color: DARK, type: 1, raycast: true }), button(), text({ value: '턴 종료', fontSize: 28, bold: true, color: GOLD, alignment: 0 }), ], })); add(entity({ id: guid('hud', hud.length), path: '/ui/DefaultGroup/DeckHud/EnergyOrb', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 3, components: [ transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 160 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }), ], })); add(entity({ id: guid('hud', hud.length), path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }), sprite({ color: TRANSPARENT }), text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }), ], })); add(entity({ id: guid('hud', hud.length), path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }), sprite({ color: TRANSPARENT }), text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }), ], })); emit('DeckHud', hud); const inspect = []; const inspectHud = entity({ id: guid('ins', 0), path: '/ui/DefaultGroup/DeckInspectHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 15, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }), ], }); inspectHud.jsonString.enable = false; inspect.push(inspectHud); inspect.push(entity({ id: guid('ins', 1), path: '/ui/DefaultGroup/DeckInspectHud/Panel', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1040, y: 760 }, pos: { x: 0, y: 10 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }), ], })); inspect.push(entity({ id: guid('ins', 2), path: '/ui/DefaultGroup/DeckInspectHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 54 }, pos: { x: 0, y: 350 } }), sprite({ color: TRANSPARENT }), text({ value: '\uB371 \uBCF4\uAE30', fontSize: 34, bold: true, color: GOLD, alignment: 4 }), ], })); inspect.push(entity({ id: guid('ins', 3), path: '/ui/DefaultGroup/DeckInspectHud/Close', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 466, y: 350 } }), sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }), button(), text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), ], })); inspect.push(entity({ id: guid('ins', 4), path: '/ui/DefaultGroup/DeckInspectHud/Empty', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }), sprite({ color: TRANSPARENT }), text({ value: '\uCE74\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }), ], })); inspect.push(entity({ id: guid('ins', 5), path: '/ui/DefaultGroup/DeckInspectHud/Grid', modelId: 'uiempty', entryId: 'UIEmpty', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent', displayOrder: 4, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 950, y: 610 }, pos: { x: 0, y: 0 } }), sprite({ color: TRANSPARENT, type: 1, raycast: true }), scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }), ], })); let insN = 6; const INSPECT_CARD_COUNT = 60; const INSPECT_CARD_W = 158; const INSPECT_CARD_H = 214; for (let i = 1; i <= INSPECT_CARD_COUNT; i++) { const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`; const card = entity({ id: guid('ins', insN++), path: cardPath, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: i, components: [ transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }), sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }), ], }); card.jsonString.enable = false; inspect.push(card); const inspectLayout = cardFaceLayout(INSPECT_CARD_W); for (const [suffix, cfg] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }])) { const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; inspect.push(entity({ id: guid('ins', insN++), path: `${cardPath}/${suffix}`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: dOrder, components: [ transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), sprite({ color: TRANSPARENT }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), ], })); } inspect.push(entity({ id: guid('ins', insN++), path: `${cardPath}/Art`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 5, components: [ transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: inspectLayout.art.size, pos: inspectLayout.art.pos }), sprite({ color: WHITE, type: 0, raycast: false }), ], })); } emit('DeckInspectHud', inspect); const allDeck = []; const allHud = entity({ id: guid('all', 0), path: '/ui/DefaultGroup/DeckAllHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 16, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }), ], }); allHud.jsonString.enable = false; allDeck.push(allHud); allDeck.push(entity({ id: guid('all', 1), path: '/ui/DefaultGroup/DeckAllHud/Panel', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1080, y: 800 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }), ], })); allDeck.push(entity({ id: guid('all', 2), path: '/ui/DefaultGroup/DeckAllHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 54 }, pos: { x: 0, y: 380 } }), sprite({ color: TRANSPARENT }), text({ value: '모든 덱', fontSize: 34, bold: true, color: GOLD, alignment: 4 }), ], })); allDeck.push(entity({ id: guid('all', 3), path: '/ui/DefaultGroup/DeckAllHud/Close', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 486, y: 380 } }), sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }), button(), text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), ], })); allDeck.push(entity({ id: guid('all', 4), path: '/ui/DefaultGroup/DeckAllHud/Empty', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 40 } }), sprite({ color: TRANSPARENT }), text({ value: '덱이 없습니다', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }), ], })); allDeck.push(entity({ id: guid('all', 5), path: '/ui/DefaultGroup/DeckAllHud/Grid', modelId: 'uiempty', entryId: 'UIEmpty', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent', displayOrder: 4, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 980, y: 620 }, pos: { x: 0, y: 0 } }), sprite({ color: TRANSPARENT, type: 1, raycast: true }), scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }), ], })); let allN = 6; const ALL_DECK_CARD_COUNT = 120; const ALL_DECK_CARD_W = 158; const ALL_DECK_CARD_H = 214; for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) { const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`; const card = entity({ id: guid('all', allN++), path: cardPath, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: i, components: [ transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }), sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }), ], }); card.jsonString.enable = false; allDeck.push(card); const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W); for (const [suffix, cfg] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }])) { const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; allDeck.push(entity({ id: guid('all', allN++), path: `${cardPath}/${suffix}`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: dOrder, components: [ transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), sprite({ color: TRANSPARENT }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), ], })); } allDeck.push(entity({ id: guid('all', allN++), path: `${cardPath}/Art`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 5, components: [ transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: allDeckLayout.art.size, pos: allDeckLayout.art.pos }), sprite({ color: WHITE, type: 0, raycast: false }), ], })); } emit('DeckAllHud', allDeck); const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 }; const combat = []; combat.push(entity({ id: guid('cmb', 0), path: '/ui/DefaultGroup/CombatHud', modelId: 'uiempty', entryId: 'UIEmpty', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 4, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), ], })); const SLOT_W = 140, SLOT_H = 96; for (let i = 1; i <= MAX_MONSTERS; i++) { const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`; const slot = entity({ id: guid('cmb', 40 + i), path: base, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: 20 + i, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }), sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }), button(), ], }); slot.jsonString.enable = false; combat.push(slot); const targetFrame = entity({ id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 0, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }), sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }), ], }); targetFrame.jsonString.enable = false; combat.push(targetFrame); const actFrame = entity({ id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 0, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }), sprite({ color: { r: 0.95, g: 0.3, b: 0.25, a: 0.3 }, type: 1 }), ], }); actFrame.jsonString.enable = false; combat.push(actFrame); combat.push(entity({ id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 30 }, pos: { x: 0, y: 34 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }), ], })); combat.push(entity({ id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 26 }, pos: { x: 0, y: 6 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); combat.push(entity({ id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 3, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }), sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }), ], })); combat.push(entity({ id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 4, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }), sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }), ], })); combat.push(entity({ id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 5, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 40, y: 24 }, pos: { x: 0, y: -36 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }), ], })); const dmgPop = entity({ id: guid('cmb', 250 + i), path: `${base}/DmgPop`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 9, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 30 }, pos: { x: 0, y: 60 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 24, bold: true, color: { r: 1, g: 0.35, b: 0.3, a: 1 }, alignment: 4 }), ], }); dmgPop.jsonString.enable = false; combat.push(dmgPop); const mBlockBadge = entity({ id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 6, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 36 }, pos: { x: -HP_BAR_W / 2 - 30, y: -14 } }), sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }), ], }); mBlockBadge.jsonString.enable = false; combat.push(mBlockBadge); combat.push(entity({ id: guid('cmb', 280 + i), path: `${base}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 40, parentH: 36, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 32 }, pos: { x: 0, y: 0 } }), sprite({ color: TRANSPARENT }), text({ value: '0', fontSize: 17, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); combat.push(entity({ id: guid('cmb', 290 + i), path: `${base}/Buffs`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 7, components: [ transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 60, y: 22 }, pos: { x: 0, y: -58 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 15, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }), ], })); } const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel'; combat.push(entity({ id: guid('cmb', 210), path: PP, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 5, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 96 }, pos: { x: -760, y: -494 }, align: ALIGN_CENTER }), sprite({ color: PANEL_BG, type: 1 }), ], })); combat.push(entity({ id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }), sprite({ color: TRANSPARENT }), text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }), ], })); combat.push(entity({ id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 1, components: [ transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }), sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }), ], })); combat.push(entity({ id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 2, components: [ transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 14 }, pos: { x: -94, y: -6 } }), sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }), ], })); combat.push(entity({ id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }), sprite({ color: TRANSPARENT }), text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); const blockBadge = entity({ id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 4, components: [ transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }), sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }), ], }); blockBadge.jsonString.enable = false; combat.push(blockBadge); combat.push(entity({ id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }), sprite({ color: TRANSPARENT }), text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); combat.push(entity({ id: guid('cmb', 217), path: `${PP}/Buffs`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 6, components: [ transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 22 }, pos: { x: 0, y: -44 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 14, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }), ], })); const playerDmgPop = entity({ id: guid('cmb', 260), path: `${PP}/DmgPop`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 5, components: [ transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 30 }, pos: { x: 16, y: 40 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 22, bold: true, color: { r: 1, g: 0.4, b: 0.35, a: 1 }, alignment: 4 }), ], }); playerDmgPop.jsonString.enable = false; combat.push(playerDmgPop); combat.push(entity({ id: guid('cmb', 200), path: '/ui/DefaultGroup/CombatHud/TopBar', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 9, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }), ], })); const topTexts = [ ['Floor', -520, 160, '막 1/3', GOLD], ['Gold', -360, 160, '메소 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }], ]; topTexts.forEach(([suffix, x, w, value, color], ti) => { combat.push(entity({ id: guid('cmb', 201 + ti), path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: ti, components: [ transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }), sprite({ color: TRANSPARENT }), text({ value, fontSize: 22, bold: true, color, alignment: 4 }), ], })); }); for (let i = 1; i <= 10; i++) { combat.push(entity({ id: guid('cmb', 300 + i), path: `/ui/DefaultGroup/CombatHud/TopBar/RelicSlot${i}`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent', displayOrder: 3 + i, components: [ transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: -240 + (i - 1) * 48, y: 0 } }), sprite({ color: { r: 0.15, g: 0.16, b: 0.2, a: 0.6 }, type: 0, raycast: true }), { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, ], })); } combat.push(entity({ id: guid('cmb', 311), path: '/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 14, components: [ transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 60, y: 30 }, pos: { x: 192, y: 0 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 18, bold: true, color: { r: 0.8, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }), ], })); for (let i = 1; i <= 5; i++) { combat.push(entity({ id: guid('cmb', 320 + i), path: `/ui/DefaultGroup/CombatHud/TopBar/PotionSlot${i}`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent', displayOrder: 14 + i, components: [ transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: 240 + (i - 1) * 44, y: 0 } }), sprite({ color: { r: 0.22, g: 0.25, b: 0.3, a: 0.9 }, type: 0, raycast: true }), { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, ], })); } const tooltipBox = entity({ id: guid('cmb', 330), path: '/ui/DefaultGroup/CombatHud/TooltipBox', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 20, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 80 }, pos: { x: 0, y: 400 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.04, g: 0.05, b: 0.08, a: 0.96 }, type: 1 }), ], }); tooltipBox.jsonString.enable = false; combat.push(tooltipBox); combat.push(entity({ id: guid('cmb', 331), path: '/ui/DefaultGroup/CombatHud/TooltipBox/Name', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 300, parentH: 80, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 18 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 19, bold: true, color: GOLD, alignment: 4 }), ], })); combat.push(entity({ id: guid('cmb', 332), path: '/ui/DefaultGroup/CombatHud/TooltipBox/Desc', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 300, parentH: 80, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 284, y: 30 }, pos: { x: 0, y: -14 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 4 }), ], })); const potionMenu = entity({ id: guid('cmb', 340), path: '/ui/DefaultGroup/CombatHud/PotionMenu', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 21, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 180 }, pos: { x: 0, y: 120 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.07, g: 0.08, b: 0.12, a: 0.97 }, type: 1 }), ], }); potionMenu.jsonString.enable = false; combat.push(potionMenu); combat.push(entity({ id: guid('cmb', 341), path: '/ui/DefaultGroup/CombatHud/PotionMenu/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 36 }, pos: { x: 0, y: 52 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 19, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); const pmButtons = [ ['Use', '사용', -120, { r: 0.32, g: 0.55, b: 0.36, a: 1 }], ['Toss', '버리기', 0, { r: 0.6, g: 0.32, b: 0.3, a: 1 }], ['Close', '닫기', 120, { r: 0.25, g: 0.28, b: 0.35, a: 1 }], ]; pmButtons.forEach(([suffix, label, x, color], bi) => { combat.push(entity({ id: guid('cmb', 342 + bi), path: `/ui/DefaultGroup/CombatHud/PotionMenu/${suffix}`, modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 1 + bi, components: [ transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 104, y: 46 }, pos: { x, y: -40 } }), sprite({ color, type: 1, raycast: true }), button(), text({ value: label, fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); }); combat.push(entity({ id: guid('cmb', 205), path: '/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 140, y: 40 }, pos: { x: 528, y: 0 } }), sprite({ color: DARK, type: 1, raycast: true }), button(), text({ value: '모든덱보기', fontSize: 18, bold: true, color: GOLD, alignment: 0 }), ], })); const skillFx = entity({ id: guid('cmb', 230), path: '/ui/DefaultGroup/CombatHud/SkillFx', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 30, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 110, y: 110 }, pos: { x: 0, y: 0 } }), sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), ], }); skillFx.jsonString.enable = false; combat.push(skillFx); const result = entity({ id: guid('cmb', 2), path: '/ui/DefaultGroup/CombatHud/Result', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 8, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }), ], }); result.jsonString.enable = false; combat.push(result); emit('CombatHud', combat); const reward = []; const rewardHud = entity({ id: guid('rwd', 0), path: '/ui/DefaultGroup/RewardHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 6, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.86 }, type: 1, raycast: true }), ], }); rewardHud.jsonString.enable = false; reward.push(rewardHud); reward.push(entity({ id: guid('rwd', 1), path: '/ui/DefaultGroup/RewardHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 64 }, pos: { x: 0, y: 300 } }), sprite({ color: TRANSPARENT }), text({ value: '보상 카드 선택', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), ], })); let rwdN = 2; const rewardXs = [-300, 0, 300]; for (let i = 1; i <= 3; i++) { const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`; reward.push(entity({ id: guid('rwd', rwdN++), path: cardPath, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: i, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: rewardXs[i - 1], y: 0 } }), sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }), button(), ], })); const rewardLayout = cardFaceLayout(CARD_W); for (const [suffix, cfg] of rewardLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }])) { const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; reward.push(entity({ id: guid('rwd', rwdN++), path: `${cardPath}/${suffix}`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: dOrder, components: [ transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), sprite({ color: TRANSPARENT }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), ], })); } reward.push(entity({ id: guid('rwd', rwdN++), path: `${cardPath}/Art`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 5, components: [ transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: rewardLayout.art.size, pos: rewardLayout.art.pos }), sprite({ color: WHITE, type: 0, raycast: false }), ], })); } reward.push(entity({ id: guid('rwd', rwdN++), path: '/ui/DefaultGroup/RewardHud/Skip', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 10, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -260 } }), sprite({ color: DARK, type: 1, raycast: true }), button(), text({ value: '건너뛰기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), ], })); emit('RewardHud', reward); const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스', shop: '상점', rest: '휴식' }; const map = []; const mapHud = entity({ id: guid('map', 0), path: '/ui/DefaultGroup/MapHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 7, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.9 }, type: 1, raycast: true }), ], }); mapHud.jsonString.enable = false; map.push(mapHud); map.push(entity({ id: guid('map', 1), path: '/ui/DefaultGroup/MapHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 510 } }), sprite({ color: TRANSPARENT }), text({ value: '다음 노드 선택', fontSize: 40, bold: true, color: GOLD, alignment: 4 }), ], })); // 절차 생성 맵용 정적 그리드 — 노드 7행×4열 + 보스, 점선 도트. RenderMap이 런타임 토글. const nodeX = (c) => -270 + (c - 1) * 180; const nodeY = (r) => -330 + (r - 1) * 105; const BOSS_POS = { x: 0, y: 405 }; let mapN = 2; const pushMapNode = (id, pos, size, label) => { const nodePath = `/ui/DefaultGroup/MapHud/Node_${id}`; const nodeEnt = entity({ id: guid('map', mapN++), path: nodePath, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: 5, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }), sprite({ color: { r: 0.2, g: 0.22, b: 0.26, a: 1 }, type: 1, raycast: true }), button(), ], }); nodeEnt.jsonString.enable = false; map.push(nodeEnt); map.push(entity({ id: guid('map', mapN++), path: `${nodePath}/Label`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: size.x, parentH: size.y, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: size.x + 20, y: 30 }, pos: { x: 0, y: 0 } }), sprite({ color: TRANSPARENT }), text({ value: label, fontSize: id === 'boss' ? 18 : 15, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); }; for (let r = 1; r <= MAP_ROWS; r++) { for (let c = 1; c <= MAP_COLS; c++) { pushMapNode(`r${r}c${c}`, { x: nodeX(c), y: nodeY(r) }, { x: 56, y: 56 }, ''); } } pushMapNode('boss', BOSS_POS, { x: 72, y: 72 }, '보스'); const pushDots = (dotId, from, to) => { for (let k = 1; k <= 3; k++) { const t = k / 4; const dot = entity({ id: guid('map', mapN++), path: `/ui/DefaultGroup/MapHud/Dot_${dotId}_${k}`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 8, y: 8 }, pos: { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t } }), sprite({ color: { r: 0.5, g: 0.5, b: 0.55, a: 0.8 }, type: 1 }), ], }); dot.jsonString.enable = false; map.push(dot); } }; for (let r = 1; r < MAP_ROWS; r++) { for (let c = 1; c <= MAP_COLS; c++) { for (let c2 = c - 1; c2 <= c + 1; c2++) { if (c2 < 1 || c2 > MAP_COLS) continue; pushDots(`r${r}c${c}_${c2}`, { x: nodeX(c), y: nodeY(r) }, { x: nodeX(c2), y: nodeY(r + 1) }); } } } for (let c = 1; c <= MAP_COLS; c++) { pushDots(`r${MAP_ROWS}c${c}_b`, { x: nodeX(c), y: nodeY(MAP_ROWS) }, BOSS_POS); } emit('MapHud', map); const shop = []; const shopHud = entity({ id: guid('shp', 0), path: '/ui/DefaultGroup/ShopHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 8, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), ], }); shopHud.jsonString.enable = false; shop.push(shopHud); shop.push(entity({ id: guid('shp', 1), path: '/ui/DefaultGroup/ShopHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }), sprite({ color: TRANSPARENT }), text({ value: '상점', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), ], })); shop.push(entity({ id: guid('shp', 2), path: '/ui/DefaultGroup/ShopHud/Gold', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 44 }, pos: { x: 0, y: 330 } }), sprite({ color: TRANSPARENT }), text({ value: '메소 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), ], })); let shpN = 3; const shopXs = [-300, 0, 300]; for (let i = 1; i <= 3; i++) { const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`; shop.push(entity({ id: guid('shp', shpN++), path: cardPath, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: i, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: shopXs[i - 1], y: 20 } }), sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }), button(), ], })); const shopLayout = cardFaceLayout(CARD_W); const shopTexts = shopLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]); shopTexts.push(['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -135 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }]); for (const [suffix, cfg] of shopTexts) { const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9; shop.push(entity({ id: guid('shp', shpN++), path: `${cardPath}/${suffix}`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: dOrder, components: [ transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), sprite({ color: TRANSPARENT }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), ], })); } shop.push(entity({ id: guid('shp', shpN++), path: `${cardPath}/Art`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 5, components: [ transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: shopLayout.art.size, pos: shopLayout.art.pos }), sprite({ color: WHITE, type: 0, raycast: false }), ], })); } shop.push(entity({ id: guid('shp', shpN++), path: '/ui/DefaultGroup/ShopHud/Relic', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: 9, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -190 } }), sprite({ color: { r: 0.7, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }), button(), ], })); shop.push(entity({ id: guid('shp', shpN++), path: '/ui/DefaultGroup/ShopHud/Relic/Label', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }), sprite({ color: TRANSPARENT }), text({ value: '유물', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); shop.push(entity({ id: guid('shp', shpN++), path: '/ui/DefaultGroup/ShopHud/Relic/Price', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }), sprite({ color: TRANSPARENT }), text({ value: '60 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), ], })); shop.push(entity({ id: guid('shp', shpN++), path: '/ui/DefaultGroup/ShopHud/Potion', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: 11, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -278 } }), sprite({ color: { r: 0.45, g: 0.7, b: 0.55, a: 1 }, type: 1, raycast: true }), button(), ], })); shop.push(entity({ id: guid('shp', shpN++), path: '/ui/DefaultGroup/ShopHud/Potion/Label', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }), sprite({ color: TRANSPARENT }), text({ value: '물약', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); shop.push(entity({ id: guid('shp', shpN++), path: '/ui/DefaultGroup/ShopHud/Potion/Price', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }), sprite({ color: TRANSPARENT }), text({ value: '20 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), ], })); shop.push(entity({ id: guid('shp', shpN++), path: '/ui/DefaultGroup/ShopHud/Leave', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 10, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 } }), sprite({ color: DARK, type: 1, raycast: true }), button(), text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), ], })); emit('ShopHud', shop); const rest = []; const restHud = entity({ id: guid('rst', 0), path: '/ui/DefaultGroup/RestHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 9, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.05, g: 0.08, b: 0.06, a: 0.92 }, type: 1, raycast: true }), ], }); restHud.jsonString.enable = false; rest.push(restHud); rest.push(entity({ id: guid('rst', 1), path: '/ui/DefaultGroup/RestHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 140 } }), sprite({ color: TRANSPARENT }), text({ value: '휴식', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), ], })); rest.push(entity({ id: guid('rst', 2), path: '/ui/DefaultGroup/RestHud/Info', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }), sprite({ color: TRANSPARENT }), text({ value: 'HP 회복', fontSize: 30, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); rest.push(entity({ id: guid('rst', 3), path: '/ui/DefaultGroup/RestHud/Leave', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -120 } }), sprite({ color: DARK, type: 1, raycast: true }), button(), text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), ], })); emit('RestHud', rest); // 유물 방 — 보물 상자 (P8) const treasure = []; const treasureHud = entity({ id: guid('trs', 0), path: '/ui/DefaultGroup/TreasureHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 8, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), ], }); treasureHud.jsonString.enable = false; treasure.push(treasureHud); treasure.push(entity({ id: guid('trs', 1), path: '/ui/DefaultGroup/TreasureHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 320 } }), sprite({ color: TRANSPARENT }), text({ value: '보물 상자', fontSize: 40, bold: true, color: GOLD, alignment: 4 }), ], })); treasure.push(entity({ id: guid('trs', 2), path: '/ui/DefaultGroup/TreasureHud/Chest', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 180 }, pos: { x: 0, y: 40 } }), sprite({ dataId: CHEST_CLOSED_RUID, color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }), button(), ], })); treasure.push(entity({ id: guid('trs', 3), path: '/ui/DefaultGroup/TreasureHud/Hint', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 500, y: 34 }, pos: { x: 0, y: -90 } }), sprite({ color: TRANSPARENT }), text({ value: '상자를 클릭해 여세요', fontSize: 20, bold: false, color: { r: 0.85, g: 0.85, b: 0.9, a: 1 }, alignment: 4 }), ], })); const treasureReward = entity({ id: guid('trs', 4), path: '/ui/DefaultGroup/TreasureHud/Reward', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 44 }, pos: { x: 0, y: -160 } }), sprite({ color: TRANSPARENT }), text({ value: '', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), ], }); treasureReward.jsonString.enable = false; treasure.push(treasureReward); treasure.push(entity({ id: guid('trs', 5), path: '/ui/DefaultGroup/TreasureHud/Leave', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 4, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -280 } }), sprite({ color: DARK, type: 1, raycast: true }), button(), text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), ], })); emit('TreasureHud', treasure); // 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직 const jobChoice = []; const jobChoiceHud = entity({ id: guid('job', 0), path: '/ui/DefaultGroup/JobChoiceHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 9, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), ], }); jobChoiceHud.jsonString.enable = false; jobChoice.push(jobChoiceHud); jobChoice.push(entity({ id: guid('job', 1), path: '/ui/DefaultGroup/JobChoiceHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 220 } }), sprite({ color: TRANSPARENT }), text({ value: '보스 처치 보상을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), ], })); const jcButtons = [ ['RelicButton', '유물 획득', -240, { r: 0.7, g: 0.55, b: 0.85, a: 1 }], ['JobButton', '2차 전직', 240, { r: 0.86, g: 0.6, b: 0.3, a: 1 }], ]; jcButtons.forEach(([suffix, label, x, color], bi) => { jobChoice.push(entity({ id: guid('job', 2 + bi), path: `/ui/DefaultGroup/JobChoiceHud/${suffix}`, modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 1 + bi, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 140 }, pos: { x, y: 0 } }), sprite({ color, type: 1, raycast: true }), button(), text({ value: label, fontSize: 32, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); }); emit('JobChoiceHud', jobChoice); const jobSelect = []; const jobSelectHud = entity({ id: guid('job', 10), path: '/ui/DefaultGroup/JobSelectHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 10, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.94 }, type: 1, raycast: true }), ], }); jobSelectHud.jsonString.enable = false; jobSelect.push(jobSelectHud); jobSelect.push(entity({ id: guid('job', 11), path: '/ui/DefaultGroup/JobSelectHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 300 } }), sprite({ color: TRANSPARENT }), text({ value: '2차 전직 — 직업을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), ], })); // 범용 슬롯 3개 — ShowJobSelect(Lua)가 클래스별 JOBS로 텍스트를 채움 (P10 동적화) const jobs = [ ['slot1', '', '', '', -440, { r: 0.82, g: 0.4, b: 0.34, a: 1 }], ['slot2', '', '', '', 0, { r: 0.4, g: 0.55, b: 0.85, a: 1 }], ['slot3', '', '', '', 440, { r: 0.42, g: 0.72, b: 0.46, a: 1 }], ]; jobs.forEach(([jobId, name, desc, starter, x, color], ji) => { const base = `/ui/DefaultGroup/JobSelectHud/Job_${jobId}`; jobSelect.push(entity({ id: guid('job', 12 + ji * 4), path: base, modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: 1 + ji, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 420 }, pos: { x, y: -20 } }), sprite({ color, type: 1, raycast: true }), button(), ], })); jobSelect.push(entity({ id: guid('job', 13 + ji * 4), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 50 }, pos: { x: 0, y: 150 } }), sprite({ color: TRANSPARENT }), text({ value: name, fontSize: 34, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), ], })); jobSelect.push(entity({ id: guid('job', 14 + ji * 4), path: `${base}/Desc`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 160 }, pos: { x: 0, y: 0 } }), sprite({ color: TRANSPARENT }), text({ value: desc, fontSize: 22, bold: false, color: { r: 0.95, g: 0.95, b: 0.97, a: 1 }, alignment: 4 }), ], })); jobSelect.push(entity({ id: guid('job', 15 + ji * 4), path: `${base}/Starter`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 32 }, pos: { x: 0, y: -160 } }), sprite({ color: TRANSPARENT }), text({ value: starter, fontSize: 18, bold: true, color: GOLD, alignment: 4 }), ], })); }); emit('JobSelectHud', jobSelect); const menu = []; menu.push(entity({ id: guid('menu', 0), path: '/ui/DefaultGroup/MainMenu', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 20, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.06, g: 0.09, b: 0.13, a: 1 }, type: 1, raycast: true }), ], })); menu.push(entity({ id: guid('menu', 50), path: '/ui/DefaultGroup/MainMenu/OpaqueBackdrop', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT, type: 1, raycast: false }), ], })); menu.push(entity({ id: guid('menu', 1), path: '/ui/DefaultGroup/MainMenu/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 100 }, pos: { x: 0, y: 180 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '슬레이 메이플', fontSize: 64, bold: true, color: GOLD, alignment: 0 }), ], })); menu.push(entity({ id: guid('menu', 2), path: '/ui/DefaultGroup/MainMenu/Subtitle', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 48 }, pos: { x: 0, y: 104 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '카드를 뽑고, 덱을 만들고, 첨탑을 오른다', fontSize: 24, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 0 }), ], })); menu.push(entity({ id: guid('menu', 3), path: '/ui/DefaultGroup/MainMenu/NewGameButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 68 }, pos: { x: 0, y: -20 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), button(), text({ value: '새 게임', fontSize: 30, bold: true, color: GOLD, alignment: 0 }), ], })); // 승천 선택 (P11): [-] 라벨 [+] menu.push(entity({ id: guid('menu', 195), path: '/ui/DefaultGroup/MainMenu/AscMinus', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 5, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: -170, y: -185 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), button(), text({ value: '-', fontSize: 30, bold: true, color: GOLD, alignment: 4 }), ], })); menu.push(entity({ id: guid('menu', 196), path: '/ui/DefaultGroup/MainMenu/AscLabel', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 6, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 250, y: 40 }, pos: { x: 0, y: -185 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '승천 0 / 해금 0', fontSize: 22, bold: true, color: { r: 0.85, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }), ], })); menu.push(entity({ id: guid('menu', 197), path: '/ui/DefaultGroup/MainMenu/AscPlus', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 7, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: 170, y: -185 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), button(), text({ value: '+', fontSize: 30, bold: true, color: GOLD, alignment: 4 }), ], })); menu.push(entity({ id: guid('menu', 4), path: '/ui/DefaultGroup/MainMenu/ContinueButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 58 }, pos: { x: 0, y: -100 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.1, g: 0.11, b: 0.13, a: 0.78 }, type: 1, raycast: false }), button({ enabled: false }), text({ value: '이어하기', fontSize: 24, bold: true, color: { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 0 }), ], })); const select = []; select.push(entity({ id: guid('menu', 100), path: '/ui/DefaultGroup/CharacterSelectHud', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 21, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }), ], })); select.push(entity({ id: guid('menu', 190), path: '/ui/DefaultGroup/CharacterSelectHud/OpaqueBackdrop', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 0, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT, type: 1, raycast: false }), ], })); select.push(entity({ id: guid('menu', 101), path: '/ui/DefaultGroup/CharacterSelectHud/Title', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 72 }, pos: { x: 0, y: 355 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '\uCE90\uB9AD\uD130 \uC120\uD0DD', fontSize: 42, bold: true, color: GOLD, alignment: 0 }), ], })); select.push(entity({ id: guid('menu', 102), path: '/ui/DefaultGroup/CharacterSelectHud/Status', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 680, y: 44 }, pos: { x: 0, y: 298 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '\uC804\uC0AC\uB97C \uC120\uD0DD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694', fontSize: 22, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }), ], })); const classCards = [ { key: 'Warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } }, { key: 'Thief', label: '\uB3C4\uC801', desc: '\uCD94\uD6C4 \uC5F4\uB9BC', x: 0, enabled: false, tint: { r: 0.18, g: 0.19, b: 0.21, a: 1 } }, { key: 'Mage', label: '\uB9C8\uBC95\uC0AC', desc: '\uB9C8\uBC95 \uC6D0\uAC70\uB9AC \uB51C\uB7EC', x: 360, enabled: true, tint: { r: 0.3, g: 0.4, b: 0.75, a: 1 } }, ]; for (let i = 0; i < classCards.length; i++) { const cls = classCards[i]; const base = `/ui/DefaultGroup/CharacterSelectHud/${cls.key}Button`; select.push(entity({ id: guid('menu', 110 + i), path: base, modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', displayOrder: 10 + i, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 270, y: 330 }, pos: { x: cls.x, y: 40 }, align: ALIGN_CENTER }), sprite({ color: cls.enabled ? { r: 0.16, g: 0.2, b: 0.26, a: 1 } : { r: 0.11, g: 0.12, b: 0.14, a: 1 }, type: 1, raycast: cls.enabled }), button({ enabled: cls.enabled }), ], })); select.push(entity({ id: guid('menu', 120 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 54 }, pos: { x: 0, y: 108 } }), sprite({ color: TRANSPARENT }), text({ value: cls.label, fontSize: 34, bold: true, color: cls.enabled ? GOLD : { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 4 }), ], })); select.push(entity({ id: guid('menu', 130 + i), path: `${base}/Portrait`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 1, components: [ transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 142, y: 142 }, pos: { x: 0, y: 8 } }), sprite({ color: cls.tint, type: 1 }), ], })); select.push(entity({ id: guid('menu', 140 + i), path: `${base}/Desc`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 50 }, pos: { x: 0, y: -105 } }), sprite({ color: TRANSPARENT }), text({ value: cls.desc, fontSize: 20, color: cls.enabled ? { r: 0.86, g: 0.9, b: 0.94, a: 1 } : { r: 0.52, g: 0.55, b: 0.59, a: 1 }, alignment: 4 }), ], })); if (!cls.enabled) { select.push(entity({ id: guid('menu', 150 + i), path: `${base}/LockBody`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 3, components: [ transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 76, y: 58 }, pos: { x: 0, y: 4 } }), sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }), ], })); select.push(entity({ id: guid('menu', 160 + i), path: `${base}/LockShackle`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 4, components: [ transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 54, y: 42 }, pos: { x: 0, y: 48 } }), sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }), ], })); } } select.push(entity({ id: guid('menu', 180), path: '/ui/DefaultGroup/CharacterSelectHud/StartButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 20, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 68 }, pos: { x: 720, y: -360 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }), button(), text({ value: '\uC2DC\uC791', fontSize: 30, bold: true, color: GOLD, alignment: 0 }), ], })); select[0].jsonString.enable = false; emit('MainMenu', menu); emit('CharacterSelectHud', select); for (const section of UI_APPEND_ORDER) { const entities = uiSections.get(section); if (entities == null) { throw new Error(`[gen-slaydeck] missing generated UI section: ${section}`); } appendUiSection(ui, section, entities); } JSON.parse(JSON.stringify(ui)); writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8'); } function prop(Type, Name, DefaultValue = 'nil') { return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name }; } function method(Name, Code, Arguments = [], ExecSpace = 0, ReturnType = 'void') { return { Return: { Type: ReturnType, DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null }, Arguments, Code, Scope: 2, ExecSpace, Attributes: [], Name, }; } function codeblock(id, name, properties, methods) { return { Id: '', GameId: '', EntryKey: `codeblock://${id}`, ContentType: 'x-mod/codeblock', Content: '', Usage: 0, UsePublish: 1, UseService: 0, CoreVersion: '26.5.0.0', StudioVersion: '', DynamicLoading: 0, ContentProto: { Use: 'Json', Json: { CoreVersion: { Major: 0, Minor: 2 }, ScriptVersion: { Major: 1, Minor: 0 }, Description: '', Id: id, Language: 1, Name: name, Type: 1, Source: 0, Target: null, Properties: properties, Methods: methods, EntityEventHandlers: [], }, }, }; } function writeCodeblocks() { const RUN_LENGTH = 3; const GOLD_PER_WIN = 25; const CARD_PRICE = 30; const REST_HEAL = 30; const RELIC_PRICE = 60; const ACT_COUNT = 3; const ACT_MAPS = ['map01', 'map02', 'map03']; const combat = codeblock('SlayDeckController', 'SlayDeckController', [ prop('any', 'DrawPile'), prop('any', 'DiscardPile'), prop('any', 'Hand'), prop('number', 'Energy', '0'), prop('number', 'MaxEnergy', '3'), prop('number', 'Turn', '0'), prop('number', 'TweenEventId', '0'), prop('any', 'EndTurnHandler'), prop('any', 'NewGameHandler'), prop('any', 'WarriorSelectHandler'), prop('any', 'MageSelectHandler'), prop('any', 'AscMinusHandler'), prop('any', 'AscPlusHandler'), prop('any', 'JobOpts'), prop('any', 'Jobs'), prop('number', 'AscensionLevel', '0'), prop('number', 'AscensionUnlocked', '0'), prop('any', 'StartGameHandler'), prop('string', 'SelectedClass', '""'), prop('any', 'DrawPileHandler'), prop('any', 'DiscardPileHandler'), prop('any', 'DeckInspectCloseHandler'), prop('any', 'AllDeckHandler'), prop('any', 'AllDeckCloseHandler'), prop('string', 'DeckInspectKind', '""'), prop('boolean', 'DeckAllOpen', 'false'), prop('any', 'Cards'), prop('any', 'CardFrames'), prop('any', 'ClassToFrame'), prop('number', 'PlayerHp', '0'), prop('number', 'PlayerMaxHp', '80'), prop('number', 'PlayerBlock', '0'), prop('boolean', 'CombatOver', 'false'), prop('any', 'Monsters'), prop('any', 'Registered'), prop('number', 'TargetIndex', '1'), prop('any', 'RunDeck'), prop('number', 'Gold', '0'), prop('number', 'Floor', '0'), prop('number', 'RunLength', String(RUN_LENGTH)), prop('any', 'RewardChoices'), prop('boolean', 'RunActive', 'false'), prop('any', 'Enemies'), prop('any', 'MapNodes'), prop('any', 'MapStart'), prop('string', 'CurrentNodeId', '""'), prop('string', 'CurrentEnemyId', '""'), prop('any', 'ShopChoices'), prop('any', 'ShopBought'), prop('any', 'Relics'), prop('any', 'RunRelics'), prop('any', 'RelicPool'), prop('string', 'ShopRelic', '""'), prop('boolean', 'ShopRelicBought', 'false'), prop('number', 'DragSlot', '0'), prop('boolean', 'FxBusy', 'false'), prop('boolean', 'TurnBusy', 'false'), prop('number', 'PlayerStr', '0'), prop('number', 'PlayerWeak', '0'), prop('number', 'PlayerVuln', '0'), prop('any', 'PlayerPowers'), prop('any', 'Potions'), prop('any', 'RunPotions'), prop('number', 'PotionSlots', String(POTIONS.baseSlots)), prop('string', 'ShopPotion', '""'), prop('boolean', 'ShopPotionBought', 'false'), prop('number', 'FightAttackCount', '0'), prop('boolean', 'FirstHpLossDone', 'false'), prop('number', 'ClayBlockNext', '0'), prop('number', 'PotionMenuSlot', '0'), prop('number', 'Depth', '0'), prop('any', 'VisitedNodes'), prop('boolean', 'ChestOpened', 'false'), prop('string', 'PlayerJob', '""'), ], [ method('OnBeginPlay', `self:ShowMainMenu() local lp = _UserService.LocalPlayer if lp ~= nil then self:ReqLoadAscension(lp.PlayerComponent.UserId) end`), method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId) local errCode, value = ds:GetAndWait("ascensionUnlocked") local n = 0 if errCode == 0 and value ~= nil and value ~= "" then n = tonumber(value) or 0 end self:RecvAscension(n, userId)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }], 5), method('RecvAscension', `self.AscensionUnlocked = n if self.AscensionLevel > self.AscensionUnlocked then self.AscensionLevel = self.AscensionUnlocked end self:RenderAscension()`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }, ], 6), method('SaveAscension', `local ds = _DataStorageService:GetUserDataStorage(userId) ds:SetAndWait("ascensionUnlocked", tostring(n))`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }, ], 5), method('AdjustAscension', `local v = self.AscensionLevel + delta if v < 0 then v = 0 end if v > self.AscensionUnlocked then v = self.AscensionUnlocked end self.AscensionLevel = v self:RenderAscension()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delta' }]), method('RenderAscension', `self:SetText("/ui/DefaultGroup/MainMenu/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`), method('AscHpMult', `local m = 1 if self.AscensionLevel >= 1 then m = m + 0.1 end if self.AscensionLevel >= 6 then m = m + 0.1 end return m`, [], 0, 'number'), method('AscAtkMult', `local m = 1 if self.AscensionLevel >= 2 then m = m + 0.1 end if self.AscensionLevel >= 7 then m = m + 0.1 end return m`, [], 0, 'number'), method('AscEliteBonus', `local b = 0 if self.AscensionLevel >= 4 then b = b + 0.2 end if self.AscensionLevel >= 9 then b = b + 0.2 end return b`, [], 0, 'number'), method('AscGoldMult', `local m = 1 if self.AscensionLevel >= 5 then m = m - 0.25 end if self.AscensionLevel >= 10 then m = m - 0.25 end return m`, [], 0, 'number'), method('AscStartHpPenalty', `local p = 0 if self.AscensionLevel >= 3 then p = p + 10 end if self.AscensionLevel >= 8 then p = p + 10 end return p`, [], 0, 'number'), method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false) self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false) self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", false) self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false) self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false) self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false) self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false) self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false) self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)`), method('ShowState', `self:HideGameHud() self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu") self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect") if state == "map" then self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true) elseif state == "combat" then self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true) self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true) elseif state == "shop" then self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true) elseif state == "rest" then self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true) elseif state == "treasure" then self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true) end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]), method('ShowMainMenu', `self.SelectedClass = "" self:RenderAscension() self:ShowState("menu") self:SetText("/ui/DefaultGroup/MainMenu/Title", "메이플 덱 어드벤처") self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱을 만들어 모험을 시작하세요") self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임") self:BindMenuButtons()`), method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton") if buttonEntity ~= nil and buttonEntity.ButtonComponent ~= nil then if self.NewGameHandler ~= nil then buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler) self.NewGameHandler = nil end self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowCharacterSelect() end) end local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton") if warrior ~= nil and warrior.ButtonComponent ~= nil then if self.WarriorSelectHandler ~= nil then warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler) self.WarriorSelectHandler = nil end self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end) end local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton") if mage ~= nil and mage.ButtonComponent ~= nil then if self.MageSelectHandler ~= nil then mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler) self.MageSelectHandler = nil end self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end) end local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton") if start ~= nil and start.ButtonComponent ~= nil then if self.StartGameHandler ~= nil then start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler) self.StartGameHandler = nil end self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end) end local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus") if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then if self.AscMinusHandler ~= nil then ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler) self.AscMinusHandler = nil end self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end) end local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus") if ascPlus ~= nil and ascPlus.ButtonComponent ~= nil then if self.AscPlusHandler ~= nil then ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler) self.AscPlusHandler = nil end self.AscPlusHandler = ascPlus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(1) end) end`), method('ShowCharacterSelect', `self.SelectedClass = "" self:ShowState("charselect") self:RenderCharacterSelect()`), method('SelectClass', `self.SelectedClass = className self:RenderCharacterSelect()`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }, ]), method('RenderCharacterSelect', `local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton") if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then if self.SelectedClass == "warrior" then warrior.SpriteGUIRendererComponent.Color = Color(0.28, 0.36, 0.46, 1) else warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) end end local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton") if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then if self.SelectedClass == "magician" then mage.SpriteGUIRendererComponent.Color = Color(0.28, 0.36, 0.46, 1) else mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) end end if self.SelectedClass == "warrior" then self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨") elseif self.SelectedClass == "magician" then self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "마법사 선택됨") else self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요") end`), method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "magician" then self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요") return end self:StartRun()`), method('SetEntityEnabled', `local e = _EntityService:GetEntityByPath(path) if e ~= nil then e.Enable = enabled end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' }, ]), method('StartRun', `if self.SelectedClass == "magician" then self.PlayerMaxHp = ${CLASSES.magician.maxHp} self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} } else self.PlayerMaxHp = ${CLASSES.warrior.maxHp} self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} } end self.PlayerMaxHp = self.PlayerMaxHp - self:AscStartHpPenalty() self.PlayerHp = self.PlayerMaxHp self.Gold = 0 self.Floor = 1 self.RunLength = ${ACT_COUNT} self.RunActive = true self.RunRelics = {} self.RunPotions = {} self.PotionSlots = ${POTIONS.baseSlots} ${luaPotionsTable(POTIONS.potions)} ${luaRelicsTable(RELICS.relics)} self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} } ${luaEnemiesTable(ENEMIES.enemies)} self.CurrentNodeId = "" self.CurrentEnemyId = "" self.PlayerJob = "" ${luaJobsTable(JOBS)} ${luaFramesTable()} self:GenerateMap() self:BindButtons() self:AddRelic("${RELICS.startingRelic}") self:RenderPotions() self:ShowMap()`), method('StartCombat', `self:ShowState("combat") self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false) self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) self.MaxEnergy = 3 self.Turn = 0 self.PlayerBlock = 0 self.PlayerStr = 0 self.PlayerWeak = 0 self.PlayerVuln = 0 self.PlayerPowers = {} self.FightAttackCount = 0 self.FirstHpLossDone = false self.ClayBlockNext = 0 self.CombatOver = false self.DiscardPile = {} self.Hand = {} ${luaCardsTable(CARDS.cards)} self.DrawPile = {} for i = 1, #self.RunDeck do self.DrawPile[i] = self.RunDeck[i] end self:Shuffle(self.DrawPile) self:BuildMonsters() self:RenderCombat() self:StartPlayerTurn() self:ApplyRelics("combatStart") self:RenderCombat()`), method('RegisterMonster', `if self.Registered == nil then self.Registered = {} end local g = group if g == nil or g == "" then g = "combat" end local mp = mapName if mp == nil then mp = "" end table.insert(self.Registered, { entity = monster, enemyId = enemyId, group = g, map = mp })`, [ { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'group' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'mapName' }, ]), method('BuildMonsters', `self.Monsters = {} local g = "combat" local node = self.MapNodes[self.CurrentNodeId] if node ~= nil and node.type ~= nil then g = node.type end local pmap = "" local lp = _UserService.LocalPlayer if lp ~= nil and lp.CurrentMapName ~= nil then pmap = lp.CurrentMapName end local reg = self.Registered or {} for i = 1, #reg do if reg[i].entity ~= nil and isvalid(reg[i].entity) then reg[i].entity:SetVisible(false) end end local list = {} for i = 1, #reg do local r = reg[i] if r.entity ~= nil and isvalid(r.entity) and r.group == g and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then local x = 0 if r.entity.TransformComponent ~= nil then x = r.entity.TransformComponent.WorldPosition.x end table.insert(list, { entity = r.entity, enemyId = r.enemyId, x = x }) end end table.sort(list, function(a, b) return a.x < b.x end) local mult = 1 + (self.Floor - 1) * 0.6 if g == "elite" or g == "boss" then mult = mult + self:AscEliteBonus() end local n = #list if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end for i = 1, n do local item = list[i] local e = self.Enemies[item.enemyId] if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end local intents = {} for k = 1, #e.intents do local v = e.intents[k].value if e.intents[k].kind == "Attack" then v = math.floor(v * mult * self:AscAtkMult()) elseif e.intents[k].kind ~= "Debuff" then v = math.floor(v * mult) end intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect } end local maxHp = math.floor(e.maxHp * mult * self:AscHpMult()) local hitClip = nil local standClip = nil if item.entity.StateAnimationComponent ~= nil then pcall(function() hitClip = item.entity.StateAnimationComponent.ActionSheet["hit"] standClip = item.entity.StateAnimationComponent.ActionSheet["stand"] end) end self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name, hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0, hitClip = hitClip, standClip = standClip, motionBusy = false, intents = intents, intentIdx = 1, alive = true, slot = i } self:ReviveMonsterEntity(item.entity) self:PositionMonsterSlot(i) end self.TargetIndex = 1`), method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then return end monster:SetEnable(true) monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]), method('Shuffle', `if list == nil then \treturn end for i = #list, 2, -1 do \tlocal j = math.random(1, i) \tlist[i], list[j] = list[j], list[i] end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]), method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton") if endTurn ~= nil and endTurn.ButtonComponent ~= nil then if self.EndTurnHandler ~= nil then endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler) self.EndTurnHandler = nil end self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end) end local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile") if drawPile ~= nil and drawPile.ButtonComponent ~= nil then if self.DrawPileHandler ~= nil then drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler) self.DrawPileHandler = nil end self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end) end local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile") if discardPile ~= nil and discardPile.ButtonComponent ~= nil then if self.DiscardPileHandler ~= nil then discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler) self.DiscardPileHandler = nil end self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end) end local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close") if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then if self.DeckInspectCloseHandler ~= nil then inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler) self.DeckInspectCloseHandler = nil end self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end) end local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton") if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then if self.AllDeckHandler ~= nil then allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler) self.AllDeckHandler = nil end self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end) end local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then if self.AllDeckCloseHandler ~= nil then allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) self.AllDeckCloseHandler = nil end self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) end for i = 1, 5 do local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end) cardEntity:ConnectEvent(UITouchDragEvent, function(ev) self:OnCardDrag(i, ev.TouchPoint) end) cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(i, ev.TouchPoint) end) end end for i = 1, 3 do local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i)) if rc ~= nil and rc.ButtonComponent ~= nil then rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end) end end local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip") if skip ~= nil and skip.ButtonComponent ~= nil then skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end) end local mapNodeIds = {} for r = 1, ${MAP_ROWS} do for c = 1, ${MAP_COLS} do table.insert(mapNodeIds, "r" .. tostring(r) .. "c" .. tostring(c)) end end table.insert(mapNodeIds, "boss") for i = 1, #mapNodeIds do local nid = mapNodeIds[i] local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid) if mn ~= nil and mn.ButtonComponent ~= nil then mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end) end end for i = 1, 3 do local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i)) if sc ~= nil and sc.ButtonComponent ~= nil then sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end) end end local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave") if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) end local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic") if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end) end local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave") if restLeave ~= nil and restLeave.ButtonComponent ~= nil then restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) end for i = 1, ${MAX_MONSTERS} do local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)) if ms ~= nil and ms.ButtonComponent ~= nil then ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end) end end for i = 1, 10 do local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)) if rs ~= nil and rs.UITouchReceiveComponent ~= nil then local idx = i rs:ConnectEvent(UITouchEnterEvent, function() local rid = nil if self.RunRelics ~= nil then rid = self.RunRelics[idx] end if rid ~= nil and self.Relics[rid] ~= nil then self:ShowTooltip(self.Relics[rid].name, self.Relics[rid].desc, -240 + (idx - 1) * 48) end end) rs:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end) end end for i = 1, 5 do local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)) if ps ~= nil and ps.UITouchReceiveComponent ~= nil then local idx = i ps:ConnectEvent(UITouchEnterEvent, function() local pid = nil if self.RunPotions ~= nil then pid = self.RunPotions[idx] end if pid ~= nil and self.Potions[pid] ~= nil then self:ShowTooltip(self.Potions[pid].name, self.Potions[pid].desc, 240 + (idx - 1) * 44) end end) ps:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end) ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end) end end local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use") if pmUse ~= nil and pmUse.ButtonComponent ~= nil then pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end) end local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss") if pmToss ~= nil and pmToss.ButtonComponent ~= nil then pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end) end local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close") if pmClose ~= nil and pmClose.ButtonComponent ~= nil then pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end) end local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion") if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end) end local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") if chest ~= nil and chest.ButtonComponent ~= nil then chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end) end local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave") if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) end local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton") if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end) end local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton") if jcJob ~= nil and jcJob.ButtonComponent ~= nil then jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end) end for i = 1, 3 do local slotIdx = i local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)) if jb ~= nil and jb.ButtonComponent ~= nil then jb:ConnectEvent(ButtonClickEvent, function() if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then self:SetJob(self.JobOpts[slotIdx].id) end end) end end`), method('StartPlayerTurn', `self.Turn = self.Turn + 1 self.Energy = self.MaxEnergy self:ApplyRelics("turnStart") self.PlayerBlock = 0 if self.ClayBlockNext > 0 then self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext self.ClayBlockNext = 0 end if self.PlayerPowers ~= nil then for i = 1, #self.PlayerPowers do local pc = self.Cards[self.PlayerPowers[i]] if pc ~= nil then if pc.powerEffect == "strengthPerTurn" then self.PlayerStr = self.PlayerStr + pc.value elseif pc.powerEffect == "energyPerTurn" then self.Energy = self.Energy + pc.value elseif pc.powerEffect == "blockPerTurn" then self.PlayerBlock = self.PlayerBlock + pc.value end end end end self:DrawCards(5) self:RenderHand(true) self:RenderCombat()`), method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then return end for i = 1, #self.Hand do \ttable.insert(self.DiscardPile, self.Hand[i]) end self.Hand = {} if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end self:RenderHand(false) self:RenderPiles() self:EnemyTurn()`), method('DrawCards', `for i = 1, amount do \tif #self.DrawPile <= 0 then \t\tself:RecycleDiscardIntoDraw() \tend \tif #self.DrawPile <= 0 then \t\tbreak \tend \tlocal cardId = table.remove(self.DrawPile) \ttable.insert(self.Hand, cardId) end self:RenderPiles()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]), method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then \treturn end self.DrawPile = {} for i = 1, #self.DiscardPile do \tself.DrawPile[i] = self.DiscardPile[i] end self.DiscardPile = {} self:Shuffle(self.DrawPile)`), method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", tostring(#self.DrawPile)) self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", tostring(#self.DiscardPile)) self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy)) local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then self:OpenDeckInspect(self.DeckInspectKind) end`), method('OpenDeckInspect', `self.DeckInspectKind = kind if self.DeckAllOpen == true then self.DeckAllOpen = false local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") if allHud ~= nil then allHud.Enable = false end end local pile = {} local title = "" if kind == "discard" then pile = self.DiscardPile or {} title = "버린 덱" else pile = self.DrawPile or {} title = "뽑을 덱" end self:RenderDeckInspect(pile, title) local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") if hud ~= nil then hud.Enable = true end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]), method('CloseDeckInspect', `self.DeckInspectKind = "" local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") if hud ~= nil then hud.Enable = false end`), method('RenderDeckInspect', `local count = 0 if pile ~= nil then count = #pile end local suffix = " (" .. tostring(count) .. ")" if count > 60 then suffix = suffix .. " - 60장까지 표시" end self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix) local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty") if empty ~= nil then empty.Enable = count <= 0 end for i = 1, 60 do local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i)) if e ~= nil then local cardId = nil if pile ~= nil then cardId = pile[i] end if cardId == nil then e.Enable = false else e.Enable = true self:ApplyInspectCardVisual(i, cardId) end end end`, [ { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' }, ]), method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, ]), method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") if inspectHud ~= nil then inspectHud.Enable = false end self.DeckInspectKind = "" self.DeckAllOpen = true self:RenderAllDeck() local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") if hud ~= nil then hud.Enable = true end`), method('CloseAllDeck', `self.DeckAllOpen = false local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") if hud ~= nil then hud.Enable = false end`), method('RenderAllDeck', `local pile = self.RunDeck or {} local count = #pile self:SetText("/ui/DefaultGroup/DeckAllHud/Title", "모든 덱 (" .. tostring(count) .. ")") local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Empty") if empty ~= nil then empty.Enable = count <= 0 end for i = 1, 120 do local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(i)) if e ~= nil then local cardId = pile[i] if cardId == nil then e.Enable = false else e.Enable = true self:ApplyAllDeckCardVisual(i, cardId) end end end`), method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, ]), method('RenderHand', `local drawStart = Vector2(-590, 8) for i = 1, 5 do \tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) \tif cardEntity ~= nil then \t\tlocal cardId = self.Hand[i] \t\tif cardId == nil then \t\t\tcardEntity.Enable = false \t\telse \t\t\tcardEntity.Enable = true \t\t\tself:ApplyCardVisual(i, cardId) \t\t\tif animate == true then \t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2((i - 3) * 200, 0), 0.16 + i * 0.045) \t\t\tend \t\tend \tend end self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]), method('ApplyCardFace', `local c = self.Cards[cardId] if c == nil then c = { name = cardId, cost = 0, desc = "", kind = "Skill", class = "warrior", rarity = "normal" } end local e = _EntityService:GetEntityByPath(base) if e ~= nil and e.SpriteGUIRendererComponent ~= nil then local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"] local ruid = nil if frames ~= nil then ruid = frames[c.rarity or "normal"] end if ruid ~= nil then e.SpriteGUIRendererComponent.ImageRUID = ruid e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) end end self:SetText(base .. "/Cost", string.format("%d", c.cost)) self:SetText(base .. "/Name", c.name) self:SetText(base .. "/Desc", c.desc) local art = _EntityService:GetEntityByPath(base .. "/Art") if art ~= nil then if c.image ~= nil and c.image ~= "" then art.Enable = true if art.SpriteGUIRendererComponent ~= nil then art.SpriteGUIRendererComponent.ImageRUID = c.image end else art.Enable = false end end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, ]), method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, ]), method('SetText', `local entity = _EntityService:GetEntityByPath(path) if entity ~= nil and entity.TextComponent ~= nil then \tentity.TextComponent.Text = value end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }, ]), method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) if cardEntity == nil or cardEntity.UITransformComponent == nil then \treturn end local tr = cardEntity.UITransformComponent tr.anchoredPosition = fromPos local elapsed = 0 local eventId = 0 eventId = _TimerService:SetTimerRepeat(function() \telapsed = elapsed + 1 / 60 \tlocal t = math.min(elapsed / duration, 1) \tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t) \ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased) \tif t >= 1 then \t\t_TimerService:ClearTimer(eventId) \tend end, 1 / 60)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' }, ]), method('CalcPlayerAttack', `local base2 = base self.FightAttackCount = self.FightAttackCount + 1 if self.FightAttackCount == 1 and self:HasRelic("akabeko") then base2 = base2 + 8 end local dmg = base2 + self.PlayerStr if self:HasRelic("penNib") and self.FightAttackCount % 10 == 0 then dmg = dmg * 2 end if self.PlayerWeak > 0 then dmg = math.floor(dmg * 0.75) end if dmg > 0 and dmg < 5 and self:HasRelic("boot") then dmg = 5 end if dmg < 0 then dmg = 0 end return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), method('PlayCard', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then return end if self.Hand == nil then return end local cardId = self.Hand[slot] if cardId == nil then return end local c = self.Cards[cardId] if c == nil then return end if self.Energy < c.cost then self:Toast("에너지가 부족합니다") return end self.Energy = self.Energy - c.cost if c.kind == "Attack" then if c.damage ~= nil then self:PlayerAttackMotion() local total = 0 local hitN = c.hits or 1 for h = 1, hitN do total = total + self:CalcPlayerAttack(c.damage) end if c.aoe == true then self:PlayAoeFx(c.image, total) else self:PlayAttackFx(self.TargetIndex, c.image, total, c.pierce == true) end end if c.block ~= nil then self.PlayerBlock = self.PlayerBlock + c.block end self:ApplyRelics("cardPlayed") elseif c.kind == "Skill" then if c.block ~= nil then self.PlayerBlock = self.PlayerBlock + c.block end elseif c.kind == "Power" then if c.powerEffect ~= nil then table.insert(self.PlayerPowers, cardId) end end if c.strength ~= nil then self.PlayerStr = self.PlayerStr + c.strength end if c.selfVuln ~= nil then self.PlayerVuln = self.PlayerVuln + c.selfVuln end if c.heal ~= nil then self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp) end if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then local tm = self.Monsters[self.TargetIndex] if tm ~= nil and tm.alive == true then if c.weak ~= nil then tm.weak = tm.weak + c.weak end if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end if c.vuln ~= nil then tm.vuln = tm.vuln + c.vuln if self:HasRelic("championBelt") then tm.weak = tm.weak + 1 end end end end table.remove(self.Hand, slot) if c.kind ~= "Power" then table.insert(self.DiscardPile, cardId) end if c.draw ~= nil then self:DrawCards(c.draw) end self:RenderHand(false) self:RenderPiles() self:RenderCombat() self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then return end if self.Hand == nil or self.Hand[slot] == nil then return end self.DragSlot = slot`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('OnCardDrag', `if self.DragSlot ~= slot then return end local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) if e ~= nil and e.UITransformComponent ~= nil then local ui = _UILogic:ScreenToUIPosition(touchPoint) e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360) end`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, ]), method('OnCardDragEnd', `if self.DragSlot ~= slot then return end self.DragSlot = 0 local cardXs = { ${CARD_XS.join(', ')} } local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) if e ~= nil and e.UITransformComponent ~= nil then e.UITransformComponent.anchoredPosition = Vector2(cardXs[slot], 0) end self:ResolveCardDrop(slot, touchPoint)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, ]), method('ResolveCardDrop', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then return end local cardId = self.Hand[slot] if cardId == nil then return end local c = self.Cards[cardId] if c == nil then return end if c.kind == "Attack" then local best = 0 local bestDist = 200 for i = 1, #self.Monsters do local m = self.Monsters[i] if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then local wp = m.entity.TransformComponent.WorldPosition local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7)) local dx = sp.x - touchPoint.x local dy = sp.y - touchPoint.y local d = math.sqrt(dx * dx + dy * dy) if d < bestDist then bestDist = d best = i end end end if best > 0 then self.TargetIndex = best self:PlayCard(slot) end else local ui = _UILogic:ScreenToUIPosition(touchPoint) if ui.y > -180 then self:PlayCard(slot) end end`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, ]), method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]), method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex] if m == nil or m.alive ~= true then m = nil for i = 1, #self.Monsters do if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end end end if m == nil then return end local dmg = amount if m.vuln > 0 then dmg = math.floor(dmg * 1.5) end if m.block > 0 and pierce ~= true then local absorbed = math.min(m.block, dmg) m.block = m.block - absorbed dmg = dmg - absorbed end m.hp = m.hp - dmg self:MonsterHitMotion(m.slot) if m.hp <= 0 then m.hp = 0 self:KillMonster(m.slot) end`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' }, ]), method('PlayAttackFx', `local m = self.Monsters[targetIndex] if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then self:DealDamageToTarget(damage, pierce) self:RenderCombat() self:CheckCombatEnd() return end self.FxBusy = true local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx") if fx ~= nil then if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then fx.SpriteGUIRendererComponent.ImageRUID = image end if fx.UITransformComponent ~= nil and m.entity.TransformComponent ~= nil then local wp = m.entity.TransformComponent.WorldPosition local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7)) fx.UITransformComponent.anchoredPosition = _UILogic:ScreenToUIPosition(sp) end fx.Enable = true end _TimerService:SetTimerOnce(function() if fx ~= nil then fx.Enable = false end self.FxBusy = false local shown = damage local mt = self.Monsters[targetIndex] if mt ~= nil and mt.alive == true and mt.vuln > 0 then shown = math.floor(damage * 1.5) end self:DealDamageToTarget(damage, pierce) self:ShowDmgPop(targetIndex, shown) self:RenderCombat() self:CheckCombatEnd() end, 0.35)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' }, ]), method('PlayAoeFx', `self.FxBusy = true local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx") if fx ~= nil then if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then fx.SpriteGUIRendererComponent.ImageRUID = image end if fx.UITransformComponent ~= nil then fx.UITransformComponent.anchoredPosition = Vector2(300, 60) end fx.Enable = true end _TimerService:SetTimerOnce(function() if fx ~= nil then fx.Enable = false end self.FxBusy = false for i = 1, #self.Monsters do local m = self.Monsters[i] if m ~= nil and m.alive == true then local dmg = damage if m.vuln > 0 then dmg = math.floor(dmg * 1.5) end if m.block > 0 then local absorbed = math.min(m.block, dmg) m.block = m.block - absorbed dmg = dmg - absorbed end m.hp = m.hp - dmg self:ShowDmgPop(i, dmg) self:MonsterHitMotion(i) if m.hp <= 0 then m.hp = 0 self:KillMonster(m.slot) end end end self:RenderCombat() self:CheckCombatEnd() end, 0.35)`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' }, ]), method('KillMonster', `local m = self.Monsters[slot] if m == nil then return end m.alive = false if m.entity ~= nil and isvalid(m.entity) then local ent = m.entity _TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4) end self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false) for i = 1, #self.Monsters do if self.Monsters[i].alive == true then self.TargetIndex = i; break end end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('DealDamageToPlayer', `local dmg = amount if self.PlayerBlock > 0 then local absorbed = math.min(self.PlayerBlock, dmg) self.PlayerBlock = self.PlayerBlock - absorbed dmg = dmg - absorbed end if dmg > 0 then self.PlayerHp = self.PlayerHp - dmg if self:HasRelic("bronzeScales") and attackerSlot ~= nil and attackerSlot > 0 then local am = self.Monsters[attackerSlot] if am ~= nil and am.alive == true then am.hp = am.hp - 3 self:MonsterHitMotion(am.slot) if am.hp <= 0 then am.hp = 0 self:KillMonster(am.slot) end end end if self:HasRelic("selfFormingClay") then self.ClayBlockNext = self.ClayBlockNext + 3 end if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then self.FirstHpLossDone = true self:DrawCards(3) self:RenderHand(false) end end if self.PlayerHp < 0 then self.PlayerHp = 0 end`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'attackerSlot' }, ]), method('EnemyTurn', `self.TurnBusy = true self:EnemyActStep(1)`), method('EnemyActStep', `local idx = 0 for i = fromIndex, #self.Monsters do if self.Monsters[i].alive == true then idx = i; break end end if idx == 0 or self.PlayerHp <= 0 then self:FinishEnemyTurn() return end local m = self.Monsters[idx] local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx) self:SetEntityEnabled(base .. "/ActFrame", true) _TimerService:SetTimerOnce(function() if m.poison ~= nil and m.poison > 0 then m.hp = m.hp - m.poison self:ShowDmgPop(idx, m.poison) self:MonsterHitMotion(idx) m.poison = m.poison - 1 if m.hp <= 0 then m.hp = 0 self:KillMonster(m.slot) self:RenderCombat() self:SetEntityEnabled(base .. "/ActFrame", false) _TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15) return end end m.block = 0 local intent = m.intents[m.intentIdx] if intent ~= nil then if intent.kind == "Attack" then self:MonsterLunge(idx) local atk = intent.value + m.str if m.weak > 0 then atk = math.floor(atk * 0.75) end if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end local before = self.PlayerHp self:DealDamageToPlayer(atk, idx) self:ShowPlayerDmgPop(before - self.PlayerHp) self:PlayerHitMotion() elseif intent.kind == "Defend" then m.block = m.block + intent.value elseif intent.kind == "Debuff" then if intent.effect == "weak" then self.PlayerWeak = self.PlayerWeak + intent.value elseif intent.effect == "vuln" then self.PlayerVuln = self.PlayerVuln + intent.value end end end m.intentIdx = m.intentIdx + 1 if m.intentIdx > #m.intents then m.intentIdx = 1 end if m.weak > 0 then m.weak = m.weak - 1 end if m.vuln > 0 then m.vuln = m.vuln - 1 end self:RenderCombat() self:SetEntityEnabled(base .. "/ActFrame", false) _TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15) end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]), method('FinishEnemyTurn', `self.TurnBusy = false self:CheckCombatEnd() if self.CombatOver == true then return end _TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`), method('CheckCombatEnd', `local anyAlive = false for i = 1, #self.Monsters do if self.Monsters[i].alive == true then anyAlive = true; break end end if anyAlive == false then self.CombatOver = true self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult()) self:ApplyRelics("combatEnd") self:ApplyRelics("combatReward") self:MaybeDropPotion() self:RenderRun() local node = self.MapNodes[self.CurrentNodeId] if node ~= nil and node.type == "elite" then self.Gold = self.Gold + 15 local nid = self:PickNewRelic() if nid ~= "" then self:AddRelic(nid) local nr = self.Relics[nid] if nr ~= nil then self:Toast("유물 획득: " .. nr.name) end end end if node ~= nil and node.type == "boss" then if self.PlayerJob == "" and self.Floor < self.RunLength then self:ShowJobChoice() else local bid = self:PickNewRelic() if bid ~= "" then self:AddRelic(bid) local br = self.Relics[bid] if br ~= nil then self:Toast("유물 획득: " .. br.name) end end self:ContinueAfterBoss() end else self:OfferReward() end elseif self.PlayerHp <= 0 then self.CombatOver = true self:EndRun("패배...") end`), method('ContinueAfterBoss', `if self.Floor < self.RunLength then self.Floor = self.Floor + 1 self.CurrentNodeId = "" self.CurrentEnemyId = "" self:GenerateMap() self:RenderRun() self:TeleportToActMap() self:ShowMap() else self:EndRun("런 클리어!") end`), method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`), method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) if kind == "relic" then local bid = self:PickNewRelic() if bid ~= "" then self:AddRelic(bid) local br = self.Relics[bid] if br ~= nil then self:Toast("유물 획득: " .. br.name) end end self:ContinueAfterBoss() else self:ShowJobSelect() end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]), method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass] if opts == nil then opts = self.Jobs["warrior"] end self.JobOpts = opts for i = 1, 3 do local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i) local o = opts[i] if o ~= nil then self:SetEntityEnabled(base, true) self:SetText(base .. "/Name", o.name) self:SetText(base .. "/Desc", o.desc) local sc = self.Cards[o.starter] if sc ~= nil then self:SetText(base .. "/Starter", "대표 카드: " .. sc.name) end else self:SetEntityEnabled(base, false) end end self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`), method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then for cls, list in pairs(self.Jobs) do for i = 1, #list do if list[i].id == self.PlayerJob then return list[i].name end end end end if self.SelectedClass == "warrior" then return "전사" elseif self.SelectedClass == "magician" then return "마법사" end return "플레이어"`, [], 0, 'string'), method('SetJob', `self.PlayerJob = jobId local starter = "" local opts = self.Jobs[self.SelectedClass] or {} for i = 1, #opts do if opts[i].id == jobId then starter = opts[i].starter end end if starter ~= "" then table.insert(self.RunDeck, starter) local sc = self.Cards[starter] if sc ~= nil then self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name) end end self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]), method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} } local target = maps[self.Floor] if target == nil then return end local lp = _UserService.LocalPlayer if lp == nil then return end if lp.CurrentMapName == target then return end _TeleportService:TeleportToMapPosition(lp, Vector3(-6, 0.03, 0), target)`), method('ShowResult', `self:SetText("/ui/DefaultGroup/CombatHud/Result", text) local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/Result") if entity ~= nil then entity.Enable = true end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]), method('EndRun', `local msg = text if text == "런 클리어!" and self.AscensionLevel >= self.AscensionUnlocked and self.AscensionUnlocked < 10 then self.AscensionUnlocked = self.AscensionUnlocked + 1 local lp = _UserService.LocalPlayer if lp ~= nil then self:SaveAscension(self.AscensionUnlocked, lp.PlayerComponent.UserId) end self:RenderAscension() msg = "런 클리어! 승천 " .. string.format("%d", self.AscensionUnlocked) .. " 해금!" end self:ShowResult(msg) self.RunActive = false _TimerService:SetTimerOnce(function() self:ShowMainMenu() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]), method('BuffsLabel', `local parts = {} if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end return table.concat(parts, " ")`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' }, ], 0, 'string'), method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) local m = self.Monsters[i] if m ~= nil and m.alive == true then self:SetEntityEnabled(base, true) self:SetText(base .. "/Name", m.name) self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp)) local intent = m.intents[m.intentIdx] local t = "" if intent ~= nil then if intent.kind == "Attack" then local atk = intent.value + m.str if m.weak > 0 then atk = math.floor(atk * 0.75) end if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end t = "공격 " .. tostring(atk) elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value) elseif intent.kind == "Debuff" then if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여" else t = "취약 " .. tostring(intent.value) .. " 부여" end end end self:SetText(base .. "/Intent", t) self:SetEntityEnabled(base .. "/TargetFrame", i == self.TargetIndex) local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent") if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then if intent.kind == "Attack" then intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1) elseif intent.kind == "Debuff" then intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1) else intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1) end end self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W}) self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0) self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block)) self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0)) else self:SetEntityEnabled(base, false) end end self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp)) self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then local names = {} for i = 1, #self.PlayerPowers do local pc = self.Cards[self.PlayerPowers[i]] if pc ~= nil then table.insert(names, pc.name) end end if pb ~= "" then pb = pb .. " · " end pb = pb .. table.concat(names, " ") end self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb) self:RenderRun()`), method('ShowDmgPop', `local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot) .. "/DmgPop" self:SetText(base, "-" .. string.format("%d", amount)) self:SetEntityEnabled(base, true) _TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, ]), method('ShowPlayerDmgPop', `local base = "/ui/DefaultGroup/CombatHud/PlayerPanel/DmgPop" if amount > 0 then self:SetText(base, "-" .. string.format("%d", amount)) else self:SetText(base, "막음") end self:SetEntityEnabled(base, true) _TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]), method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer if lp == nil or lp.StateComponent == nil then return end pcall(function() lp.StateComponent:ChangeState("ATTACK") end) _TimerService:SetTimerOnce(function() if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then pcall(function() lp.StateComponent:ChangeState("IDLE") end) end end, 0.5)`), method('PlayerHitMotion', `local lp = _UserService.LocalPlayer if lp == nil then return end if lp.StateComponent ~= nil then pcall(function() lp.StateComponent:ChangeState("HIT") end) end local tr = lp.TransformComponent if tr == nil then return end local p = tr.Position tr.Position = Vector3(p.x - 0.15, p.y, p.z) _TimerService:SetTimerOnce(function() if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then lp.TransformComponent.Position = Vector3(p.x, p.y, p.z) end end, 0.15)`), method('MonsterLunge', `local m = self.Monsters[idx] if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then return end if m.motionBusy == true then return end m.motionBusy = true local e = m.entity local tr = e.TransformComponent if tr == nil then m.motionBusy = false return end local p = tr.Position tr.Position = Vector3(p.x - 0.35, p.y, p.z) _TimerService:SetTimerOnce(function() if isvalid(e) and e.TransformComponent ~= nil then e.TransformComponent.Position = Vector3(p.x, p.y, p.z) end m.motionBusy = false end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]), method('MonsterHitMotion', `local m = self.Monsters[slot] if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then return end local e = m.entity if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then e.SpriteRendererComponent.SpriteRUID = m.hitClip _TimerService:SetTimerOnce(function() if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then e.SpriteRendererComponent.SpriteRUID = m.standClip end end, 0.5) else if m.motionBusy == true then return end m.motionBusy = true local tr = e.TransformComponent if tr == nil then m.motionBusy = false return end local p = tr.Position local seq = { 0.12, -0.12, 0 } for i = 1, #seq do local dx = seq[i] _TimerService:SetTimerOnce(function() if isvalid(e) and e.TransformComponent ~= nil then e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z) end if i == #seq then m.motionBusy = false end end, 0.06 * i) end end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('SetHpBar', `local e = _EntityService:GetEntityByPath(path) if e == nil or e.UITransformComponent == nil then return end local ratio = 0 if maxHp > 0 then ratio = hp / maxHp end if ratio < 0 then ratio = 0 end local w = width * ratio e.UITransformComponent.RectSize = Vector2(w, 14)`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' }, ]), method('PositionMonsterSlot', `local m = self.Monsters[slot] if m == nil or m.entity == nil or not isvalid(m.entity) then return end local tr = m.entity.TransformComponent if tr == nil then return end local wp = tr.WorldPosition local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y})) local uipos = _UILogic:ScreenToUIPosition(screen) local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot)) if e ~= nil and e.UITransformComponent ~= nil then e.UITransformComponent.anchoredPosition = uipos end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then self.TargetIndex = slot self:RenderCombat() end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층" if self.AscensionLevel > 0 then floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel) end self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Floor", floorText) self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`), method('CardPool', `local pool = {} for id, c in pairs(self.Cards) do if c.class == self.SelectedClass or (self.PlayerJob ~= "" and c.class == self.PlayerJob) then table.insert(pool, id) end end table.sort(pool) return pool`, [], 0, 'any'), method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) local pool = self:CardPool() self.RewardChoices = {} for i = 1, 3 do self.RewardChoices[i] = pool[math.random(1, #pool)] self:ApplyRewardVisual(i, self.RewardChoices[i]) end local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud") if hud ~= nil then hud.Enable = true end`), method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, ]), method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then return end if slot ~= 0 and self.RewardChoices ~= nil then local id = self.RewardChoices[slot] if id ~= nil then table.insert(self.RunDeck, id) end end local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud") if hud ~= nil then hud.Enable = false end self:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('HasRelic', `if self.RunRelics == nil then return false end for i = 1, #self.RunRelics do if self.RunRelics[i] == id then return true end end return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'), method('ApplyRelics', `if self.RunRelics == nil then return end for i = 1, #self.RunRelics do local r = self.Relics[self.RunRelics[i]] if r ~= nil and r.hook == hook then if r.effect == "block" then self.PlayerBlock = self.PlayerBlock + r.value elseif r.effect == "energy" then self.Energy = self.Energy + r.value elseif r.effect == "strength" then self.PlayerStr = self.PlayerStr + r.value elseif r.effect == "draw" then self:DrawCards(r.value) self:RenderHand(false) elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then self.PlayerHp = self.PlayerHp + r.value if self.PlayerHp > self.PlayerMaxHp then self.PlayerHp = self.PlayerMaxHp end elseif r.effect == "healIfLow" then if self.PlayerHp * 2 <= self.PlayerMaxHp then self.PlayerHp = self.PlayerHp + r.value if self.PlayerHp > self.PlayerMaxHp then self.PlayerHp = self.PlayerMaxHp end end elseif r.effect == "gold" then self.Gold = self.Gold + r.value end end end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hook' }]), method('AddRelic', `if self.RunRelics == nil then self.RunRelics = {} end table.insert(self.RunRelics, id) local r = self.Relics[id] if r ~= nil and r.hook == "passive" then if r.effect == "potionSlots" then self.PotionSlots = r.value self:RenderPotions() elseif r.effect == "maxHp" then self.PlayerMaxHp = self.PlayerMaxHp + r.value self.PlayerHp = self.PlayerHp + r.value end end self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), method('PickNewRelic', `local pool = {} for i = 1, #self.RelicPool do if self:HasRelic(self.RelicPool[i]) == false then table.insert(pool, self.RelicPool[i]) end end if #pool == 0 then self.Gold = self.Gold + 25 self:Toast("유물을 모두 모았습니다! 메소 +25") return "" end return pool[math.random(1, #pool)]`, [], 0, 'string'), method('AddPotion', `if self.RunPotions == nil then self.RunPotions = {} end if #self.RunPotions >= self.PotionSlots then self:Toast("물약 슬롯이 가득 찼습니다") return false end table.insert(self.RunPotions, pid) self:RenderPotions() return true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pid' }], 0, 'boolean'), method('MaybeDropPotion', `if math.random() > ${POTIONS.dropChance} then return end local keys = {} for pid, _ in pairs(self.Potions) do table.insert(keys, pid) end table.sort(keys) local pid = keys[math.random(1, #keys)] if self:AddPotion(pid) == true then local p = self.Potions[pid] self:Toast("물약 획득: " .. p.name) end`), method('RenderPotions', `for i = 1, 5 do local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i) local e = _EntityService:GetEntityByPath(base) if e ~= nil and e.SpriteGUIRendererComponent ~= nil then local pid = nil if self.RunPotions ~= nil then pid = self.RunPotions[i] end if pid ~= nil and self.Potions[pid] ~= nil then e.SpriteGUIRendererComponent.ImageRUID = self.Potions[pid].icon e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) elseif i > self.PotionSlots then e.SpriteGUIRendererComponent.ImageRUID = "" e.SpriteGUIRendererComponent.Color = Color(0.1, 0.1, 0.12, 0.85) else e.SpriteGUIRendererComponent.ImageRUID = "" e.SpriteGUIRendererComponent.Color = Color(0.22, 0.25, 0.3, 0.9) end end end`), method('OpenPotionMenu', `if self.RunPotions == nil or self.RunPotions[slot] == nil then return end self.PotionMenuSlot = slot local pid = self.RunPotions[slot] local p = self.Potions[pid] if p ~= nil then self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc) end self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('ClosePotionMenu', `self.PotionMenuSlot = 0 self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`), method('UsePotion', `if self.PotionMenuSlot <= 0 then return end if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then self:Toast("지금은 사용할 수 없습니다") return end local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud") local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand") if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then self:Toast("전투 중에만 사용할 수 있습니다") return end local pid = self.RunPotions[self.PotionMenuSlot] if pid == nil then return end local p = self.Potions[pid] if p == nil then return end if p.effect == "heal" then self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp) elseif p.effect == "damage" then self:DealDamageToTarget(p.value, false) self:ShowDmgPop(self.TargetIndex, p.value) elseif p.effect == "strength" then self.PlayerStr = self.PlayerStr + p.value elseif p.effect == "block" then self.PlayerBlock = self.PlayerBlock + p.value elseif p.effect == "energy" then self.Energy = self.Energy + p.value elseif p.effect == "weak" then local tm = self.Monsters[self.TargetIndex] if tm ~= nil and tm.alive == true then tm.weak = tm.weak + p.value end end table.remove(self.RunPotions, self.PotionMenuSlot) self:Toast("물약 사용: " .. p.name) self:ClosePotionMenu() self:RenderPotions() self:RenderPiles() self:RenderCombat() self:CheckCombatEnd()`), method('TossPotion', `if self.PotionMenuSlot <= 0 then return end local pid = self.RunPotions[self.PotionMenuSlot] if pid ~= nil then local p = self.Potions[pid] table.remove(self.RunPotions, self.PotionMenuSlot) if p ~= nil then self:Toast("물약 버림: " .. p.name) end end self:ClosePotionMenu() self:RenderPotions()`), method('RenderRelics', `local count = 0 if self.RunRelics ~= nil then count = #self.RunRelics end for i = 1, 10 do local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i) local e = _EntityService:GetEntityByPath(base) if e ~= nil and e.SpriteGUIRendererComponent ~= nil then local rid = nil if self.RunRelics ~= nil then rid = self.RunRelics[i] end if rid ~= nil and self.Relics[rid] ~= nil and (i < 10 or count <= 10) then e.SpriteGUIRendererComponent.ImageRUID = self.Relics[rid].icon e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) else e.SpriteGUIRendererComponent.ImageRUID = "" e.SpriteGUIRendererComponent.Color = Color(0.15, 0.16, 0.2, 0.6) end end end local of = "" if count > 10 then of = "+" .. tostring(count - 9) end self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`), method('ShowTooltip', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name) self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc) local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox") if e ~= nil then if e.UITransformComponent ~= nil then e.UITransformComponent.anchoredPosition = Vector2(x, 400) end e.Enable = true end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' }, ]), method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`), method('ShowMap', `self:ShowState("map") self:RenderMap()`), method('GenerateMap', `-- 절차 생성 — tools/map/rogue-map.mjs(JS 미러)와 로직 동기화 유지 self.MapNodes = {} self.MapStart = {} self.VisitedNodes = {} self.Depth = 0 self.MapNodes["boss"] = { type = "boss", row = ${MAP_ROWS} + 1, col = 0, next = {} } local cols = { 1, 2, 3, 4 } for i = #cols, 2, -1 do local j = math.random(1, i) cols[i], cols[j] = cols[j], cols[i] end local starts = { cols[1], cols[2], math.random(1, ${MAP_COLS}), math.random(1, ${MAP_COLS}) } for p = 1, 4 do local c = starts[p] local sid = "r1c" .. tostring(c) if self.MapNodes[sid] == nil then self.MapNodes[sid] = { type = "combat", row = 1, col = c, next = {} } end local found = false for i = 1, #self.MapStart do if self.MapStart[i] == sid then found = true end end if found == false then table.insert(self.MapStart, sid) end for r = 1, ${MAP_ROWS} - 1 do local nc = c + math.random(-1, 1) if nc < 1 then nc = 1 end if nc > ${MAP_COLS} then nc = ${MAP_COLS} end local nid = "r" .. tostring(r + 1) .. "c" .. tostring(nc) if self.MapNodes[nid] == nil then self.MapNodes[nid] = { type = "combat", row = r + 1, col = nc, next = {} } end local fid = "r" .. tostring(r) .. "c" .. tostring(c) local dup = false for i = 1, #self.MapNodes[fid].next do if self.MapNodes[fid].next[i] == nid then dup = true end end if dup == false then table.insert(self.MapNodes[fid].next, nid) end c = nc end local lid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c) local bdup = false for i = 1, #self.MapNodes[lid].next do if self.MapNodes[lid].next[i] == "boss" then bdup = true end end if bdup == false then table.insert(self.MapNodes[lid].next, "boss") end end for r = 3, ${MAP_ROWS} do for c = 1, ${MAP_COLS} do local id = "r" .. tostring(r) .. "c" .. tostring(c) local node = self.MapNodes[id] if node ~= nil then local eliteParent = false for pid, pn in pairs(self.MapNodes) do if pn.row == r - 1 and pn.type == "elite" then for i = 1, #pn.next do if pn.next[i] == id then eliteParent = true end end end end local w if r == ${MAP_ROWS} then w = { { "rest", 50 }, { "combat", 25 }, { "shop", 10 }, { "elite", 8 }, { "treasure", 7 } } elseif r >= 4 then w = { { "combat", 45 }, { "elite", 16 }, { "shop", 12 }, { "rest", 12 }, { "treasure", 15 } } else w = { { "combat", 45 }, { "shop", 12 }, { "rest", 12 } } end local total = 0 for i = 1, #w do if w[i][1] == "elite" and eliteParent == true then w[i][2] = 0 end total = total + w[i][2] end local roll = math.random() * total local acc = 0 for i = 1, #w do acc = acc + w[i][2] if roll <= acc then node.type = w[i][1] break end end end end end`), method('IsReachable', `local list if self.CurrentNodeId == "" then list = self.MapStart else local node = self.MapNodes[self.CurrentNodeId] if node == nil then return false end list = node.next end for i = 1, #list do if list[i] == id then return true end end return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'), method('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id local e = _EntityService:GetEntityByPath(base) if e == nil then return end local node = self.MapNodes[id] if node == nil then e.Enable = false return end e.Enable = true local tname = "전투" local r0 = 0.78 local g0 = 0.36 local b0 = 0.32 if node.type == "elite" then tname = "엘리트" r0 = 0.62 g0 = 0.4 b0 = 0.85 elseif node.type == "shop" then tname = "상점" r0 = 0.9 g0 = 0.75 b0 = 0.35 elseif node.type == "rest" then tname = "휴식" r0 = 0.4 g0 = 0.75 b0 = 0.45 elseif node.type == "treasure" then tname = "보물" r0 = 0.35 g0 = 0.7 b0 = 0.75 elseif node.type == "boss" then tname = "보스" r0 = 0.85 g0 = 0.25 b0 = 0.25 end self:SetText(base .. "/Label", tname) local reachable = self:IsReachable(id) local visited = false if self.VisitedNodes ~= nil then for i = 1, #self.VisitedNodes do if self.VisitedNodes[i] == id then visited = true end end end if e.SpriteGUIRendererComponent ~= nil then if id == self.CurrentNodeId then e.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1) elseif visited == true then e.SpriteGUIRendererComponent.Color = Color(0.18, 0.19, 0.22, 0.9) elseif reachable == true then e.SpriteGUIRendererComponent.Color = Color(r0, g0, b0, 1) else e.SpriteGUIRendererComponent.Color = Color(r0 * 0.45, g0 * 0.45, b0 * 0.45, 0.55) end end if e.ButtonComponent ~= nil then e.ButtonComponent.Enable = reachable end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), method('RenderMapDots', `local node = self.MapNodes[fromId] local has = false if node ~= nil then for i = 1, #node.next do if node.next[i] == toId then has = true end end end for k = 1, 3 do local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k)) if d ~= nil then d.Enable = has if has == true and d.SpriteGUIRendererComponent ~= nil then if fromId == self.CurrentNodeId then d.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1) else d.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.8) end end end end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'dotId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toId' }, ]), method('RenderMap', `for r = 1, ${MAP_ROWS} do for c = 1, ${MAP_COLS} do self:RenderMapNode("r" .. tostring(r) .. "c" .. tostring(c)) end end self:RenderMapNode("boss") for r = 1, ${MAP_ROWS} - 1 do for c = 1, ${MAP_COLS} do local fid = "r" .. tostring(r) .. "c" .. tostring(c) for c2 = c - 1, c + 1 do if c2 >= 1 and c2 <= ${MAP_COLS} then self:RenderMapDots(fid .. "_" .. tostring(c2), fid, "r" .. tostring(r + 1) .. "c" .. tostring(c2)) end end end end for c = 1, ${MAP_COLS} do local fid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c) self:RenderMapDots(fid .. "_b", fid, "boss") end `), method('PickNode', `if self.RunActive ~= true then return end if self:IsReachable(id) ~= true then return end self.CurrentNodeId = id if self.VisitedNodes == nil then self.VisitedNodes = {} end table.insert(self.VisitedNodes, id) local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud") if hud ~= nil then hud.Enable = false end local node = self.MapNodes[id] self.Depth = node.row self:RenderRun() if node.type == "shop" then self:ShowShop() elseif node.type == "rest" then self:ShowRest() elseif node.type == "treasure" then self:ShowTreasure() else self.CurrentEnemyId = "" self:StartCombat() end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), method('ShowShop', `local pool = self:CardPool() self.ShopChoices = {} self.ShopBought = { false, false, false } for i = 1, 3 do self.ShopChoices[i] = pool[math.random(1, #pool)] end self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)] self.ShopRelicBought = false local pkeys = {} for pid, _ in pairs(self.Potions) do table.insert(pkeys, pid) end table.sort(pkeys) self.ShopPotion = pkeys[math.random(1, #pkeys)] self.ShopPotionBought = false self:RenderShop() self:ShowState("shop")`), method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold)) for i = 1, 3 do local cid = self.ShopChoices[i] local c = self.Cards[cid] local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i) if c ~= nil then self:ApplyCardFace(base, cid) self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소") local e = _EntityService:GetEntityByPath(base) if e ~= nil and e.SpriteGUIRendererComponent ~= nil then if self.ShopBought[i] == true then e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) end end end end local rr = self.Relics[self.ShopRelic] if rr ~= nil then self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc) self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소") local re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic") if re ~= nil and re.SpriteGUIRendererComponent ~= nil then if self.ShopRelicBought == true then re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) else re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1) end end end local pp = self.Potions[self.ShopPotion] if pp ~= nil then self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc) self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소") local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion") if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then if self.ShopPotionBought == true then pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) else pe.SpriteGUIRendererComponent.Color = Color(0.45, 0.7, 0.55, 1) end end end`), method('BuyRelic', `if self.ShopRelicBought == true then return end if self.Gold < ${RELIC_PRICE} then return end self.Gold = self.Gold - ${RELIC_PRICE} self:AddRelic(self.ShopRelic) self.ShopRelicBought = true self:RenderShop() self:RenderRun()`), method('BuyPotion', `if self.ShopPotionBought == true then return end if self.Gold < ${POTIONS.shopPrice} then return end if self.RunPotions ~= nil and #self.RunPotions >= self.PotionSlots then self:Toast("물약 슬롯이 가득 찼습니다") return end if self:AddPotion(self.ShopPotion) == true then self.Gold = self.Gold - ${POTIONS.shopPrice} self.ShopPotionBought = true end self:RenderShop() self:RenderRun()`), method('BuyCard', `if self.ShopBought == nil or self.ShopBought[slot] == true then return end if self.Gold < ${CARD_PRICE} then return end self.Gold = self.Gold - ${CARD_PRICE} table.insert(self.RunDeck, self.ShopChoices[slot]) self.ShopBought[slot] = true self:RenderShop() self:RenderRun()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('ShowRest', `local old = self.PlayerHp self.PlayerHp = self.PlayerHp + ${REST_HEAL} if self.PlayerHp > self.PlayerMaxHp then self.PlayerHp = self.PlayerMaxHp end local healed = self.PlayerHp - old self:SetText("/ui/DefaultGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")") self:RenderCombat() self:ShowState("rest")`), method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud") if s ~= nil then s.Enable = false end local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud") if r ~= nil then r.Enable = false end local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud") if t ~= nil then t.Enable = false end self:ShowMap()`), method('ShowTreasure', `self.ChestOpened = false local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") if chest ~= nil then if chest.SpriteGUIRendererComponent ~= nil then chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}" end if chest.UITransformComponent ~= nil then chest.UITransformComponent.anchoredPosition = Vector2(0, 40) end end self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false) self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true) self:ShowState("treasure")`), method('OpenChest', `if self.ChestOpened == true then return end self.ChestOpened = true self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false) local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") local steps = { 10, -10, 8, -8, 5, 0 } for i = 1, #steps do local dx = steps[i] _TimerService:SetTimerOnce(function() if chest ~= nil and isvalid(chest) and chest.UITransformComponent ~= nil then chest.UITransformComponent.anchoredPosition = Vector2(dx, 40) end end, 0.08 * i) end _TimerService:SetTimerOnce(function() if chest ~= nil and isvalid(chest) and chest.SpriteGUIRendererComponent ~= nil then chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_OPEN_RUID}" end local g = 40 + math.random(0, 20) local nid = self:PickNewRelic() local msg = "" if nid ~= "" then self:AddRelic(nid) local nr = self.Relics[nid] msg = "유물 획득: " .. nr.name .. " · 메소 +" .. tostring(g) else g = g + 30 msg = "메소 +" .. tostring(g) end self.Gold = self.Gold + g self:RenderRun() self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg) self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true) end, 0.55)`), ]); for (const m of combat.ContentProto.Json.Methods) { if (m.ExecSpace === 0) m.ExecSpace = 6; // 기본은 ClientOnly(6), 서버 RPC(Server=1·Client=2) 명시값은 보존 } writeFileSync('RootDesk/MyDesk/SlayDeckController.codeblock', JSON.stringify(combat, null, 2), 'utf8'); } function patchCommon() { const common = JSON.parse(readFileSync(COMMON_FILE, 'utf8')); const entity = common.ContentProto.Entities.find((e) => e.path === '/common'); entity.componentNames = 'script.SlayDeckController'; entity.jsonString['@components'] = [ { '@type': 'script.SlayDeckController', Enable: true, Energy: 0, MaxEnergy: 3, Turn: 0, TweenEventId: 0 }, ]; JSON.parse(JSON.stringify(common)); writeFileSync(COMMON_FILE, JSON.stringify(common, null, 2), 'utf8'); } upsertUi(); writeCodeblocks(); patchCommon(); console.log('Slay deck UI and combat codeblocks generated.');