import { readFileSync, writeFileSync } from 'node:fs'; import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs'; import { buildDeckHud } from './hud/deckhud.mjs'; import { buildDeckInspect } from './hud/deckinspect.mjs'; import { buildDeckAll } from './hud/deckall.mjs'; import { buildCombat } from './hud/combat.mjs'; import { buildReward } from './hud/reward.mjs'; import { buildMap } from './hud/map.mjs'; import { buildShop } from './hud/shop.mjs'; import { buildRest } from './hud/rest.mjs'; import { buildTreasure } from './hud/treasure.mjs'; import { buildJobChoice } from './hud/jobchoice.mjs'; import { buildJobSelect } from './hud/jobselect.mjs'; import { buildLobby } from './hud/lobby.mjs'; import { buildBoard } from './hud/board.mjs'; import { buildSoulShop } from './hud/soulshop.mjs'; import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from './lib/ui-helpers.mjs'; function upsertUi() { const ui = JSON.parse(readFileSync(UI_FILE, 'utf8')); const E = ui.ContentProto.Entities; // 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: 10 }, (_, i) => { const c = CARDS.cards[previewIds[i % previewIds.length]]; return { name: c.name, cost: String(c.cost), desc: c.desc, frame: frameRuid(c) }; }); // 손패 슬롯 10개 (최대 손패 한도). Card1~5는 기존 엔티티, Card6~10은 신규 생성. for (let i = 1; i <= 10; i++) { const cardPath = `/ui/DefaultGroup/CardHand/Card${i}`; let card = byPath.get(cardPath); if (!card) { card = entity({ id: guid('dck', 500 + i), path: cardPath, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent', displayOrder: 4, 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: 0, y: 0 } }), sprite({ color: WHITE, type: 0, raycast: true }), button(), ], }); ui.ContentProto.Entities.push(card); byPath.set(cardPath, card); } const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent'); const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent'); const sx = -680 + (i - 1) * 150; tr.RectSize = { x: CARD_W, y: CARD_H }; tr.anchoredPosition = { x: sx, y: 0 }; tr.OffsetMin = { x: sx - CARD_W / 2, y: -CARD_H / 2 }; tr.OffsetMax = { x: sx + 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, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), ], }); 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; child.jsonString['@components'][2].Bold = cfg.bold; child.jsonString['@components'][2].DropShadow = cfg.dropShadow === true; child.jsonString['@components'][2].DropShadowDistance = cfg.dropShadow === true ? 18 : 32; child.jsonString['@components'][2].OutlineWidth = cfg.outlineWidth || 1; } } // 프레임 이미지가 이름판·코스트판을 내장하므로 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 }; } } } } emit('DeckHud', buildDeckHud()); emit('DeckInspectHud', buildDeckInspect()); emit('DeckAllHud', buildDeckAll()); emit('CombatHud', buildCombat()); emit('RewardHud', buildReward()); emit('MapHud', buildMap()); emit('ShopHud', buildShop()); emit('RestHud', buildRest()); // 유물 방 — 보물 상자 (P8) emit('TreasureHud', buildTreasure()); // 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직 emit('JobChoiceHud', buildJobChoice()); emit('JobSelectHud', buildJobSelect()); 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', classId: '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', classId: 'bandit', label: '\uB3C4\uC801', desc: '\uB3C5\u00B7\uB2E8\uAC80\u00B7\uB4DC\uB85C\uC6B0', x: 0, enabled: true, tint: { r: 0.26, g: 0.5, b: 0.34, a: 1 } }, { key: 'Mage', classId: 'magician', 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', 200 + i), path: `${base}/Art`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 0, components: [ transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 318 }, pos: { x: 0, y: 0 } }), sprite({ dataId: CHARS.portraits[cls.classId], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), ], })); select.push(entity({ id: guid('menu', 210 + i), path: `${base}/NameBanner`, 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: 258, y: 60 }, pos: { x: 0, y: -137 } }), sprite({ color: { r: 0, g: 0, b: 0, a: 0.55 }, type: 1, raycast: false }), ], })); 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: 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: 54 }, pos: { x: 0, y: -137 } }), 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 }), ], })); 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', 170 + i), path: `/ui/DefaultGroup/CharacterSelectHud/${cls.key}DeckButton`, modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 18 + i, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 46 }, pos: { x: cls.x, y: -160 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }), button({ enabled: cls.enabled }), text({ value: '\uB371 \uBCF4\uAE30', fontSize: 20, bold: true, color: GOLD, alignment: 0 }), ], })); } 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.push(entity({ id: guid('menu', 230), path: '/ui/DefaultGroup/CharacterSelectHud/BackButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 22, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 56 }, pos: { x: -800, y: 430 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }), button(), text({ value: '\u2190 \uB4A4\uB85C', fontSize: 26, bold: true, color: GOLD, alignment: 0 }), ], })); select[0].jsonString.enable = false; emit('MainMenu', menu); emit('CharacterSelectHud', select); // ── LobbyHud — 반복 런의 허브. NPC 클릭으로 런시작/도감/영혼상점/게시판 ── emit('LobbyHud', buildLobby()); // ── BoardHud — 게시판(공지/팁) ── emit('BoardHud', buildBoard()); // ── SoulShopHud — 영혼 메타 상점 (Phase 9에서 해금 항목·구매 로직 채움) ── emit('SoulShopHud', buildSoulShop()); 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); } // 엔티티 id 유일성 검증 — 같은 id가 다른 path에 재배정되면 메이커 refresh 병합이 꼬임 const seenIds = new Map(); for (const e of ui.ContentProto.Entities) { const prev = seenIds.get(e.id); if (prev != null) throw new Error(`[gen-slaydeck] 엔티티 id 중복: ${e.id} (${prev} ↔ ${e.path})`); seenIds.set(e.id, e.path); } 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 = 5; const GOLD_PER_WIN = 25; const CARD_PRICE = 30; const REST_HEAL = 30; const RELIC_PRICE = 60; const ACT_COUNT = 5; const ACT_MAPS = ['map01', 'map02', 'map03', 'map04', 'map05']; const LOBBY_MAP = 'lobby'; const LOBBY_SPAWN = 'Vector3(-5, 0.03, 0)'; // 정찰: map01 지면 좌측 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('number', 'CardHoverTweenId', '0'), prop('number', 'DmgPopSeq', '0'), prop('any', 'EndTurnHandler'), prop('any', 'NewGameHandler'), prop('any', 'WarriorSelectHandler'), prop('any', 'ThiefSelectHandler'), prop('any', 'MageSelectHandler'), prop('any', 'WarriorDeckHandler'), prop('any', 'ThiefDeckHandler'), prop('any', 'MageDeckHandler'), prop('any', 'WarriorDeckTabHandler'), prop('any', 'ThiefDeckTabHandler'), prop('any', 'MageDeckTabHandler'), prop('any', 'AscMinusHandler'), prop('any', 'AscPlusHandler'), prop('any', 'JobOpts'), prop('any', 'Jobs'), prop('number', 'AscensionLevel', '0'), prop('number', 'AscensionUnlocked', '0'), prop('any', 'StartGameHandler'), prop('any', 'CharBackHandler'), prop('string', 'SelectedClass', '""'), prop('any', 'DrawPileHandler'), prop('any', 'DiscardPileHandler'), prop('any', 'DeckInspectCloseHandler'), prop('any', 'AllDeckHandler'), prop('any', 'AllDeckCloseHandler'), prop('number', 'SoulPoints', '0'), prop('boolean', 'LobbyBound', 'false'), prop('number', 'LobbyTpTries', '0'), prop('boolean', 'CodexMode', 'false'), prop('any', 'CodexCards'), prop('boolean', 'ClassDeckMode', 'false'), prop('any', 'ClassDeckCards'), prop('string', 'ClassDeckTitle', '""'), prop('string', 'ClassDeckClass', '""'), prop('any', 'SoulUnlocks'), prop('any', 'SoulShopDef'), prop('boolean', 'SoulShopBound', 'false'), prop('string', 'DeckInspectKind', '""'), prop('boolean', 'DeckAllOpen', 'false'), prop('any', 'Cards'), prop('any', 'CardFrames'), prop('any', 'NodeIcons'), 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('number', 'DragTargetIndex', '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', '""'), prop('number', 'DiscardSelectRemaining', '0'), prop('number', 'DiscardSelectTotal', '0'), ], [ method('OnBeginPlay', `${luaCardsTable(CARDS.cards)} ${luaFramesTable()} ${luaNodeIconsTable()} ${luaSoulShopTable(SOUL_UNLOCKS)} self.SoulUnlocks = {} self.SoulPoints = self.SoulPoints or 0 self:ShowLobby() local lp = _UserService.LocalPlayer if lp ~= nil then self:ReqLoadAscension(lp.PlayerComponent.UserId) self:ReqLoadSouls(lp.PlayerComponent.UserId) end _InputService:ConnectEvent(KeyDownEvent, function(e) if e.key == KeyboardKey.LeftControl then local lp2 = _UserService.LocalPlayer if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then self:PlayerAttackMotion() end end 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)) self:SetText("/ui/DefaultGroup/LobbyHud/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) self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false) self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false) self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`), method('ShowState', `self:HideGameHud() self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu") self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect") self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", state == "lobby") 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 thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton") if thief ~= nil and thief.ButtonComponent ~= nil then if self.ThiefSelectHandler ~= nil then thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler) self.ThiefSelectHandler = nil end self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") 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 warriorDeck = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorDeckButton") if warriorDeck ~= nil and warriorDeck.ButtonComponent ~= nil then if self.WarriorDeckHandler ~= nil then warriorDeck:DisconnectEvent(ButtonClickEvent, self.WarriorDeckHandler) self.WarriorDeckHandler = nil end self.WarriorDeckHandler = warriorDeck:ConnectEvent(ButtonClickEvent, function() self:OpenClassDeck("warrior") end) end local thiefDeck = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefDeckButton") if thiefDeck ~= nil and thiefDeck.ButtonComponent ~= nil then if self.ThiefDeckHandler ~= nil then thiefDeck:DisconnectEvent(ButtonClickEvent, self.ThiefDeckHandler) self.ThiefDeckHandler = nil end self.ThiefDeckHandler = thiefDeck:ConnectEvent(ButtonClickEvent, function() self:OpenClassDeck("bandit") end) end local mageDeck = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageDeckButton") if mageDeck ~= nil and mageDeck.ButtonComponent ~= nil then if self.MageDeckHandler ~= nil then mageDeck:DisconnectEvent(ButtonClickEvent, self.MageDeckHandler) self.MageDeckHandler = nil end self.MageDeckHandler = mageDeck:ConnectEvent(ButtonClickEvent, function() self:OpenClassDeck("magician") 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 self:BindClassDeckTabs() 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 charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton") if charBack ~= nil and charBack.ButtonComponent ~= nil then if self.CharBackHandler ~= nil then charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler) self.CharBackHandler = nil end self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() 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('ShowLobby', `self.SelectedClass = "" self:RenderAscension() self:RenderSoulLabel() self:ShowState("lobby") self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false) self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false) self:BindLobbyButtons() self:BindMenuButtons() self:GoLobbyMap()`), method('GoLobbyMap', `self.LobbyTpTries = 0 local eventId = 0 local function go() self.LobbyTpTries = self.LobbyTpTries + 1 local lp = _UserService.LocalPlayer if lp ~= nil then if lp.CurrentMapName ~= "${LOBBY_MAP}" then _TeleportService:TeleportToMapPosition(lp, ${LOBBY_SPAWN}, "${LOBBY_MAP}") end _TimerService:ClearTimer(eventId) elseif self.LobbyTpTries > 50 then _TimerService:ClearTimer(eventId) end end eventId = _TimerService:SetTimerRepeat(go, 0.1)`), method('OnLobbyNpcInteract', `if self.RunActive == true then return end if id == "run" then self:ShowCharacterSelect() elseif id == "codex" then self:ShowCodex() elseif id == "shop" then self:ShowSoulShop() elseif id == "board" then self:ShowBoard() end`, [{ Type: 'string', DefaultValue: '""', SyncDirection: 0, Attributes: [], Name: 'id' }]), method('RenderSoulLabel', `local s = self.SoulPoints or 0 self:SetText("/ui/DefaultGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", s)) self:SetText("/ui/DefaultGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", s))`), method('BindLobbyButtons', `if self.LobbyBound == true then return end self.LobbyBound = true local function bindClick(path, fn) local e = _EntityService:GetEntityByPath(path) if e ~= nil and e.ButtonComponent ~= nil then e:ConnectEvent(ButtonClickEvent, fn) end end bindClick("/ui/DefaultGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end) bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end) bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end) bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`), method('ShowCodex', `self.CodexMode = true self.ClassDeckMode = true local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") if close ~= nil and close.ButtonComponent ~= nil then if self.AllDeckCloseHandler ~= nil then close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) end self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) end self:BindClassDeckTabs() self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false) self:SetClassDeckTab("warrior") local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") if hud ~= nil then hud.Enable = true end self:RenderAllDeck()`), method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`), method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`), method('ShowSoulShop', `self:RenderSoulLabel() self:RenderSoulShop() self:BindSoulShopButtons() self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`), method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`), method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId) local e1, pts = ds:GetAndWait("soulPoints") local e2, unl = ds:GetAndWait("soulUnlocks") local p = 0 if e1 == 0 and pts ~= nil and pts ~= "" then p = tonumber(pts) or 0 end local u = "" if e2 == 0 and unl ~= nil then u = unl end self:RecvSouls(p, u, userId)`, [{ Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5), method('RecvSouls', `self.SoulPoints = p self.SoulUnlocks = {} if u ~= nil and u ~= "" then for key in string.gmatch(u, "([^,]+)") do self.SoulUnlocks[key] = true end end self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 6), method('SaveSouls', `local ds = _DataStorageService:GetUserDataStorage(userId) ds:SetAndWait("soulPoints", tostring(p)) ds:SetAndWait("soulUnlocks", u)`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5), method('SerializeUnlocks', `local parts = {} if self.SoulUnlocks ~= nil then for k, v in pairs(self.SoulUnlocks) do if v == true then table.insert(parts, k) end end end return table.concat(parts, ",")`, [], 0, 'string'), method('AwardSouls', `self.SoulPoints = (self.SoulPoints or 0) + n local lp = _UserService.LocalPlayer if lp ~= nil then self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId) end self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]), method('BuySoulUnlock', `local d = nil if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end if d == nil then return end if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then self:Toast("이미 보유 중입니다") return end if (self.SoulPoints or 0) < d.cost then self:Toast("영혼이 부족합니다") return end self.SoulPoints = self.SoulPoints - d.cost if self.SoulUnlocks == nil then self.SoulUnlocks = {} end self.SoulUnlocks[d.key] = true local lp = _UserService.LocalPlayer if lp ~= nil then self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId) end self:Toast(d.name .. " 해금!") self:RenderSoulLabel() self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]), method('RenderSoulShop', `local defs = self.SoulShopDef or {} for i = 1, 4 do local base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i) local d = defs[i] if d == nil then self:SetEntityEnabled(base, false) else self:SetEntityEnabled(base, true) self:SetText(base .. "/Name", d.name) self:SetText(base .. "/Desc", d.desc) local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true if owned then self:SetText(base .. "/Status", "보유 중") elseif (self.SoulPoints or 0) >= d.cost then self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매") else self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족") end end end`), method('BindSoulShopButtons', `if self.SoulShopBound == true then return end self.SoulShopBound = true for i = 1, 4 do local idx = i local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)) if e ~= nil and e.ButtonComponent ~= nil then e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end) end end`), method('ApplySoulUnlocks', `if self.SoulUnlocks == nil then return end if self.SoulUnlocks["meso"] == true then self.Gold = self.Gold + 60 end if self.SoulUnlocks["hp"] == true then self.PlayerMaxHp = self.PlayerMaxHp + 15 self.PlayerHp = self.PlayerMaxHp end if self.SoulUnlocks["trim"] == true then for i = 1, #self.RunDeck do local cid = self.RunDeck[i] if cid == "Defend" or cid == "MagicGuard" or cid == "DarkSight" then table.remove(self.RunDeck, i) break end end end if self.SoulUnlocks["relic"] == true then local nid = self:PickNewRelic() if nid ~= "" then self:AddRelic(nid) 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(1, 0.82, 0.3, 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(1, 0.82, 0.3, 1) else mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) end end local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton") if thief ~= nil and thief.SpriteGUIRendererComponent ~= nil then if self.SelectedClass == "bandit" then thief.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) else thief.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 == "bandit" 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 ~= "bandit" 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(', ')} } elseif self.SelectedClass == "bandit" then self.PlayerMaxHp = ${CLASSES.bandit.maxHp} self.RunDeck = { ${CARDS.starterDecks.bandit.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()} ${luaNodeIconsTable()} self:GenerateMap() self:BindButtons() self:AddRelic("${RELICS.startingRelic}") self:ApplySoulUnlocks() self:RenderPotions() self:TeleportToActMap() self:ShowMap()`), method('KickCombatCamera', `local cam = nil local lp = _UserService.LocalPlayer if lp ~= nil then cam = lp.CameraComponent end if cam == nil then cam = _CameraService:GetCurrentCameraComponent() end if cam ~= nil then cam.ConfineCameraArea = false end _TimerService:SetTimerOnce(function() local cc = nil local lp2 = _UserService.LocalPlayer if lp2 ~= nil then cc = lp2.CameraComponent end if cc == nil then cc = _CameraService:GetCurrentCameraComponent() end if cc ~= nil then cc.ZoomRatio = ${CAM.zoomRatio} cc.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY}) cc.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY}) cc.ConfineCameraArea = true end end, 0.2)`), method('StartCombat', `self:ShowState("combat") self:KickCombatCamera() self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/DiscardPrompt", 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.DmgPopSeq = 0 self.FirstHpLossDone = false self.ClayBlockNext = 0 self.DiscardSelectRemaining = 0 self.DiscardSelectTotal = 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 byGroup = {} for i = 1, #reg do local r = reg[i] if r.entity ~= nil and isvalid(r.entity) and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then local gg = r.group if gg == nil or gg == "" then gg = "combat" end if byGroup[gg] == nil then byGroup[gg] = {} end local x = 0 if r.entity.TransformComponent ~= nil then x = r.entity.TransformComponent.WorldPosition.x end table.insert(byGroup[gg], { entity = r.entity, enemyId = r.enemyId, x = x }) end end -- 노드 타입별 랜덤 구성: 일반 1~3 / 엘리트 1+일반0~2 / 보스 1 local chosen = {} local function takeFrom(key, k) local src = byGroup[key] or {} local pool = {} for i = 1, #src do pool[i] = src[i] end self:Shuffle(pool) local taken = 0 for i = 1, #pool do if taken >= k then break end table.insert(chosen, pool[i]) taken = taken + 1 end end if g == "boss" then takeFrom("boss", 1) elseif g == "elite" then takeFrom("elite", 1) takeFrom("combat", math.random(0, 2)) else takeFrom("combat", math.random(1, 3)) end if #chosen == 0 then takeFrom(g, 1) end if #chosen == 0 then takeFrom("combat", 1) end table.sort(chosen, function(a, b) return a.x < b.x end) local mult = 1 + (self.Floor - 1) * 0.45 if g == "elite" or g == "boss" then mult = mult + self:AscEliteBonus() end local n = #chosen if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end for i = 1, n do local item = chosen[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 or 0 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, card = e.intents[k].card, count = e.intents[k].count } 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 local startIdx = 1 if #intents > 0 then startIdx = math.random(1, #intents) 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 = startIdx, 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 self:BindClassDeckTabs() for i = 1, 10 do local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then local cardPath = "/ui/DefaultGroup/CardHand/Card" .. tostring(i) cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) 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) cardEntity:ConnectEvent(UITouchEnterEvent, function() self:HoverCard(i) end) cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end) if cardEntity.ButtonComponent ~= nil then cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end) 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) if rc.UITouchReceiveComponent ~= nil then local cardPath = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(i) rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) 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) if sc.UITouchReceiveComponent ~= nil then local cardPath = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i) sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) 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 if self:IsDiscardSelecting() == true then self:Toast("버릴 카드를 먼저 선택하세요") return end local burn = 0 for bi = 1, #self.Hand do \tlocal hc = self.Cards[self.Hand[bi]] \tif hc ~= nil and hc.endTurnDamage ~= nil then burn = burn + hc.endTurnDamage end end if burn > 0 then \tself.PlayerHp = self.PlayerHp - burn \tif self.PlayerHp < 0 then self.PlayerHp = 0 end \tself:ShowPlayerDmgPop(burn) \tself:RenderCombat() end local kept = {} for i = 1, #self.Hand do \tlocal cardId = self.Hand[i] \tlocal c = self.Cards[cardId] \tif c ~= nil and c.retain == true then \t\ttable.insert(kept, cardId) \telse \t\ttable.insert(self.DiscardPile, cardId) \tend end self.Hand = kept 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', `local drawnSlots = {} 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) \tif #self.Hand >= 10 then \t\ttable.insert(self.DiscardPile, cardId) \t\tself:TriggerSly(cardId) \telse \t\ttable.insert(self.Hand, cardId) \t\tif #self.Hand <= 5 then \t\t\ttable.insert(drawnSlots, #self.Hand) \t\tend \tend end self:RenderPiles() if animate == true and #drawnSlots > 0 then \tself:RenderHand(false) \tlocal drawStart = Vector2(-590, 8) \tfor i = 1, #drawnSlots do \t\tlocal slot = drawnSlots[i] \t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045) \tend end`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }, ]), 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('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/WarriorTab") if warriorTab ~= nil and warriorTab.ButtonComponent ~= nil then if self.WarriorDeckTabHandler ~= nil then warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler) self.WarriorDeckTabHandler = nil end self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end) end local thiefTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/ThiefTab") if thiefTab ~= nil and thiefTab.ButtonComponent ~= nil then if self.ThiefDeckTabHandler ~= nil then thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler) self.ThiefDeckTabHandler = nil end self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("bandit") end) end local mageTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/MageTab") if mageTab ~= nil and mageTab.ButtonComponent ~= nil then if self.MageDeckTabHandler ~= nil then mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler) self.MageDeckTabHandler = nil end self.MageDeckTabHandler = mageTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("magician") end) end`), method('OpenClassDeck', `self.CodexMode = false self.ClassDeckMode = true self.DeckAllOpen = true self:SetClassDeckTab(className) local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") if hud ~= nil then hud.Enable = true end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]), method('SetClassDeckTab', `if self.ClassDeckMode ~= true then return end self.ClassDeckCards = {} self.ClassDeckTitle = "직업 덱" if className ~= "warrior" and className ~= "magician" and className ~= "bandit" then className = "bandit" end self.ClassDeckClass = className local allowed = {} if className == "warrior" then allowed["warrior"] = true allowed["fighter"] = true allowed["page"] = true allowed["spearman"] = true self.ClassDeckTitle = "전사 전체 덱" elseif className == "magician" then allowed["magician"] = true allowed["firepoison"] = true allowed["icelightning"] = true allowed["cleric"] = true self.ClassDeckTitle = "마법사 전체 덱" else allowed["bandit"] = true allowed["shiv"] = true allowed["poisoner"] = true allowed["trickster"] = true self.ClassDeckTitle = "도적 전체 덱" end for id, c in pairs(self.Cards) do if c ~= nil and c.curse ~= true and allowed[c.class] == true then table.insert(self.ClassDeckCards, id) end end table.sort(self.ClassDeckCards, function(a, b) local ca = self.Cards[a] local cb = self.Cards[b] local na = a local nb = b if ca ~= nil and ca.name ~= nil then na = ca.name end if cb ~= nil and cb.name ~= nil then nb = cb.name end if na == nb then return a < b end return na < nb end) self:RenderAllDeck() self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]), method('RenderClassDeckTabs', `local tabs = { { path = "/ui/DefaultGroup/DeckAllHud/WarriorTab", cls = "warrior" }, { path = "/ui/DefaultGroup/DeckAllHud/ThiefTab", cls = "bandit" }, { path = "/ui/DefaultGroup/DeckAllHud/MageTab", cls = "magician" }, } for i = 1, #tabs do local e = _EntityService:GetEntityByPath(tabs[i].path) if e ~= nil then e.Enable = self.ClassDeckMode == true if e.SpriteGUIRendererComponent ~= nil then if self.ClassDeckClass == tabs[i].cls then e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1) else e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1) end end end end`), method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") if inspectHud ~= nil then inspectHud.Enable = false end self.DeckInspectKind = "" self.ClassDeckMode = false self.ClassDeckClass = "" self:RenderClassDeckTabs() 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 if self.ClassDeckMode == true then self.ClassDeckMode = false self.ClassDeckCards = {} self.ClassDeckTitle = "" self.ClassDeckClass = "" end self:RenderClassDeckTabs() if self.CodexMode == true then self.CodexMode = false self:ShowLobby() end`), method('RenderAllDeck', `local pile = self.RunDeck or {} local title = "모든 덱" if self.ClassDeckMode == true then pile = self.ClassDeckCards or {} title = self.ClassDeckTitle elseif self.CodexMode == true then pile = self.CodexCards or {} title = "카드 도감" end local count = #pile self:SetText("/ui/DefaultGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")") self:RenderClassDeckTabs() 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('GetHandSlotX', `local n = 0 if self.Hand ~= nil then n = #self.Hand end if n <= 0 then return 0 end local spacing = 175 if n > 8 then spacing = math.floor(1400 / n) end local startX = -((n - 1) * spacing) / 2 return startX + (slot - 1) * spacing`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'), method('RenderHand', `local n = #self.Hand local spacing = 175 if n > 8 then spacing = math.floor(1400 / n) end local startX = -((n - 1) * spacing) / 2 local drawStart = Vector2(-590, 8) for i = 1, 10 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\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end \t\t\tself:ApplyCardVisual(i, cardId) \t\t\tlocal tx = self:GetHandSlotX(i) \t\t\tif animate == true then \t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03) \t\t\telse \t\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.anchoredPosition = Vector2(tx, 0) end \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 if e.UITransformComponent ~= nil then e.UITransformComponent.UIScale = Vector3(1, 1, 1) end 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('SetCardHover', `local prefix = "" local count = 0 local xs = {} local baseY = 0 local hoverIndex = 0 local push = 110 if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then if self.DragSlot ~= nil and self.DragSlot > 0 then return end prefix = "/ui/DefaultGroup/CardHand/Card" count = 0 if self.Hand ~= nil then count = #self.Hand end for i = 1, count do xs[i] = self:GetHandSlotX(i) end baseY = 0 hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0 elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then prefix = "/ui/DefaultGroup/RewardHud/Reward" count = 3 xs = { -300, 0, 300 } baseY = 0 hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0 elseif string.find(path, "/ui/DefaultGroup/ShopHud/Card") == 1 then prefix = "/ui/DefaultGroup/ShopHud/Card" count = 3 xs = { -300, 0, 300 } baseY = 20 hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0 end if count <= 0 then return end if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then _TimerService:ClearTimer(self.CardHoverTweenId) self.CardHoverTweenId = 0 end local items = {} for i = 1, count do local e = _EntityService:GetEntityByPath(prefix .. tostring(i)) if e ~= nil and e.UITransformComponent ~= nil then local tr = e.UITransformComponent local tx = xs[i] local ty = baseY local sc = 1 if hover == true and hoverIndex > 0 then if i == hoverIndex and e.Enable == true then sc = 1.5 elseif i < hoverIndex then tx = tx - push elseif i > hoverIndex then tx = tx + push end end table.insert(items, { tr = tr, sx = tr.anchoredPosition.x, sy = tr.anchoredPosition.y, ss = tr.UIScale.x, tx = tx, ty = ty, ts = sc }) end end local elapsed = 0 local duration = 0.12 local eventId = 0 eventId = _TimerService:SetTimerRepeat(function() elapsed = elapsed + 1 / 60 local t = math.min(elapsed / duration, 1) local eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t) for i = 1, #items do local it = items[i] local x = it.sx + (it.tx - it.sx) * eased local y = it.sy + (it.ty - it.sy) * eased local s = it.ss + (it.ts - it.ss) * eased it.tr.anchoredPosition = Vector2(x, y) it.tr.UIScale = Vector3(s, s, 1) end if t >= 1 then _TimerService:ClearTimer(eventId) if self.CardHoverTweenId == eventId then self.CardHoverTweenId = 0 end end end, 1 / 60) self.CardHoverTweenId = eventId`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' }, ]), 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('ResolveCardEffects', `if c == nil then return end 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.fx or c.image, total) else self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true) end end if c.block ~= nil then self.PlayerBlock = self.PlayerBlock + c.block end if free ~= true then self:ApplyRelics("cardPlayed") end 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 and free ~= true 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 or tm.alive ~= true then for i = 1, #self.Monsters do if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end end end 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 if c.draw ~= nil then self:DrawCards(c.draw, true) end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' }, ]), method('TriggerSly', `local c = self.Cards[cardId] if c == nil or c.sly ~= true then return end self:Toast("교활 발동: " .. c.name) self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]), method('DiscardHandCard', `if self.Hand == nil then return end local cardId = self.Hand[slot] if cardId == nil then return end table.remove(self.Hand, slot) table.insert(self.DiscardPile, cardId) if triggerSly == true then self:TriggerSly(cardId) end`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'triggerSly' }, ]), method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'), method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/DiscardPrompt") if e == nil then return end if self:IsDiscardSelecting() == true then local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining self:SetText("/ui/DefaultGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. tostring(picked + 1) .. "/" .. tostring(self.DiscardSelectTotal)) e.Enable = true else e.Enable = false end`), method('BeginDiscardSelection', `if c == nil or self.Hand == nil then return false end local n = 0 if c.discardAll == true then n = #self.Hand elseif c.discard ~= nil then n = math.min(c.discard, #self.Hand) end if n <= 0 then return false end self.DiscardSelectRemaining = n self.DiscardSelectTotal = n self:UpdateDiscardPrompt() self:Toast("버릴 카드를 선택하세요") return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'), method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0 self.DiscardSelectTotal = 0 self:UpdateDiscardPrompt() self:RenderHand(false) self:RenderPiles() self:RenderCombat() self:CheckCombatEnd()`), method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then return false end if self.Hand == nil or self.Hand[slot] == nil then return true end self:DiscardHandCard(slot, true) self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1 if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then self:FinishDiscardSelection() else self:UpdateDiscardPrompt() self:RenderHand(false) self:RenderPiles() self:RenderCombat() end return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'), method('PlayCard', `if self:IsDiscardSelecting() == true then self:SelectDiscardSlot(slot) return end 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 c.unplayable == true then self:Toast("사용할 수 없는 카드입니다") return end if self.Energy < c.cost then self:Toast("에너지가 부족합니다") return end self.Energy = self.Energy - c.cost self:ResolveCardEffects(cardId, c, false) table.remove(self.Hand, slot) if c.kind ~= "Power" then table.insert(self.DiscardPile, cardId) end self:RenderHand(false) self:RenderPiles() self:RenderCombat() if self:BeginDiscardSelection(c) == true then return end self:RenderHand(false) self:RenderPiles() self:RenderCombat() self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('OnCardButton', `if self:IsDiscardSelecting() == true then self:SelectDiscardSlot(slot) end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('FindMonsterAtTouch', `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 return best`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }], 0, 'number'), method('RenderTargetFrames', `local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 local shownTarget = self.TargetIndex if dragActive == true then shownTarget = self.DragTargetIndex end for i = 1, #self.Monsters do local m = self.Monsters[i] local active = false if m ~= nil and m.alive == true and i == shownTarget then active = true end self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetFrame", active) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive) end`), 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 if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then _TimerService:ClearTimer(self.CardHoverTweenId) self.CardHoverTweenId = 0 end for i = 1, 10 do local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) if e ~= nil and e.UITransformComponent ~= nil then e.UITransformComponent.UIScale = Vector3(1, 1, 1) e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0) end end self.DragSlot = slot self.DragTargetIndex = 0 self:RenderTargetFrames()`, [{ 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 local cardId = self.Hand[slot] local c = nil if cardId ~= nil then c = self.Cards[cardId] end if c ~= nil and c.kind == "Attack" then local best = self:FindMonsterAtTouch(touchPoint) if best ~= self.DragTargetIndex then self.DragTargetIndex = best self:RenderTargetFrames() end 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 e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) if e ~= nil and e.UITransformComponent ~= nil then e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0) e.UITransformComponent.UIScale = Vector3(1, 1, 1) 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:IsDiscardSelecting() == true then self:SelectDiscardSlot(slot) return end 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 = self.DragTargetIndex or 0 if best <= 0 then best = self:FindMonsterAtTouch(touchPoint) end self.DragTargetIndex = 0 if best > 0 then self.TargetIndex = best self:PlayCard(slot) self:RenderTargetFrames() else self:RenderTargetFrames() end else self.DragTargetIndex = 0 self:RenderTargetFrames() 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 elseif intent.kind == "AddCard" then local cnt = intent.count or 1 for ci = 1, cnt do table.insert(self.DiscardPile, intent.card) end self:RenderPiles() local cn = intent.card local cc = self.Cards[intent.card] if cc ~= nil then cn = cc.name end self:Toast(m.name .. ": " .. cn .. " 추가!") end end if #m.intents > 0 then m.intentIdx = math.random(1, #m.intents) 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('ClearCombatCards', `self.DrawPile = {} self.DiscardPile = {} self.Hand = {} self.DiscardSelectRemaining = 0 self.DiscardSelectTotal = 0 self:UpdateDiscardPrompt() self:RenderHand(false) self:RenderPiles()`), 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:ClearCombatCards() 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 if self.PlayerJob ~= "" then self:AwardSouls(1) end 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 == "bandit" 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:ShowLobby() 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 elseif intent.kind == "AddCard" then t = "저주 카드 추가" end end self:SetText(base .. "/Intent", t) local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 local shownTarget = self.TargetIndex if dragActive == true then shownTarget = self.DragTargetIndex end self:SetEntityEnabled(base .. "/TargetFrame", i == shownTarget) self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive) self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive) 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) elseif intent.kind == "AddCard" then intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 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 slotKey = string.format("%d", math.floor(slot or 0)) local base = "/ui/DefaultGroup/CombatHud/DmgPop" .. slotKey local pop = _EntityService:GetEntityByPath(base) if pop == nil then return end self.DmgPopSeq = (self.DmgPopSeq or 0) + 1 local popSeq = self.DmgPopSeq self:SetText(base, "") local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} } local shown = tostring(math.max(0, math.floor(amount))) if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS}) end local digits = {} for i = 1, string.len(shown) do table.insert(digits, tonumber(string.sub(shown, i, i)) or 0) end local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING} local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2 for i = 1, ${DAMAGE_POP_MAX_DIGITS} do self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false) end for i = 1, ${DAMAGE_POP_MAX_DIGITS} do local digitPath = base .. "/Digit" .. tostring(i) local digitEntity = _EntityService:GetEntityByPath(digitPath) if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then if digits[i] ~= nil then digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1] digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) if digitEntity.UITransformComponent ~= nil then digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0) end self:SetEntityEnabled(digitPath, true) else self:SetEntityEnabled(digitPath, false) end end end local popPos = nil local m = self.Monsters[slot] if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then local wp = m.entity.TransformComponent.WorldPosition local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45})) popPos = _UILogic:ScreenToUIPosition(screen) else local slotEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. slotKey) if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then local sp = slotEntity.UITransformComponent.anchoredPosition popPos = Vector2(sp.x, sp.y + 76) end end if pop ~= nil and pop.UITransformComponent ~= nil then if popPos ~= nil then pop.UITransformComponent.anchoredPosition = popPos else pop.UITransformComponent.anchoredPosition = Vector2(0, 120) end end self:SetEntityEnabled(base, true) for i = 1, 6 do _TimerService:SetTimerOnce(function() if self.DmgPopSeq ~= popSeq then return end local p = _EntityService:GetEntityByPath(base) if p ~= nil and p.UITransformComponent ~= nil then local cur = p.UITransformComponent.anchoredPosition p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7) end end, 0.045 * i) end _TimerService:SetTimerOnce(function() if self.DmgPopSeq ~= popSeq then return end self:SetEntityEnabled(base, false) end, 0.48)`, [ { 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 then return end if 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() local byRarity = {} for _, id in ipairs(pool) do local r = self.Cards[id].rarity or "normal" if byRarity[r] == nil then byRarity[r] = {} end table.insert(byRarity[r], id) end self.RewardChoices = {} for i = 1, 3 do local roll = math.random(1, 100) local want = "normal" if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end local bucket = byRarity[want] if bucket == nil or #bucket == 0 then bucket = pool end self.RewardChoices[i] = bucket[math.random(1, #bucket)] 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('BuildCardKeywordTooltip', `if c == nil then return "" end local lines = {} local function add(name, desc) for i = 1, #lines do if string.find(lines[i], name .. ":", 1, true) == 1 then return end end table.insert(lines, name .. ": " .. desc) end local cardDesc = c.desc or "" if c.sly == true or string.find(cardDesc, "교활", 1, true) ~= nil then add("교활", "버려지면 비용 없이 사용됩니다.") end if c.retain == true or string.find(cardDesc, "보존", 1, true) ~= nil then add("보존", "턴 종료 시 버려지지 않고 손에 남습니다.") end if string.find(cardDesc, "소멸", 1, true) ~= nil then add("소멸", "사용 후 이번 전투 동안 제거됩니다.") end if string.find(cardDesc, "선천성", 1, true) ~= nil then add("선천성", "전투 시작 시 손패에 들어옵니다.") end if c.vuln ~= nil and c.vuln > 0 then add("취약", "받는 공격 피해가 50% 증가합니다.") end if c.weak ~= nil and c.weak > 0 then add("약화", "주는 공격 피해가 25% 감소합니다.") end if c.poison ~= nil and c.poison > 0 then add("중독", "턴 시작 시 체력을 잃고 수치가 1 감소합니다.") end if c.pierce == true then add("관통", "방어도를 무시하고 피해를 줍니다.") end if c.aoe == true then add("전체", "모든 적에게 적용됩니다.") end if c.kind == "Power" then add("파워", "사용하면 전투 동안 지속 효과로 남습니다.") end if c.unplayable == true then add("저주", "사용할 수 없고 손패를 방해합니다.") end local out = "" for i = 1, #lines do if i > 1 then out = out .. "\\n" end out = out .. lines[i] end return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'string'), method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then return end local cardId = self.Hand[slot] if cardId == nil then return end local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) local tx = 0 if e ~= nil and e.UITransformComponent ~= nil then tx = e.UITransformComponent.anchoredPosition.x e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1) end local c = self.Cards[cardId] if c ~= nil then local tip = self:BuildCardKeywordTooltip(c) if tip ~= "" then local tipX = tx + 270 if tx > 180 then tipX = tx - 270 end if tipX > 760 then tipX = tx - 270 end if tipX < -760 then tipX = tx + 270 end self:ShowTooltipAt("키워드", tip, tipX, 90) else self:HideTooltip() end end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) if e ~= nil and e.UITransformComponent ~= nil then e.UITransformComponent.UIScale = Vector3(1, 1, 1) end self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('ShowTooltip', `self:ShowTooltipAt(name, desc, x, 400)`, [ { 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('ShowTooltipAt', `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, y) 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' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' }, ]), 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 -- 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지) local parentTypes = {} for pid, pn in pairs(self.MapNodes) do if pn.row == r - 1 then for i = 1, #pn.next do if pn.next[i] == id then parentTypes[pn.type] = 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 local t = w[i][1] if (t == "elite" or t == "rest" or t == "shop") and parentTypes[t] == 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 ruid = self.NodeIcons[node.type] if ruid == nil then ruid = self.NodeIcons["combat"] end if e.SpriteGUIRendererComponent ~= nil and ruid ~= nil then e.SpriteGUIRendererComponent.ImageRUID = ruid end 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(1, 0.82, 0.3, 1) elseif visited == true then e.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.9) elseif reachable == true then e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) else e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85) 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.');