Files
maplecontest/tools/deck/gen-slaydeck.mjs
gahusb 62b2193f2e feat(potions-relics): 유물 아이콘 행·물약 슬롯·툴팁·물약 메뉴 UI (생성기+산출물)
- TopBar: RelicSlot×10(+오버플로)·PotionSlot×5 (UITouchReceive hover 툴팁)
- TooltipBox(이름+설명)·PotionMenu(사용/버리기/닫기) 팝업
- ShopHud Potion 판매 항목, AllDeckButton 우측 이동

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

3669 lines
150 KiB
JavaScript

import { readFileSync, writeFileSync } from 'node:fs';
const CARDS = JSON.parse(readFileSync('data/cards.json', 'utf8'));
const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
// 검증 (fail-fast): 잘못된 데이터면 생성 중단
for (const id of CARDS.starterDeck) {
if (!CARDS.cards[id]) {
throw new Error(`[gen-slaydeck] starterDeck에 없는 카드 id 참조: ${id}`);
}
}
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`);
}
const MAP = JSON.parse(readFileSync('data/map.json', 'utf8'));
for (const id of MAP.start) {
if (!MAP.nodes[id]) throw new Error(`[gen-slaydeck] map.start에 없는 노드 id: ${id}`);
}
for (const [id, n] of Object.entries(MAP.nodes)) {
if (n.enemy && !ENEMIES.enemies[n.enemy]) throw new Error(`[gen-slaydeck] 노드 ${id}의 enemy 없음: ${n.enemy}`);
for (const nx of n.next) {
if (!MAP.nodes[nx]) throw new Error(`[gen-slaydeck] 노드 ${id}.next에 없는 노드 id: ${nx}`);
}
}
const MAX_ROW = Math.max(...Object.values(MAP.nodes).map((n) => n.row));
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
for (const id of RELICS.relicPool) {
if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`);
}
function luaRelicsTable(relics) {
const lines = Object.entries(relics).map(([id, r]) =>
`\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`);
return `self.Relics = {\n${lines.join('\n')}\n}`;
}
const POTIONS = JSON.parse(readFileSync('data/potions.json', 'utf8'));
for (const [pid, p] of Object.entries(POTIONS.potions)) {
if (!p.name || !p.effect || p.value == null) throw new Error(`[gen-slaydeck] potion 필드 누락: ${pid}`);
}
function luaPotionsTable(potions) {
const lines = Object.entries(potions).map(([id, p]) =>
`\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`);
return `self.Potions = {\n${lines.join('\n')}\n}`;
}
function luaIntentsArray(intents) {
return '{ ' + intents.map((it) => {
const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value}`];
if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`);
return `{ ${fields.join(', ')} }`;
}).join(', ') + ' }';
}
function luaEnemiesTable(enemies) {
const lines = Object.entries(enemies).map(([id, e]) =>
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
return `self.Enemies = {\n${lines.join('\n')}\n}`;
}
function luaMapNodesTable(nodes) {
const lines = Object.entries(nodes).map(([id, n]) => {
const nx = '{ ' + n.next.map(luaStr).join(', ') + ' }';
const enemyField = n.enemy ? `enemy = ${luaStr(n.enemy)}, ` : '';
return `\t${id} = { type = ${luaStr(n.type)}, ${enemyField}row = ${n.row}, col = ${n.col}, next = ${nx} },`;
});
return `self.MapNodes = {\n${lines.join('\n')}\n}`;
}
function luaStartArray(start) {
return 'self.MapStart = { ' + start.map(luaStr).join(', ') + ' }';
}
// Lua 직렬화 헬퍼
function luaStr(s) {
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
}
function luaCardsTable(cards) {
const lines = Object.entries(cards).map(([id, c]) => {
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
if (c.damage != null) fields.push(`damage = ${c.damage}`);
if (c.block != null) fields.push(`block = ${c.block}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`);
if (c.weak != null) fields.push(`weak = ${c.weak}`);
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
if (c.value != null) fields.push(`value = ${c.value}`);
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
return `\t${id} = { ${fields.join(', ')} },`;
});
return `self.Cards = {\n${lines.join('\n')}\n}`;
}
function luaDeckTable(deck) {
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
}
const UI_FILE = 'ui/DefaultGroup.ui';
const COMMON_FILE = 'Global/common.gamelogic';
const UI_ROOT = '/ui/DefaultGroup';
const GENERATED_UI_SECTIONS = [
'DeckHud',
'DeckInspectHud',
'DeckAllHud',
'CombatHud',
'RewardHud',
'MapHud',
'ShopHud',
'RestHud',
'MainMenu',
'CharacterSelectHud',
];
const UI_APPEND_ORDER = [
'DeckHud',
'CombatHud',
'RewardHud',
'MapHud',
'ShopHud',
'RestHud',
'DeckInspectHud',
'DeckAllHud',
'MainMenu',
'CharacterSelectHud',
];
const DISABLED_STOCK_CONTROLS = ['Button_Attack', 'Button_Jump', 'UIJoystick'];
const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
const DARK = { r: 0.08, g: 0.09, b: 0.11, a: 0.92 };
const GOLD = { r: 0.94, g: 0.74, b: 0.26, a: 1 };
const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1 };
const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1 };
const SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 };
const MAX_MONSTERS = 4;
const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산
const HP_BAR_W = 140;
const CARD_W = 180;
const CARD_H = 250;
const CARD_SPACING = 200;
const CARD_XS = [-400, -200, 0, 200, 400];
const ALIGN_CENTER = 0;
const ALIGN_BOTTOM_CENTER = 6;
function guid(prefix, n) {
// 유효한 8-4-4-4-12 hex GUID 생성. prefix는 충돌 방지용 네임스페이스 바이트로 매핑.
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : 0xfe;
const v = (ns * 0x100000 + n) >>> 0;
return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`;
}
function transform({ parentW, parentH, anchor, pivot, size, pos, align = 0 }) {
const offMin = { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y };
const offMax = { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y };
return {
'@type': 'MOD.Core.UITransformComponent',
ActivePlatform: 255,
AlignmentOption: align,
AnchorsMax: anchor,
AnchorsMin: anchor,
MobileOnly: false,
OffsetMax: offMax,
OffsetMin: offMin,
Pivot: pivot,
RectSize: size,
UIMode: 1,
UIScale: { x: 1, y: 1, z: 1 },
UIVersion: 2,
anchoredPosition: pos,
Position: { x: anchor.x * parentW - parentW / 2 + pos.x, y: anchor.y * parentH - parentH / 2 + pos.y, z: 0 },
QuaternionRotation: { x: 0, y: 0, z: 0, w: 1 },
Scale: { x: 1, y: 1, z: 1 },
Enable: true,
};
}
function sprite({ dataId = '', color = TRANSPARENT, type = 1, raycast = false }) {
return {
'@type': 'MOD.Core.SpriteGUIRendererComponent',
AnimClipPlayType: 0,
EndFrameIndex: 2147483647,
ImageRUID: { DataId: dataId },
LocalPosition: { x: 0, y: 0 },
LocalScale: { x: 1, y: 1 },
OverrideSorting: false,
PlayRate: 1,
PreserveSprite: 0,
StartFrameIndex: 0,
Color: color,
DropShadow: false,
DropShadowAngle: 30,
DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 },
DropShadowDistance: 32,
FillAmount: 1,
FillCenter: true,
FillClockWise: true,
FillMethod: 0,
FillOrigin: 0,
FlipX: false,
FlipY: false,
FrameColumn: 1,
FrameRate: 0,
FrameRow: 1,
Outline: false,
OutlineColor: { r: 0, g: 0, b: 0, a: 1 },
OutlineWidth: 3,
RaycastTarget: raycast,
Type: type,
Enable: true,
};
}
function button({ enabled = true } = {}) {
return {
'@type': 'MOD.Core.ButtonComponent',
Colors: {
NormalColor: { r: 1, g: 1, b: 1, a: 1 },
HighlightedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 },
PressedColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 1 },
SelectedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 },
DisabledColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 0.5019608 },
ColorMultiplier: 1,
FadeDuration: 0.1,
},
ImageRUIDs: {
HighlightedSprite: null,
PressedSprite: null,
SelectedSprite: null,
DisabledSprite: null,
},
KeyCode: 0,
OverrideSorting: false,
Transition: 1,
Enable: enabled,
};
}
function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4 }) {
return {
'@type': 'MOD.Core.TextComponent',
Alignment: alignment,
Bold: bold,
DropShadow: false,
DropShadowAngle: 30,
DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 },
DropShadowDistance: 32,
Font: 0,
FontColor: color,
FontSize: fontSize,
MaxSize: fontSize,
MinSize: 8,
OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 },
OutlineDistance: { x: 1, y: -1 },
OutlineWidth: 1,
Overflow: 0,
OverrideSorting: false,
Padding: { left: 0, right: 0, top: 0, bottom: 0 },
SizeFit: false,
Text: value,
UseOutLine: true,
Enable: true,
};
}
function scrollLayoutGroup({ cellSize, spacing, columns }) {
return {
'@type': 'MOD.Core.ScrollLayoutGroupComponent',
CellSize: cellSize,
ChildAlignment: 0,
Constraint: 1,
ConstraintCount: columns,
GridChildAlignment: 0,
GridSpacing: spacing,
HorizontalScrollBarDirection: 0,
IgnoreMapLayerCheck: false,
OrderInLayer: 0,
OverrideSorting: false,
Padding: { left: 16, right: 16, top: 16, bottom: 16 },
ReverseArrangement: false,
ScrollBarBackgroundColor: { r: 1, g: 1, b: 1, a: 0.18 },
ScrollBarBgImageRUID: { DataId: '' },
ScrollBarHandleColor: { r: 0.94, g: 0.74, b: 0.26, a: 0.9 },
ScrollBarHandleImageRUID: { DataId: '' },
ScrollBarThickness: 12,
ScrollBarVisible: 1,
SortingLayer: 'UI',
Spacing: 0,
StartAxis: 0,
StartCorner: 0,
Type: 2,
UseScroll: true,
VerticalScrollBarDirection: 1,
Enable: true,
};
}
function popupLayerFor(path) {
if (path.startsWith('/ui/DefaultGroup/DeckAllHud')) return { root: '/ui/DefaultGroup/DeckAllHud', base: 4000 };
if (path.startsWith('/ui/DefaultGroup/DeckInspectHud')) return { root: '/ui/DefaultGroup/DeckInspectHud', base: 3000 };
return null;
}
function uiOrderFor(path, displayOrder) {
const popup = popupLayerFor(path);
if (popup != null) {
const relative = path.slice(popup.root.length).split('/').filter(Boolean);
return popup.base + relative.length * 100 + displayOrder;
}
return displayOrder;
}
function displayOrderFor(path, displayOrder) {
return uiOrderFor(path, displayOrder);
}
function applySortingOverride(path, components, displayOrder) {
if (popupLayerFor(path) == null) return components;
const order = uiOrderFor(path, displayOrder);
return components.map((component) => {
if (component['@type'] !== 'MOD.Core.SpriteGUIRendererComponent' && component['@type'] !== 'MOD.Core.TextComponent') {
return component;
}
return {
...component,
OverrideSorting: true,
SortingLayer: 'UI',
OrderInLayer: order,
};
});
}
function entity({ id, path, modelId, entryId, componentNames, components, displayOrder }) {
const parts = path.split('/');
const name = parts[parts.length - 1];
const sortedComponents = applySortingOverride(path, components, displayOrder);
return {
id,
path,
componentNames,
jsonString: {
name,
path,
nameEditable: true,
enable: true,
visible: true,
localize: true,
displayOrder: displayOrderFor(path, displayOrder),
pathConstraints: '/'.repeat(parts.length - 1),
revision: 1,
origin: {
type: 'Model',
entry_id: entryId,
sub_entity_id: null,
root_entity_id: null,
replaced_model_id: null,
},
modelId,
'@components': sortedComponents,
'@version': 1,
},
};
}
function uiPath(...parts) {
return [UI_ROOT, ...parts].join('/');
}
function sectionRoot(section) {
return uiPath(section);
}
function isGeneratedUiEntity(e) {
return GENERATED_UI_SECTIONS.some((section) => e.path.startsWith(sectionRoot(section)));
}
function appendUiSection(ui, section, entities) {
if (!GENERATED_UI_SECTIONS.includes(section)) {
throw new Error(`[gen-slaydeck] unknown generated UI section: ${section}`);
}
const root = sectionRoot(section);
for (const e of entities) {
if (!e.path.startsWith(root)) {
throw new Error(`[gen-slaydeck] ${section} section emitted unexpected path: ${e.path}`);
}
}
ui.ContentProto.Entities.push(...entities);
}
function upsertUi() {
const ui = JSON.parse(readFileSync(UI_FILE, 'utf8'));
const E = ui.ContentProto.Entities;
ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e));
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
const uiSections = new Map();
const emit = (section, entities) => {
if (uiSections.has(section)) {
throw new Error(`[gen-slaydeck] duplicate generated UI section: ${section}`);
}
uiSections.set(section, entities);
};
for (const path of DISABLED_STOCK_CONTROLS.map((name) => uiPath(name))) {
const e = byPath.get(path);
if (e != null) {
e.jsonString.enable = false;
e.jsonString.visible = false;
for (const component of e.jsonString['@components'] || []) {
component.Enable = false;
if (component.RaycastTarget != null) component.RaycastTarget = false;
}
}
}
// 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시
const previewIds = Object.keys(CARDS.cards);
const cards = Array.from({ length: 5 }, (_, i) => {
const c = CARDS.cards[previewIds[i % previewIds.length]];
return { name: c.name, cost: String(c.cost), desc: c.desc, tint: c.kind === 'Attack' ? ATTACK : DEFEND };
});
for (let i = 1; i <= 5; i++) {
const card = byPath.get(`/ui/DefaultGroup/CardHand/Card${i}`);
if (!card) continue;
const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent');
tr.RectSize = { x: CARD_W, y: CARD_H };
tr.anchoredPosition = { x: CARD_XS[i - 1], y: 0 };
tr.OffsetMin = { x: CARD_XS[i - 1] - CARD_W / 2, y: -CARD_H / 2 };
tr.OffsetMax = { x: CARD_XS[i - 1] + CARD_W / 2, y: CARD_H / 2 };
sp.ImageRUID = { DataId: '' };
sp.Type = 1;
sp.Color = cards[i - 1].tint;
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 children = [
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: cards[i - 1].cost, fontSize: 26, bold: true }],
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: cards[i - 1].name, fontSize: 20, bold: true }],
['Desc', { size: { x: 164, y: 70 }, pos: { x: 0, y: -62 }, value: cards[i - 1].desc, fontSize: 18, bold: false }],
];
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 }),
],
});
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;
}
}
const frameKids = [
['NamePlate', 3, { size: { x: 168, y: 34 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
['CostPlate', 4, { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
['Art', 5, { size: { x: 96, y: 96 }, pos: { x: 0, y: 52 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
];
for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) {
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
if (!byPath.get(fPath)) {
const 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);
}
}
}
const hud = [];
const add = (e) => hud.push(e);
add(entity({
id: guid('hud', 0),
path: '/ui/DefaultGroup/DeckHud',
modelId: 'uiempty',
entryId: 'UIEmpty',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 5,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }),
sprite({ color: TRANSPARENT }),
],
}));
for (const pile of [
{ key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.20, b: 0.25, a: 1 } },
{ key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } },
]) {
add(entity({
id: guid('hud', hud.length),
path: `/ui/DefaultGroup/DeckHud/${pile.key}`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: pile.key === 'DrawPile' ? 0 : 1,
components: [
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }),
sprite({ color: pile.color, type: 1, raycast: true }),
button(),
],
}));
add(entity({
id: guid('hud', hud.length),
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }),
sprite({ color: TRANSPARENT }),
text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }),
],
}));
add(entity({
id: guid('hud', hud.length),
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }),
sprite({ color: TRANSPARENT }),
text({ value: pile.count, fontSize: 42, bold: true }),
],
}));
}
add(entity({
id: guid('hud', hud.length),
path: '/ui/DefaultGroup/DeckHud/EndTurnButton',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 160 }, align: ALIGN_CENTER }),
sprite({ color: DARK, type: 1, raycast: true }),
button(),
text({ value: '턴 종료', fontSize: 28, bold: true, color: GOLD, alignment: 0 }),
],
}));
add(entity({
id: guid('hud', hud.length),
path: '/ui/DefaultGroup/DeckHud/EnergyOrb',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 3,
components: [
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 160 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }),
],
}));
add(entity({
id: guid('hud', hud.length),
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }),
sprite({ color: TRANSPARENT }),
text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }),
],
}));
add(entity({
id: guid('hud', hud.length),
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }),
sprite({ color: TRANSPARENT }),
text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }),
],
}));
emit('DeckHud', hud);
const inspect = [];
const inspectHud = entity({
id: guid('ins', 0),
path: '/ui/DefaultGroup/DeckInspectHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 15,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }),
],
});
inspectHud.jsonString.enable = false;
inspect.push(inspectHud);
inspect.push(entity({
id: guid('ins', 1),
path: '/ui/DefaultGroup/DeckInspectHud/Panel',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1040, y: 760 }, pos: { x: 0, y: 10 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
],
}));
inspect.push(entity({
id: guid('ins', 2),
path: '/ui/DefaultGroup/DeckInspectHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 54 }, pos: { x: 0, y: 350 } }),
sprite({ color: TRANSPARENT }),
text({ value: '\uB371 \uBCF4\uAE30', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
],
}));
inspect.push(entity({
id: guid('ins', 3),
path: '/ui/DefaultGroup/DeckInspectHud/Close',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 466, y: 350 } }),
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
button(),
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
],
}));
inspect.push(entity({
id: guid('ins', 4),
path: '/ui/DefaultGroup/DeckInspectHud/Empty',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 3,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }),
sprite({ color: TRANSPARENT }),
text({ value: '\uCE74\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
],
}));
inspect.push(entity({
id: guid('ins', 5),
path: '/ui/DefaultGroup/DeckInspectHud/Grid',
modelId: 'uiempty',
entryId: 'UIEmpty',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
displayOrder: 4,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 950, y: 610 }, pos: { x: 0, y: 0 } }),
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
],
}));
let insN = 6;
const INSPECT_CARD_COUNT = 60;
const INSPECT_CARD_W = 158;
const INSPECT_CARD_H = 214;
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
const card = entity({
id: guid('ins', insN++),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: i,
components: [
transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }),
sprite({ color: ATTACK, type: 1 }),
],
});
card.jsonString.enable = false;
inspect.push(card);
for (const [suffix, cfg] of [
['Cost', { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 }, value: '1', fontSize: 22, bold: true }],
['Name', { size: { x: 148, y: 26 }, pos: { x: 0, y: -8 }, value: '', fontSize: 17, bold: true }],
['Desc', { size: { x: 144, y: 60 }, pos: { x: 0, y: -54 }, value: '', fontSize: 15, bold: false }],
]) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
inspect.push(entity({
id: guid('ins', insN++),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: dOrder,
components: [
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
],
}));
}
for (const [suffix, dOrder, cfg, color, spriteType] of [
['NamePlate', 3, { size: { x: 148, y: 30 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
['CostPlate', 4, { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
['Art', 5, { size: { x: 84, y: 84 }, pos: { x: 0, y: 44 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
]) {
inspect.push(entity({
id: guid('ins', insN++),
path: `${cardPath}/${suffix}`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: dOrder,
components: [
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color, type: spriteType, raycast: false }),
],
}));
}
}
emit('DeckInspectHud', inspect);
const allDeck = [];
const allHud = entity({
id: guid('all', 0),
path: '/ui/DefaultGroup/DeckAllHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 16,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }),
],
});
allHud.jsonString.enable = false;
allDeck.push(allHud);
allDeck.push(entity({
id: guid('all', 1),
path: '/ui/DefaultGroup/DeckAllHud/Panel',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1080, y: 800 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
],
}));
allDeck.push(entity({
id: guid('all', 2),
path: '/ui/DefaultGroup/DeckAllHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 54 }, pos: { x: 0, y: 380 } }),
sprite({ color: TRANSPARENT }),
text({ value: '모든 덱', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
],
}));
allDeck.push(entity({
id: guid('all', 3),
path: '/ui/DefaultGroup/DeckAllHud/Close',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 486, y: 380 } }),
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
button(),
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
],
}));
allDeck.push(entity({
id: guid('all', 4),
path: '/ui/DefaultGroup/DeckAllHud/Empty',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 3,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 40 } }),
sprite({ color: TRANSPARENT }),
text({ value: '덱이 없습니다', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
],
}));
allDeck.push(entity({
id: guid('all', 5),
path: '/ui/DefaultGroup/DeckAllHud/Grid',
modelId: 'uiempty',
entryId: 'UIEmpty',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
displayOrder: 4,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 980, y: 620 }, pos: { x: 0, y: 0 } }),
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
],
}));
let allN = 6;
const ALL_DECK_CARD_COUNT = 120;
const ALL_DECK_CARD_W = 158;
const ALL_DECK_CARD_H = 214;
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
const card = entity({
id: guid('all', allN++),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: i,
components: [
transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }),
sprite({ color: ATTACK, type: 1 }),
],
});
card.jsonString.enable = false;
allDeck.push(card);
for (const [suffix, cfg] of [
['Cost', { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 }, value: '1', fontSize: 22, bold: true }],
['Name', { size: { x: 148, y: 26 }, pos: { x: 0, y: -8 }, value: '', fontSize: 17, bold: true }],
['Desc', { size: { x: 144, y: 60 }, pos: { x: 0, y: -54 }, value: '', fontSize: 15, bold: false }],
]) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
allDeck.push(entity({
id: guid('all', allN++),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: dOrder,
components: [
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
],
}));
}
for (const [suffix, dOrder, cfg, color, spriteType] of [
['NamePlate', 3, { size: { x: 148, y: 30 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
['CostPlate', 4, { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
['Art', 5, { size: { x: 84, y: 84 }, pos: { x: 0, y: 44 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
]) {
allDeck.push(entity({
id: guid('all', allN++),
path: `${cardPath}/${suffix}`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: dOrder,
components: [
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color, type: spriteType, raycast: false }),
],
}));
}
}
emit('DeckAllHud', allDeck);
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
const combat = [];
combat.push(entity({
id: guid('cmb', 0),
path: '/ui/DefaultGroup/CombatHud',
modelId: 'uiempty',
entryId: 'UIEmpty',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 4,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT }),
],
}));
const SLOT_W = 140, SLOT_H = 96;
for (let i = 1; i <= MAX_MONSTERS; i++) {
const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`;
const slot = entity({
id: guid('cmb', 40 + i),
path: base,
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: 20 + i,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }),
sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }),
button(),
],
});
slot.jsonString.enable = false;
combat.push(slot);
const targetFrame = entity({
id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }),
],
});
targetFrame.jsonString.enable = false;
combat.push(targetFrame);
const actFrame = entity({
id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
sprite({ color: { r: 0.95, g: 0.3, b: 0.25, a: 0.3 }, type: 1 }),
],
});
actFrame.jsonString.enable = false;
combat.push(actFrame);
combat.push(entity({
id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 30 }, pos: { x: 0, y: 34 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
],
}));
combat.push(entity({
id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 26 }, pos: { x: 0, y: 6 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
combat.push(entity({
id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 3,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }),
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 4,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }),
sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 5,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 40, y: 24 }, pos: { x: 0, y: -36 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }),
],
}));
const dmgPop = entity({
id: guid('cmb', 250 + i), path: `${base}/DmgPop`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 9,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 30 }, pos: { x: 0, y: 60 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 24, bold: true, color: { r: 1, g: 0.35, b: 0.3, a: 1 }, alignment: 4 }),
],
});
dmgPop.jsonString.enable = false;
combat.push(dmgPop);
const mBlockBadge = entity({
id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 6,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 36 }, pos: { x: -HP_BAR_W / 2 - 30, y: -14 } }),
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
],
});
mBlockBadge.jsonString.enable = false;
combat.push(mBlockBadge);
combat.push(entity({
id: guid('cmb', 280 + i), path: `${base}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 40, parentH: 36, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 32 }, pos: { x: 0, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value: '0', fontSize: 17, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
combat.push(entity({
id: guid('cmb', 290 + i), path: `${base}/Buffs`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 7,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 60, y: 22 }, pos: { x: 0, y: -58 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 15, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
],
}));
}
const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel';
combat.push(entity({
id: guid('cmb', 210), path: PP, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 5,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 96 }, pos: { x: -760, y: -494 }, align: ALIGN_CENTER }),
sprite({ color: PANEL_BG, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }),
sprite({ color: TRANSPARENT }),
text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }),
],
}));
combat.push(entity({
id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 1,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }),
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 2,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 14 }, pos: { x: -94, y: -6 } }),
sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 3,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }),
sprite({ color: TRANSPARENT }),
text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
const blockBadge = entity({
id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 4,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }),
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
],
});
blockBadge.jsonString.enable = false;
combat.push(blockBadge);
combat.push(entity({
id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
combat.push(entity({
id: guid('cmb', 217), path: `${PP}/Buffs`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 6,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 22 }, pos: { x: 0, y: -44 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 14, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
],
}));
const playerDmgPop = entity({
id: guid('cmb', 260), path: `${PP}/DmgPop`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 5,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 30 }, pos: { x: 16, y: 40 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 22, bold: true, color: { r: 1, g: 0.4, b: 0.35, a: 1 }, alignment: 4 }),
],
});
playerDmgPop.jsonString.enable = false;
combat.push(playerDmgPop);
combat.push(entity({
id: guid('cmb', 200),
path: '/ui/DefaultGroup/CombatHud/TopBar',
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 9,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }),
],
}));
const topTexts = [
['Floor', -520, 160, '막 1/3', GOLD],
['Gold', -360, 160, '골드 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
];
topTexts.forEach(([suffix, x, w, value, color], ti) => {
combat.push(entity({
id: guid('cmb', 201 + ti),
path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`,
modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: ti,
components: [
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value, fontSize: 22, bold: true, color, alignment: 4 }),
],
}));
});
for (let i = 1; i <= 10; i++) {
combat.push(entity({
id: guid('cmb', 300 + i),
path: `/ui/DefaultGroup/CombatHud/TopBar/RelicSlot${i}`,
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent',
displayOrder: 3 + i,
components: [
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: -240 + (i - 1) * 48, y: 0 } }),
sprite({ color: { r: 0.15, g: 0.16, b: 0.2, a: 0.6 }, type: 0, raycast: true }),
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
],
}));
}
combat.push(entity({
id: guid('cmb', 311),
path: '/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow',
modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 14,
components: [
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 60, y: 30 }, pos: { x: 192, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 18, bold: true, color: { r: 0.8, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }),
],
}));
for (let i = 1; i <= 5; i++) {
combat.push(entity({
id: guid('cmb', 320 + i),
path: `/ui/DefaultGroup/CombatHud/TopBar/PotionSlot${i}`,
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent',
displayOrder: 14 + i,
components: [
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: 240 + (i - 1) * 44, y: 0 } }),
sprite({ color: { r: 0.22, g: 0.25, b: 0.3, a: 0.9 }, type: 0, raycast: true }),
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
],
}));
}
const tooltipBox = entity({
id: guid('cmb', 330),
path: '/ui/DefaultGroup/CombatHud/TooltipBox',
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 20,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 80 }, pos: { x: 0, y: 400 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.04, g: 0.05, b: 0.08, a: 0.96 }, type: 1 }),
],
});
tooltipBox.jsonString.enable = false;
combat.push(tooltipBox);
combat.push(entity({
id: guid('cmb', 331),
path: '/ui/DefaultGroup/CombatHud/TooltipBox/Name',
modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 300, parentH: 80, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 18 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 19, bold: true, color: GOLD, alignment: 4 }),
],
}));
combat.push(entity({
id: guid('cmb', 332),
path: '/ui/DefaultGroup/CombatHud/TooltipBox/Desc',
modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 300, parentH: 80, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 284, y: 30 }, pos: { x: 0, y: -14 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 4 }),
],
}));
const potionMenu = entity({
id: guid('cmb', 340),
path: '/ui/DefaultGroup/CombatHud/PotionMenu',
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 21,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 180 }, pos: { x: 0, y: 120 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.07, g: 0.08, b: 0.12, a: 0.97 }, type: 1 }),
],
});
potionMenu.jsonString.enable = false;
combat.push(potionMenu);
combat.push(entity({
id: guid('cmb', 341),
path: '/ui/DefaultGroup/CombatHud/PotionMenu/Title',
modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 36 }, pos: { x: 0, y: 52 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 19, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
const pmButtons = [
['Use', '사용', -120, { r: 0.32, g: 0.55, b: 0.36, a: 1 }],
['Toss', '버리기', 0, { r: 0.6, g: 0.32, b: 0.3, a: 1 }],
['Close', '닫기', 120, { r: 0.25, g: 0.28, b: 0.35, a: 1 }],
];
pmButtons.forEach(([suffix, label, x, color], bi) => {
combat.push(entity({
id: guid('cmb', 342 + bi),
path: `/ui/DefaultGroup/CombatHud/PotionMenu/${suffix}`,
modelId: 'uibutton', entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 1 + bi,
components: [
transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 104, y: 46 }, pos: { x, y: -40 } }),
sprite({ color, type: 1, raycast: true }),
button(),
text({ value: label, fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
});
combat.push(entity({
id: guid('cmb', 205),
path: '/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton',
modelId: 'uibutton', entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 3,
components: [
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 140, y: 40 }, pos: { x: 528, y: 0 } }),
sprite({ color: DARK, type: 1, raycast: true }),
button(),
text({ value: '모든덱보기', fontSize: 18, bold: true, color: GOLD, alignment: 0 }),
],
}));
const skillFx = entity({
id: guid('cmb', 230), path: '/ui/DefaultGroup/CombatHud/SkillFx',
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 30,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 110, y: 110 }, pos: { x: 0, y: 0 } }),
sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }),
],
});
skillFx.jsonString.enable = false;
combat.push(skillFx);
const result = entity({
id: guid('cmb', 2),
path: '/ui/DefaultGroup/CombatHud/Result',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 8,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }),
],
});
result.jsonString.enable = false;
combat.push(result);
emit('CombatHud', combat);
const reward = [];
const rewardHud = entity({
id: guid('rwd', 0),
path: '/ui/DefaultGroup/RewardHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 6,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.86 }, type: 1, raycast: true }),
],
});
rewardHud.jsonString.enable = false;
reward.push(rewardHud);
reward.push(entity({
id: guid('rwd', 1),
path: '/ui/DefaultGroup/RewardHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 64 }, pos: { x: 0, y: 300 } }),
sprite({ color: TRANSPARENT }),
text({ value: '보상 카드 선택', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
],
}));
let rwdN = 2;
const rewardXs = [-300, 0, 300];
for (let i = 1; i <= 3; i++) {
const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`;
reward.push(entity({
id: guid('rwd', rwdN++),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: i,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: rewardXs[i - 1], y: 0 } }),
sprite({ color: ATTACK, type: 1, raycast: true }),
button(),
],
}));
for (const [suffix, cfg] of [
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: '1', fontSize: 26, bold: true }],
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: '카드', fontSize: 20, bold: true }],
['Desc', { size: { x: 164, y: 70 }, pos: { x: 0, y: -62 }, value: '', fontSize: 18, bold: false }],
]) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
reward.push(entity({
id: guid('rwd', rwdN++),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: dOrder,
components: [
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
],
}));
}
for (const [suffix, dOrder, cfg, color, spriteType] of [
['NamePlate', 3, { size: { x: 168, y: 34 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
['CostPlate', 4, { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
['Art', 5, { size: { x: 96, y: 96 }, pos: { x: 0, y: 52 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
]) {
reward.push(entity({
id: guid('rwd', rwdN++),
path: `${cardPath}/${suffix}`,
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 }),
],
}));
}
}
reward.push(entity({
id: guid('rwd', rwdN++),
path: '/ui/DefaultGroup/RewardHud/Skip',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 10,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -260 } }),
sprite({ color: DARK, type: 1, raycast: true }),
button(),
text({ value: '건너뛰기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
],
}));
emit('RewardHud', reward);
const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스', shop: '상점', rest: '휴식' };
const map = [];
const mapHud = entity({
id: guid('map', 0),
path: '/ui/DefaultGroup/MapHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 7,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.9 }, type: 1, raycast: true }),
],
});
mapHud.jsonString.enable = false;
map.push(mapHud);
map.push(entity({
id: guid('map', 1),
path: '/ui/DefaultGroup/MapHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 510 } }),
sprite({ color: TRANSPARENT }),
text({ value: '다음 노드 선택', fontSize: 40, bold: true, color: GOLD, alignment: 4 }),
],
}));
let mapN = 2;
for (const [id, node] of Object.entries(MAP.nodes)) {
const nodePath = `/ui/DefaultGroup/MapHud/Node_${id}`;
const pos = { x: node.col * 180, y: (node.row - (MAX_ROW + 1) / 2) * 140 };
map.push(entity({
id: guid('map', mapN++),
path: nodePath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: node.row,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 150, y: 80 }, pos }),
sprite({ color: { r: 0.3, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }),
button(),
],
}));
map.push(entity({
id: guid('map', mapN++),
path: `${nodePath}/Label`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 150, parentH: 80, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 144, y: 72 }, pos: { x: 0, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value: node.enemy ? `${TYPE_KO[node.type]}\n${ENEMIES.enemies[node.enemy].name}` : TYPE_KO[node.type], fontSize: 20, bold: true }),
],
}));
}
emit('MapHud', map);
const shop = [];
const shopHud = entity({
id: guid('shp', 0),
path: '/ui/DefaultGroup/ShopHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 8,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }),
],
});
shopHud.jsonString.enable = false;
shop.push(shopHud);
shop.push(entity({
id: guid('shp', 1),
path: '/ui/DefaultGroup/ShopHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }),
sprite({ color: TRANSPARENT }),
text({ value: '상점', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
],
}));
shop.push(entity({
id: guid('shp', 2),
path: '/ui/DefaultGroup/ShopHud/Gold',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 44 }, pos: { x: 0, y: 330 } }),
sprite({ color: TRANSPARENT }),
text({ value: '골드 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
],
}));
let shpN = 3;
const shopXs = [-300, 0, 300];
for (let i = 1; i <= 3; i++) {
const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`;
shop.push(entity({
id: guid('shp', shpN++),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: i,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: shopXs[i - 1], y: 20 } }),
sprite({ color: ATTACK, type: 1, raycast: true }),
button(),
],
}));
for (const [suffix, cfg] of [
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: '1', fontSize: 26, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: '카드', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
['Desc', { size: { x: 164, y: 56 }, pos: { x: 0, y: -58 }, value: '', fontSize: 18, bold: false, color: { r: 1, g: 1, b: 1, a: 1 } }],
['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -105 }, value: '30 골드', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }],
]) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9;
shop.push(entity({
id: guid('shp', shpN++),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: dOrder,
components: [
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
],
}));
}
for (const [suffix, dOrder, cfg, color, spriteType] of [
['NamePlate', 3, { size: { x: 168, y: 34 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
['CostPlate', 4, { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
['Art', 5, { size: { x: 96, y: 96 }, pos: { x: 0, y: 52 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
]) {
shop.push(entity({
id: guid('shp', shpN++),
path: `${cardPath}/${suffix}`,
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 }),
],
}));
}
}
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Relic',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: 9,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -190 } }),
sprite({ color: { r: 0.7, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }),
button(),
],
}));
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Relic/Label',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }),
sprite({ color: TRANSPARENT }),
text({ value: '유물', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Relic/Price',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }),
sprite({ color: TRANSPARENT }),
text({ value: '60 골드', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
],
}));
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Potion',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: 11,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -278 } }),
sprite({ color: { r: 0.45, g: 0.7, b: 0.55, a: 1 }, type: 1, raycast: true }),
button(),
],
}));
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Potion/Label',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }),
sprite({ color: TRANSPARENT }),
text({ value: '물약', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Potion/Price',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }),
sprite({ color: TRANSPARENT }),
text({ value: '20 골드', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
],
}));
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Leave',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 10,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 } }),
sprite({ color: DARK, type: 1, raycast: true }),
button(),
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
],
}));
emit('ShopHud', shop);
const rest = [];
const restHud = entity({
id: guid('rst', 0),
path: '/ui/DefaultGroup/RestHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 9,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.05, g: 0.08, b: 0.06, a: 0.92 }, type: 1, raycast: true }),
],
});
restHud.jsonString.enable = false;
rest.push(restHud);
rest.push(entity({
id: guid('rst', 1),
path: '/ui/DefaultGroup/RestHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 140 } }),
sprite({ color: TRANSPARENT }),
text({ value: '휴식', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
],
}));
rest.push(entity({
id: guid('rst', 2),
path: '/ui/DefaultGroup/RestHud/Info',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }),
sprite({ color: TRANSPARENT }),
text({ value: 'HP 회복', fontSize: 30, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
rest.push(entity({
id: guid('rst', 3),
path: '/ui/DefaultGroup/RestHud/Leave',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -120 } }),
sprite({ color: DARK, type: 1, raycast: true }),
button(),
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
],
}));
emit('RestHud', rest);
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 }),
],
}));
menu.push(entity({
id: guid('menu', 4),
path: '/ui/DefaultGroup/MainMenu/ContinueButton',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 3,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 58 }, pos: { x: 0, y: -100 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.1, g: 0.11, b: 0.13, a: 0.78 }, type: 1, raycast: false }),
button({ enabled: false }),
text({ value: '이어하기', fontSize: 24, bold: true, color: { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 0 }),
],
}));
const select = [];
select.push(entity({
id: guid('menu', 100),
path: '/ui/DefaultGroup/CharacterSelectHud',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 21,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }),
],
}));
select.push(entity({
id: guid('menu', 190),
path: '/ui/DefaultGroup/CharacterSelectHud/OpaqueBackdrop',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
],
}));
select.push(entity({
id: guid('menu', 101),
path: '/ui/DefaultGroup/CharacterSelectHud/Title',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 72 }, pos: { x: 0, y: 355 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT }),
text({ value: '\uCE90\uB9AD\uD130 \uC120\uD0DD', fontSize: 42, bold: true, color: GOLD, alignment: 0 }),
],
}));
select.push(entity({
id: guid('menu', 102),
path: '/ui/DefaultGroup/CharacterSelectHud/Status',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 680, y: 44 }, pos: { x: 0, y: 298 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT }),
text({ value: '\uC804\uC0AC\uB97C \uC120\uD0DD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694', fontSize: 22, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }),
],
}));
const classCards = [
{ key: 'Warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } },
{ key: 'Thief', label: '\uB3C4\uC801', desc: '\uCD94\uD6C4 \uC5F4\uB9BC', x: 0, enabled: false, tint: { r: 0.18, g: 0.19, b: 0.21, a: 1 } },
{ key: 'Mage', label: '\uB9C8\uBC95\uC0AC', desc: '\uCD94\uD6C4 \uC5F4\uB9BC', x: 360, enabled: false, tint: { r: 0.18, g: 0.19, b: 0.21, a: 1 } },
];
for (let i = 0; i < classCards.length; i++) {
const cls = classCards[i];
const base = `/ui/DefaultGroup/CharacterSelectHud/${cls.key}Button`;
select.push(entity({
id: guid('menu', 110 + i),
path: base,
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: 10 + i,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 270, y: 330 }, pos: { x: cls.x, y: 40 }, align: ALIGN_CENTER }),
sprite({ color: cls.enabled ? { r: 0.16, g: 0.2, b: 0.26, a: 1 } : { r: 0.11, g: 0.12, b: 0.14, a: 1 }, type: 1, raycast: cls.enabled }),
button({ enabled: cls.enabled }),
],
}));
select.push(entity({
id: guid('menu', 120 + i),
path: `${base}/Name`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 54 }, pos: { x: 0, y: 108 } }),
sprite({ color: TRANSPARENT }),
text({ value: cls.label, fontSize: 34, bold: true, color: cls.enabled ? GOLD : { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 4 }),
],
}));
select.push(entity({
id: guid('menu', 130 + i),
path: `${base}/Portrait`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 1,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 142, y: 142 }, pos: { x: 0, y: 8 } }),
sprite({ color: cls.tint, type: 1 }),
],
}));
select.push(entity({
id: guid('menu', 140 + i),
path: `${base}/Desc`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 2,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 50 }, pos: { x: 0, y: -105 } }),
sprite({ color: TRANSPARENT }),
text({ value: cls.desc, fontSize: 20, color: cls.enabled ? { r: 0.86, g: 0.9, b: 0.94, a: 1 } : { r: 0.52, g: 0.55, b: 0.59, a: 1 }, alignment: 4 }),
],
}));
if (!cls.enabled) {
select.push(entity({
id: guid('menu', 150 + i),
path: `${base}/LockBody`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 3,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 76, y: 58 }, pos: { x: 0, y: 4 } }),
sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }),
],
}));
select.push(entity({
id: guid('menu', 160 + i),
path: `${base}/LockShackle`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 4,
components: [
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 54, y: 42 }, pos: { x: 0, y: 48 } }),
sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }),
],
}));
}
}
select.push(entity({
id: guid('menu', 180),
path: '/ui/DefaultGroup/CharacterSelectHud/StartButton',
modelId: 'uibutton',
entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 20,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 68 }, pos: { x: 720, y: -360 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }),
button(),
text({ value: '\uC2DC\uC791', fontSize: 30, bold: true, color: GOLD, alignment: 0 }),
],
}));
select[0].jsonString.enable = false;
emit('MainMenu', menu);
emit('CharacterSelectHud', select);
for (const section of UI_APPEND_ORDER) {
const entities = uiSections.get(section);
if (entities == null) {
throw new Error(`[gen-slaydeck] missing generated UI section: ${section}`);
}
appendUiSection(ui, section, entities);
}
JSON.parse(JSON.stringify(ui));
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
}
function prop(Type, Name, DefaultValue = 'nil') {
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };
}
function method(Name, Code, Arguments = [], ExecSpace = 0, ReturnType = 'void') {
return {
Return: { Type: ReturnType, DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null },
Arguments,
Code,
Scope: 2,
ExecSpace,
Attributes: [],
Name,
};
}
function codeblock(id, name, properties, methods) {
return {
Id: '',
GameId: '',
EntryKey: `codeblock://${id}`,
ContentType: 'x-mod/codeblock',
Content: '',
Usage: 0,
UsePublish: 1,
UseService: 0,
CoreVersion: '26.5.0.0',
StudioVersion: '',
DynamicLoading: 0,
ContentProto: {
Use: 'Json',
Json: {
CoreVersion: { Major: 0, Minor: 2 },
ScriptVersion: { Major: 1, Minor: 0 },
Description: '',
Id: id,
Language: 1,
Name: name,
Type: 1,
Source: 0,
Target: null,
Properties: properties,
Methods: methods,
EntityEventHandlers: [],
},
},
};
}
function writeCodeblocks() {
const RUN_LENGTH = 3;
const GOLD_PER_WIN = 25;
const CARD_PRICE = 30;
const REST_HEAL = 30;
const RELIC_PRICE = 60;
const ACT_COUNT = 3;
const ACT_MAPS = ['map01', 'map02', 'map03'];
const combat = codeblock('SlayDeckController', 'SlayDeckController', [
prop('any', 'DrawPile'),
prop('any', 'DiscardPile'),
prop('any', 'Hand'),
prop('number', 'Energy', '0'),
prop('number', 'MaxEnergy', '3'),
prop('number', 'Turn', '0'),
prop('number', 'TweenEventId', '0'),
prop('any', 'EndTurnHandler'),
prop('any', 'NewGameHandler'),
prop('any', 'WarriorSelectHandler'),
prop('any', 'StartGameHandler'),
prop('string', 'SelectedClass', '""'),
prop('any', 'DrawPileHandler'),
prop('any', 'DiscardPileHandler'),
prop('any', 'DeckInspectCloseHandler'),
prop('any', 'AllDeckHandler'),
prop('any', 'AllDeckCloseHandler'),
prop('string', 'DeckInspectKind', '""'),
prop('boolean', 'DeckAllOpen', 'false'),
prop('any', 'Cards'),
prop('number', 'PlayerHp', '0'),
prop('number', 'PlayerMaxHp', '80'),
prop('number', 'PlayerBlock', '0'),
prop('boolean', 'CombatOver', 'false'),
prop('any', 'Monsters'),
prop('any', 'Registered'),
prop('number', 'TargetIndex', '1'),
prop('any', 'RunDeck'),
prop('number', 'Gold', '0'),
prop('number', 'Floor', '0'),
prop('number', 'RunLength', String(RUN_LENGTH)),
prop('any', 'RewardChoices'),
prop('boolean', 'RunActive', 'false'),
prop('any', 'Enemies'),
prop('any', 'MapNodes'),
prop('any', 'MapStart'),
prop('string', 'CurrentNodeId', '""'),
prop('string', 'CurrentEnemyId', '""'),
prop('any', 'ShopChoices'),
prop('any', 'ShopBought'),
prop('any', 'Relics'),
prop('any', 'RunRelics'),
prop('any', 'RelicPool'),
prop('string', 'ShopRelic', '""'),
prop('boolean', 'ShopRelicBought', 'false'),
prop('number', 'DragSlot', '0'),
prop('boolean', 'FxBusy', 'false'),
prop('boolean', 'TurnBusy', 'false'),
prop('number', 'PlayerStr', '0'),
prop('number', 'PlayerWeak', '0'),
prop('number', 'PlayerVuln', '0'),
prop('any', 'PlayerPowers'),
prop('any', 'Potions'),
prop('any', 'RunPotions'),
prop('number', 'PotionSlots', String(POTIONS.baseSlots)),
prop('string', 'ShopPotion', '""'),
prop('boolean', 'ShopPotionBought', 'false'),
prop('number', 'FightAttackCount', '0'),
prop('boolean', 'FirstHpLossDone', 'false'),
prop('number', 'ClayBlockNext', '0'),
prop('number', 'PotionMenuSlot', '0'),
], [
method('OnBeginPlay', `self:ShowMainMenu()`),
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/DeckInspectHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)`),
method('ShowState', `self:HideGameHud()
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect")
if state == "map" then
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true)
elseif state == "combat" then
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true)
elseif state == "shop" then
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true)
elseif state == "rest" then
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true)
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
method('ShowMainMenu', `self.SelectedClass = ""
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 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`),
method('ShowCharacterSelect', `self.SelectedClass = ""
self:ShowState("charselect")
self:RenderCharacterSelect()`),
method('SelectClass', `self.SelectedClass = className
self:RenderCharacterSelect()`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' },
]),
method('RenderCharacterSelect', `local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then
if self.SelectedClass == "warrior" then
warrior.SpriteGUIRendererComponent.Color = Color(0.28, 0.36, 0.46, 1)
else
warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
end
end
if self.SelectedClass == "warrior" then
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨")
else
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사를 선택하고 시작하세요")
end`),
method('StartNewGame', `if self.SelectedClass ~= "warrior" 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', `self.PlayerMaxHp = 80
self.PlayerHp = self.PlayerMaxHp
self.Gold = 0
self.Floor = 1
self.RunLength = ${ACT_COUNT}
self.RunDeck = { ${CARDS.starterDeck.map(luaStr).join(', ')} }
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)}
${luaMapNodesTable(MAP.nodes)}
${luaStartArray(MAP.start)}
self.CurrentNodeId = ""
self.CurrentEnemyId = ""
self:BindButtons()
self:AddRelic("${RELICS.startingRelic}")
self:RenderPotions()
self:ShowMap()`),
method('StartCombat', `self:ShowState("combat")
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)
self.MaxEnergy = 3
self.Turn = 0
self.PlayerBlock = 0
self.PlayerStr = 0
self.PlayerWeak = 0
self.PlayerVuln = 0
self.PlayerPowers = {}
self.FightAttackCount = 0
self.FirstHpLossDone = false
self.ClayBlockNext = 0
self.CombatOver = false
self.DiscardPile = {}
self.Hand = {}
${luaCardsTable(CARDS.cards)}
self.DrawPile = {}
for i = 1, #self.RunDeck do
self.DrawPile[i] = self.RunDeck[i]
end
self:Shuffle(self.DrawPile)
self:BuildMonsters()
self:RenderCombat()
self:StartPlayerTurn()
self:ApplyRelics("combatStart")
self:RenderCombat()`),
method('RegisterMonster', `if self.Registered == nil then
self.Registered = {}
end
local g = group
if g == nil or g == "" then g = "combat" end
local mp = mapName
if mp == nil then mp = "" end
table.insert(self.Registered, { entity = monster, enemyId = enemyId, group = g, map = mp })`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'group' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'mapName' },
]),
method('BuildMonsters', `self.Monsters = {}
local g = "combat"
local node = self.MapNodes[self.CurrentNodeId]
if node ~= nil and node.type ~= nil then g = node.type end
local pmap = ""
local lp = _UserService.LocalPlayer
if lp ~= nil and lp.CurrentMapName ~= nil then pmap = lp.CurrentMapName end
local reg = self.Registered or {}
for i = 1, #reg do
if reg[i].entity ~= nil and isvalid(reg[i].entity) then
reg[i].entity:SetVisible(false)
end
end
local list = {}
for i = 1, #reg do
local r = reg[i]
if r.entity ~= nil and isvalid(r.entity) and r.group == g and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then
local x = 0
if r.entity.TransformComponent ~= nil then
x = r.entity.TransformComponent.WorldPosition.x
end
table.insert(list, { entity = r.entity, enemyId = r.enemyId, x = x })
end
end
table.sort(list, function(a, b) return a.x < b.x end)
local mult = 1 + (self.Floor - 1) * 0.6
local n = #list
if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end
for i = 1, n do
local item = list[i]
local e = self.Enemies[item.enemyId]
if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end
local intents = {}
for k = 1, #e.intents do
local v = e.intents[k].value
if e.intents[k].kind ~= "Debuff" then
v = math.floor(v * mult)
end
intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect }
end
local maxHp = math.floor(e.maxHp * mult)
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0,
intents = intents, intentIdx = 1, alive = true, slot = i }
self:ReviveMonsterEntity(item.entity)
self:PositionMonsterSlot(i)
end
self.TargetIndex = 1`),
method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then
return
end
monster:SetEnable(true)
monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]),
method('Shuffle', `if list == nil then
\treturn
end
for i = #list, 2, -1 do
\tlocal j = math.random(1, i)
\tlist[i], list[j] = list[j], list[i]
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]),
method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton")
if endTurn ~= nil and endTurn.ButtonComponent ~= nil then
if self.EndTurnHandler ~= nil then
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
self.EndTurnHandler = nil
end
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
end
local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile")
if drawPile ~= nil and drawPile.ButtonComponent ~= nil then
if self.DrawPileHandler ~= nil then
drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)
self.DrawPileHandler = nil
end
self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end)
end
local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile")
if discardPile ~= nil and discardPile.ButtonComponent ~= nil then
if self.DiscardPileHandler ~= nil then
discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)
self.DiscardPileHandler = nil
end
self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end)
end
local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close")
if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then
if self.DeckInspectCloseHandler ~= nil then
inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)
self.DeckInspectCloseHandler = nil
end
self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)
end
local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton")
if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then
if self.AllDeckHandler ~= nil then
allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)
self.AllDeckHandler = nil
end
self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)
end
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
if self.AllDeckCloseHandler ~= nil then
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
self.AllDeckCloseHandler = nil
end
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
end
for i = 1, 5 do
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
cardEntity:ConnectEvent(UITouchDragEvent, function(ev) self:OnCardDrag(i, ev.TouchPoint) end)
cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(i, ev.TouchPoint) end)
end
end
for i = 1, 3 do
local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i))
if rc ~= nil and rc.ButtonComponent ~= nil then
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
end
end
local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip")
if skip ~= nil and skip.ButtonComponent ~= nil then
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
end
local mapNodeIds = { ${Object.keys(MAP.nodes).map(luaStr).join(', ')} }
for i = 1, #mapNodeIds do
local nid = mapNodeIds[i]
local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid)
if mn ~= nil and mn.ButtonComponent ~= nil then
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
end
end
for i = 1, 3 do
local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i))
if sc ~= nil and sc.ButtonComponent ~= nil then
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
end
end
local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave")
if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then
shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
end
local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)
end
local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave")
if restLeave ~= nil and restLeave.ButtonComponent ~= nil then
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
end
for i = 1, ${MAX_MONSTERS} do
local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i))
if ms ~= nil and ms.ButtonComponent ~= nil then
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
end
end
for i = 1, 10 do
local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
local idx = i
rs:ConnectEvent(UITouchEnterEvent, function()
local rid = nil
if self.RunRelics ~= nil then rid = self.RunRelics[idx] end
if rid ~= nil and self.Relics[rid] ~= nil then
self:ShowTooltip(self.Relics[rid].name, self.Relics[rid].desc, -240 + (idx - 1) * 48)
end
end)
rs:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
end
end
for i = 1, 5 do
local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
local idx = i
ps:ConnectEvent(UITouchEnterEvent, function()
local pid = nil
if self.RunPotions ~= nil then pid = self.RunPotions[idx] end
if pid ~= nil and self.Potions[pid] ~= nil then
self:ShowTooltip(self.Potions[pid].name, self.Potions[pid].desc, 240 + (idx - 1) * 44)
end
end)
ps:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end)
end
end
local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use")
if pmUse ~= nil and pmUse.ButtonComponent ~= nil then
pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end)
end
local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss")
if pmToss ~= nil and pmToss.ButtonComponent ~= nil then
pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end)
end
local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close")
if pmClose ~= nil and pmClose.ButtonComponent ~= nil then
pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end)
end
local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
end`),
method('StartPlayerTurn', `self.Turn = self.Turn + 1
self.Energy = self.MaxEnergy
self:ApplyRelics("turnStart")
if self.PlayerPowers ~= nil then
for i = 1, #self.PlayerPowers do
local pc = self.Cards[self.PlayerPowers[i]]
if pc ~= nil and pc.powerEffect == "strengthPerTurn" then
self.PlayerStr = self.PlayerStr + pc.value
end
end
end
self.PlayerBlock = 0
if self.ClayBlockNext > 0 then
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
self.ClayBlockNext = 0
end
self:DrawCards(5)
self:RenderHand(true)
self:RenderCombat()`),
method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
for i = 1, #self.Hand do
\ttable.insert(self.DiscardPile, self.Hand[i])
end
self.Hand = {}
if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end
if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
self:RenderHand(false)
self:RenderPiles()
self:EnemyTurn()`),
method('DrawCards', `for i = 1, amount do
\tif #self.DrawPile <= 0 then
\t\tself:RecycleDiscardIntoDraw()
\tend
\tif #self.DrawPile <= 0 then
\t\tbreak
\tend
\tlocal cardId = table.remove(self.DrawPile)
\ttable.insert(self.Hand, cardId)
end
self:RenderPiles()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then
\treturn
end
self.DrawPile = {}
for i = 1, #self.DiscardPile do
\tself.DrawPile[i] = self.DiscardPile[i]
end
self.DiscardPile = {}
self:Shuffle(self.DrawPile)`),
method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", tostring(#self.DrawPile))
self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", tostring(#self.DiscardPile))
self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then
self:OpenDeckInspect(self.DeckInspectKind)
end`),
method('OpenDeckInspect', `self.DeckInspectKind = kind
if self.DeckAllOpen == true then
self.DeckAllOpen = false
local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if allHud ~= nil then
allHud.Enable = false
end
end
local pile = {}
local title = ""
if kind == "discard" then
pile = self.DiscardPile or {}
title = "버린 덱"
else
pile = self.DrawPile or {}
title = "뽑을 덱"
end
self:RenderDeckInspect(pile, title)
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if hud ~= nil then
hud.Enable = true
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
method('CloseDeckInspect', `self.DeckInspectKind = ""
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if hud ~= nil then
hud.Enable = false
end`),
method('RenderDeckInspect', `local count = 0
if pile ~= nil then
count = #pile
end
local suffix = " (" .. tostring(count) .. ")"
if count > 60 then
suffix = suffix .. " - 60장까지 표시"
end
self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix)
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty")
if empty ~= nil then
empty.Enable = count <= 0
end
for i = 1, 60 do
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i))
if e ~= nil then
local cardId = nil
if pile ~= nil then
cardId = pile[i]
end
if cardId == nil then
e.Enable = false
else
e.Enable = true
self:ApplyInspectCardVisual(i, cardId)
end
end
end`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
]),
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
if inspectHud ~= nil then
inspectHud.Enable = false
end
self.DeckInspectKind = ""
self.DeckAllOpen = true
self:RenderAllDeck()
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if hud ~= nil then
hud.Enable = true
end`),
method('CloseAllDeck', `self.DeckAllOpen = false
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if hud ~= nil then
hud.Enable = false
end`),
method('RenderAllDeck', `local pile = self.RunDeck or {}
local count = #pile
self:SetText("/ui/DefaultGroup/DeckAllHud/Title", "모든 덱 (" .. tostring(count) .. ")")
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Empty")
if empty ~= nil then
empty.Enable = count <= 0
end
for i = 1, 120 do
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(i))
if e ~= nil then
local cardId = pile[i]
if cardId == nil then
e.Enable = false
else
e.Enable = true
self:ApplyAllDeckCardVisual(i, cardId)
end
end
end`),
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('RenderHand', `local drawStart = Vector2(-590, 8)
for i = 1, 5 do
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
\tif cardEntity ~= nil then
\t\tlocal cardId = self.Hand[i]
\t\tif cardId == nil then
\t\t\tcardEntity.Enable = false
\t\telse
\t\t\tcardEntity.Enable = true
\t\t\tself:ApplyCardVisual(i, cardId)
\t\t\tif animate == true then
\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2((i - 3) * 200, 0), 0.16 + i * 0.045)
\t\t\tend
\t\tend
\tend
end
self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]),
method('ApplyCardFace', `local c = self.Cards[cardId]
if c == nil then
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
end
local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
if c.kind == "Attack" then
e.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
elseif c.kind == "Power" then
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
else
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
end
end
self:SetText(base .. "/Cost", string.format("%d", c.cost))
self:SetText(base .. "/Name", c.name)
self:SetText(base .. "/Desc", c.desc)
local art = _EntityService:GetEntityByPath(base .. "/Art")
if art ~= nil then
if c.image ~= nil and c.image ~= "" then
art.Enable = true
if art.SpriteGUIRendererComponent ~= nil then
art.SpriteGUIRendererComponent.ImageRUID = c.image
end
else
art.Enable = false
end
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('SetText', `local entity = _EntityService:GetEntityByPath(path)
if entity ~= nil and entity.TextComponent ~= nil then
\tentity.TextComponent.Text = value
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' },
]),
method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if cardEntity == nil or cardEntity.UITransformComponent == nil then
\treturn
end
local tr = cardEntity.UITransformComponent
tr.anchoredPosition = fromPos
local elapsed = 0
local eventId = 0
eventId = _TimerService:SetTimerRepeat(function()
\telapsed = elapsed + 1 / 60
\tlocal t = math.min(elapsed / duration, 1)
\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)
\ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased)
\tif t >= 1 then
\t\t_TimerService:ClearTimer(eventId)
\tend
end, 1 / 60)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
]),
method('CalcPlayerAttack', `local base2 = base
self.FightAttackCount = self.FightAttackCount + 1
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
base2 = base2 + 8
end
local dmg = base2 + self.PlayerStr
if self:HasRelic("penNib") and self.FightAttackCount % 10 == 0 then
dmg = dmg * 2
end
if self.PlayerWeak > 0 then
dmg = math.floor(dmg * 0.75)
end
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
dmg = 5
end
if dmg < 0 then
dmg = 0
end
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
method('PlayCard', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
if self.Hand == nil then
return
end
local cardId = self.Hand[slot]
if cardId == nil then
return
end
local c = self.Cards[cardId]
if c == nil then
return
end
if self.Energy < c.cost then
self:Toast("에너지가 부족합니다")
return
end
self.Energy = self.Energy - c.cost
if c.kind == "Attack" then
if c.damage ~= nil then
self:PlayAttackFx(self.TargetIndex, c.image, self:CalcPlayerAttack(c.damage))
end
if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block
end
self:ApplyRelics("cardPlayed")
elseif c.kind == "Skill" then
if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block
end
elseif c.kind == "Power" then
if c.powerEffect ~= nil then
table.insert(self.PlayerPowers, cardId)
end
end
if c.strength ~= nil then
self.PlayerStr = self.PlayerStr + c.strength
end
if c.weak ~= nil or c.vuln ~= nil then
local tm = self.Monsters[self.TargetIndex]
if tm ~= nil and tm.alive == true then
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
if c.vuln ~= nil then
tm.vuln = tm.vuln + c.vuln
if self:HasRelic("championBelt") then
tm.weak = tm.weak + 1
end
end
end
end
table.remove(self.Hand, slot)
if c.kind ~= "Power" then
table.insert(self.DiscardPile, cardId)
end
self:RenderHand(false)
self:RenderPiles()
self:RenderCombat()
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
if self.Hand == nil or self.Hand[slot] == nil then
return
end
self.DragSlot = slot`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('OnCardDrag', `if self.DragSlot ~= slot then
return
end
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
local ui = _UILogic:ScreenToUIPosition(touchPoint)
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('OnCardDragEnd', `if self.DragSlot ~= slot then
return
end
self.DragSlot = 0
local cardXs = { ${CARD_XS.join(', ')} }
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(cardXs[slot], 0)
end
self:ResolveCardDrop(slot, touchPoint)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('ResolveCardDrop', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
local cardId = self.Hand[slot]
if cardId == nil then
return
end
local c = self.Cards[cardId]
if c == nil then
return
end
if c.kind == "Attack" then
local best = 0
local bestDist = 200
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
local wp = m.entity.TransformComponent.WorldPosition
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
local dx = sp.x - touchPoint.x
local dy = sp.y - touchPoint.y
local d = math.sqrt(dx * dx + dy * dy)
if d < bestDist then
bestDist = d
best = i
end
end
end
if best > 0 then
self.TargetIndex = best
self:PlayCard(slot)
end
else
local ui = _UILogic:ScreenToUIPosition(touchPoint)
if ui.y > -180 then
self:PlayCard(slot)
end
end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
if m == nil or m.alive ~= true then
m = nil
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end
end
end
if m == nil then
return
end
local dmg = amount
if m.vuln > 0 then
dmg = math.floor(dmg * 1.5)
end
if m.block > 0 then
local absorbed = math.min(m.block, dmg)
m.block = m.block - absorbed
dmg = dmg - absorbed
end
m.hp = m.hp - dmg
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
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)
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)
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' },
]),
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
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()
m.block = 0
local intent = m.intents[m.intentIdx]
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
local before = self.PlayerHp
self:DealDamageToPlayer(atk, idx)
self:ShowPlayerDmgPop(before - self.PlayerHp)
elseif intent.kind == "Defend" then
m.block = m.block + intent.value
elseif intent.kind == "Debuff" then
if intent.effect == "weak" then
self.PlayerWeak = self.PlayerWeak + intent.value
elseif intent.effect == "vuln" then
self.PlayerVuln = self.PlayerVuln + intent.value
end
end
end
m.intentIdx = m.intentIdx + 1
if m.intentIdx > #m.intents then
m.intentIdx = 1
end
if m.weak > 0 then m.weak = m.weak - 1 end
if m.vuln > 0 then m.vuln = m.vuln - 1 end
self:RenderCombat()
self:SetEntityEnabled(base .. "/ActFrame", false)
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]),
method('FinishEnemyTurn', `self.TurnBusy = false
self:CheckCombatEnd()
if self.CombatOver == true then
return
end
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`),
method('CheckCombatEnd', `local anyAlive = false
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then anyAlive = true; break end
end
if anyAlive == false then
self.CombatOver = true
self.Gold = self.Gold + ${GOLD_PER_WIN}
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
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
end
if node ~= nil and node.type == "boss" then
if self.Floor < self.RunLength then
self.Floor = self.Floor + 1
self.CurrentNodeId = ""
self.CurrentEnemyId = ""
self:RenderRun()
self:TeleportToActMap()
self:ShowMap()
else
self:EndRun("런 클리어!")
end
else
self:OfferReward()
end
elseif self.PlayerHp <= 0 then
self.CombatOver = true
self:EndRun("패배...")
end`),
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', `self:ShowResult(text)
self.RunActive = false
_TimerService:SetTimerOnce(function() self:ShowMainMenu() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
method('BuffsLabel', `local parts = {}
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
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' },
], 0, 'string'),
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)
local m = self.Monsters[i]
if m ~= nil and m.alive == true then
self:SetEntityEnabled(base, true)
self:SetText(base .. "/Name", m.name)
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
local intent = m.intents[m.intentIdx]
local t = ""
if intent ~= nil then
if intent.kind == "Attack" then
local atk = intent.value + m.str
if m.weak > 0 then atk = math.floor(atk * 0.75) end
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
t = "공격 " .. tostring(atk)
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
elseif intent.kind == "Debuff" then
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
else t = "취약 " .. tostring(intent.value) .. " 부여" end
end
end
self:SetText(base .. "/Intent", t)
self:SetEntityEnabled(base .. "/TargetFrame", i == self.TargetIndex)
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
if intent.kind == "Attack" then
intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1)
elseif intent.kind == "Debuff" then
intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1)
else
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
end
end
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln))
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)
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
local names = {}
for i = 1, #self.PlayerPowers do
local pc = self.Cards[self.PlayerPowers[i]]
if pc ~= nil then table.insert(names, pc.name) end
end
if pb ~= "" then pb = pb .. " · " end
pb = pb .. table.concat(names, " ")
end
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb)
self:RenderRun()`),
method('ShowDmgPop', `local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot) .. "/DmgPop"
self:SetText(base, "-" .. string.format("%d", amount))
self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
]),
method('ShowPlayerDmgPop', `local base = "/ui/DefaultGroup/CombatHud/PlayerPanel/DmgPop"
if amount > 0 then
self:SetText(base, "-" .. string.format("%d", amount))
else
self:SetText(base, "막음")
end
self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('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', `self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Floor", "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength))
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "골드 " .. string.format("%d", self.Gold))`),
method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
local pool = {}
for id, _ in pairs(self.Cards) do
table.insert(pool, id)
end
self.RewardChoices = {}
for i = 1, 3 do
self.RewardChoices[i] = pool[math.random(1, #pool)]
self:ApplyRewardVisual(i, self.RewardChoices[i])
end
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
if hud ~= nil then
hud.Enable = true
end`),
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
return
end
if slot ~= 0 and self.RewardChoices ~= nil then
local id = self.RewardChoices[slot]
if id ~= nil then
table.insert(self.RunDeck, id)
end
end
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
if hud ~= nil then
hud.Enable = false
end
self:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('HasRelic', `if self.RunRelics == nil then
return false
end
for i = 1, #self.RunRelics do
if self.RunRelics[i] == id then
return true
end
end
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
method('ApplyRelics', `if self.RunRelics == nil then
return
end
for i = 1, #self.RunRelics do
local r = self.Relics[self.RunRelics[i]]
if r ~= nil and r.hook == hook then
if r.effect == "block" then
self.PlayerBlock = self.PlayerBlock + r.value
elseif r.effect == "energy" then
self.Energy = self.Energy + r.value
elseif r.effect == "strength" then
self.PlayerStr = self.PlayerStr + r.value
elseif r.effect == "draw" then
self:DrawCards(r.value)
self:RenderHand(false)
elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then
self.PlayerHp = self.PlayerHp + r.value
if self.PlayerHp > self.PlayerMaxHp then
self.PlayerHp = self.PlayerMaxHp
end
elseif r.effect == "healIfLow" then
if self.PlayerHp * 2 <= self.PlayerMaxHp then
self.PlayerHp = self.PlayerHp + r.value
if self.PlayerHp > self.PlayerMaxHp then
self.PlayerHp = self.PlayerMaxHp
end
end
elseif r.effect == "gold" then
self.Gold = self.Gold + r.value
end
end
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hook' }]),
method('AddRelic', `if self.RunRelics == nil then
self.RunRelics = {}
end
table.insert(self.RunRelics, id)
local r = self.Relics[id]
if r ~= nil and r.hook == "passive" then
if r.effect == "potionSlots" then
self.PotionSlots = r.value
self:RenderPotions()
elseif r.effect == "maxHp" then
self.PlayerMaxHp = self.PlayerMaxHp + r.value
self.PlayerHp = self.PlayerHp + r.value
end
end
self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
method('PickNewRelic', `local pool = {}
for i = 1, #self.RelicPool do
if self:HasRelic(self.RelicPool[i]) == false then
table.insert(pool, self.RelicPool[i])
end
end
if #pool == 0 then
self.Gold = self.Gold + 25
self:Toast("유물을 모두 모았습니다! 골드 +25")
return ""
end
return pool[math.random(1, #pool)]`, [], 0, 'string'),
method('AddPotion', `if self.RunPotions == nil then
self.RunPotions = {}
end
if #self.RunPotions >= self.PotionSlots then
self:Toast("물약 슬롯이 가득 찼습니다")
return false
end
table.insert(self.RunPotions, pid)
self:RenderPotions()
return true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pid' }], 0, 'boolean'),
method('MaybeDropPotion', `if math.random() > ${POTIONS.dropChance} then
return
end
local keys = {}
for pid, _ in pairs(self.Potions) do
table.insert(keys, pid)
end
table.sort(keys)
local pid = keys[math.random(1, #keys)]
if self:AddPotion(pid) == true then
local p = self.Potions[pid]
self:Toast("물약 획득: " .. p.name)
end`),
method('RenderPotions', `for i = 1, 5 do
local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local pid = nil
if self.RunPotions ~= nil then
pid = self.RunPotions[i]
end
if pid ~= nil and self.Potions[pid] ~= nil then
e.SpriteGUIRendererComponent.ImageRUID = self.Potions[pid].icon
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
elseif i > self.PotionSlots then
e.SpriteGUIRendererComponent.ImageRUID = ""
e.SpriteGUIRendererComponent.Color = Color(0.1, 0.1, 0.12, 0.85)
else
e.SpriteGUIRendererComponent.ImageRUID = ""
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.25, 0.3, 0.9)
end
end
end`),
method('OpenPotionMenu', `if self.RunPotions == nil or self.RunPotions[slot] == nil then
return
end
self.PotionMenuSlot = slot
local pid = self.RunPotions[slot]
local p = self.Potions[pid]
if p ~= nil then
self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc)
end
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('ClosePotionMenu', `self.PotionMenuSlot = 0
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`),
method('UsePotion', `if self.PotionMenuSlot <= 0 then
return
end
if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then
self:Toast("지금은 사용할 수 없습니다")
return
end
local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud")
local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand")
if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then
self:Toast("전투 중에만 사용할 수 있습니다")
return
end
local pid = self.RunPotions[self.PotionMenuSlot]
if pid == nil then
return
end
local p = self.Potions[pid]
if p == nil then
return
end
if p.effect == "heal" then
self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp)
elseif p.effect == "damage" then
self:DealDamageToTarget(p.value)
self:ShowDmgPop(self.TargetIndex, p.value)
elseif p.effect == "strength" then
self.PlayerStr = self.PlayerStr + p.value
elseif p.effect == "block" then
self.PlayerBlock = self.PlayerBlock + p.value
elseif p.effect == "energy" then
self.Energy = self.Energy + p.value
elseif p.effect == "weak" then
local tm = self.Monsters[self.TargetIndex]
if tm ~= nil and tm.alive == true then
tm.weak = tm.weak + p.value
end
end
table.remove(self.RunPotions, self.PotionMenuSlot)
self:Toast("물약 사용: " .. p.name)
self:ClosePotionMenu()
self:RenderPotions()
self:RenderPiles()
self:RenderCombat()
self:CheckCombatEnd()`),
method('TossPotion', `if self.PotionMenuSlot <= 0 then
return
end
local pid = self.RunPotions[self.PotionMenuSlot]
if pid ~= nil then
local p = self.Potions[pid]
table.remove(self.RunPotions, self.PotionMenuSlot)
if p ~= nil then
self:Toast("물약 버림: " .. p.name)
end
end
self:ClosePotionMenu()
self:RenderPotions()`),
method('RenderRelics', `local count = 0
if self.RunRelics ~= nil then
count = #self.RunRelics
end
for i = 1, 10 do
local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local rid = nil
if self.RunRelics ~= nil then
rid = self.RunRelics[i]
end
if rid ~= nil and self.Relics[rid] ~= nil and (i < 10 or count <= 10) then
e.SpriteGUIRendererComponent.ImageRUID = self.Relics[rid].icon
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
else
e.SpriteGUIRendererComponent.ImageRUID = ""
e.SpriteGUIRendererComponent.Color = Color(0.15, 0.16, 0.2, 0.6)
end
end
end
local of = ""
if count > 10 then
of = "+" .. tostring(count - 9)
end
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`),
method('ShowTooltip', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name)
self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc)
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox")
if e ~= nil then
if e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(x, 400)
end
e.Enable = true
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
]),
method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`),
method('ShowMap', `self:ShowState("map")
self:RenderMap()`),
method('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('RenderMap', `for id, node in pairs(self.MapNodes) do
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. id)
if e ~= nil then
local reachable = self:IsReachable(id)
if e.SpriteGUIRendererComponent ~= nil then
if reachable then
e.SpriteGUIRendererComponent.Color = Color(0.3, 0.55, 0.85, 1)
else
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
end
end
if e.ButtonComponent ~= nil then
e.ButtonComponent.Enable = reachable
end
end
end`),
method('PickNode', `if self.RunActive ~= true then
return
end
if self:IsReachable(id) ~= true then
return
end
self.CurrentNodeId = id
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
if hud ~= nil then
hud.Enable = false
end
local node = self.MapNodes[id]
if node.type == "shop" then
self:ShowShop()
elseif node.type == "rest" then
self:ShowRest()
else
self.CurrentEnemyId = node.enemy
self:StartCombat()
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
method('ShowShop', `local pool = {}
for cid, _ in pairs(self.Cards) do
table.insert(pool, cid)
end
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
self:ShowMap()`),
]);
for (const m of combat.ContentProto.Json.Methods) {
m.ExecSpace = 6;
}
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.');