5563 lines
227 KiB
JavaScript
5563 lines
227 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): 잘못된 데이터면 생성 중단
|
||
const CLASSES = {
|
||
warrior: { label: '전사', maxHp: 80 },
|
||
bandit: { label: '도적', maxHp: 70 },
|
||
magician: { label: '마법사', maxHp: 70 },
|
||
};
|
||
for (const cls of Object.keys(CLASSES)) {
|
||
if (!CARDS.starterDecks?.[cls]) throw new Error(`[gen-slaydeck] starterDecks.${cls} 없음`);
|
||
for (const id of CARDS.starterDecks[cls]) {
|
||
if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`);
|
||
}
|
||
}
|
||
// 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드)
|
||
const JOBS = {
|
||
warrior: [
|
||
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' },
|
||
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' },
|
||
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' },
|
||
],
|
||
magician: [
|
||
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' },
|
||
{ id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' },
|
||
{ id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' },
|
||
],
|
||
bandit: [
|
||
{ id: 'shiv', name: 'Shiv', desc: 'Many small attacks\nBlade Dance\nAccuracy · After Image', starter: 'BladeDance' },
|
||
{ id: 'poisoner', name: 'Poison', desc: 'Poison scaling\nDeadly Poison\nCatalyst · Noxious Fumes', starter: 'DeadlyPoison' },
|
||
{ id: 'trickster', name: 'Trickster', desc: 'Draw and tempo\nAcrobatics\nAdrenaline · Tools', starter: 'Acrobatics' },
|
||
],
|
||
};
|
||
for (const [cls, jobs] of Object.entries(JOBS)) {
|
||
for (const j of jobs) {
|
||
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
|
||
}
|
||
}
|
||
// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점
|
||
const SOUL_UNLOCKS = [
|
||
{ key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 },
|
||
{ key: 'hp', name: '단련된 육체', desc: '시작 최대 HP +15', cost: 4 },
|
||
{ key: 'trim', name: '덱 정제', desc: '시작 덱에서 기본 카드 1장 제거', cost: 5 },
|
||
{ key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 },
|
||
];
|
||
function luaSoulShopTable(unlocks) {
|
||
const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${u.cost} },`).join('\n');
|
||
return `self.SoulShopDef = {\n${items}\n}`;
|
||
}
|
||
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
|
||
throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`);
|
||
}
|
||
|
||
// 카드 프레임 (사용자 제작 이미지 — 로컬 임포트 .sprite RUID, 직업 3종 × 등급 3종)
|
||
const CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json', 'utf8'));
|
||
const RARITIES = ['normal', 'unique', 'legend'];
|
||
for (const [fid, fr] of Object.entries(CARDFRAMES.frames)) {
|
||
for (const r of RARITIES) {
|
||
if (!fr[r]) throw new Error(`[gen-slaydeck] cardframes.frames.${fid}.${r} RUID 없음`);
|
||
}
|
||
}
|
||
for (const [id, c] of Object.entries(CARDS.cards)) {
|
||
if (!RARITIES.includes(c.rarity)) throw new Error(`[gen-slaydeck] 카드 ${id} rarity 누락/오류: ${c.rarity}`);
|
||
const fc = CARDFRAMES.classToFrame[c.class];
|
||
if (!fc || !CARDFRAMES.frames[fc]) throw new Error(`[gen-slaydeck] 카드 ${id} class ${c.class} → 프레임 매핑 없음`);
|
||
}
|
||
function frameRuid(card) {
|
||
return CARDFRAMES.frames[CARDFRAMES.classToFrame[card.class]][card.rarity];
|
||
}
|
||
function luaFramesTable() {
|
||
const frames = Object.entries(CARDFRAMES.frames).map(([fid, fr]) =>
|
||
`\t${fid} = { normal = ${luaStr(fr.normal)}, unique = ${luaStr(fr.unique)}, legend = ${luaStr(fr.legend)} },`).join('\n');
|
||
const cls = Object.entries(CARDFRAMES.classToFrame).map(([c, f]) => `\t${c} = ${luaStr(f)},`).join('\n');
|
||
return `self.CardFrames = {\n${frames}\n}\nself.ClassToFrame = {\n${cls}\n}`;
|
||
}
|
||
|
||
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
|
||
const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7)
|
||
const MAP_COLS = 4;
|
||
|
||
// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별)
|
||
const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71';
|
||
const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759';
|
||
|
||
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
||
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
|
||
for (const id of RELICS.relicPool) {
|
||
if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`);
|
||
}
|
||
function luaRelicsTable(relics) {
|
||
const lines = Object.entries(relics).map(([id, r]) =>
|
||
`\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`);
|
||
return `self.Relics = {\n${lines.join('\n')}\n}`;
|
||
}
|
||
|
||
const POTIONS = JSON.parse(readFileSync('data/potions.json', 'utf8'));
|
||
for (const [pid, p] of Object.entries(POTIONS.potions)) {
|
||
if (!p.name || !p.effect || p.value == null) throw new Error(`[gen-slaydeck] potion 필드 누락: ${pid}`);
|
||
}
|
||
function luaPotionsTable(potions) {
|
||
const lines = Object.entries(potions).map(([id, p]) =>
|
||
`\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`);
|
||
return `self.Potions = {\n${lines.join('\n')}\n}`;
|
||
}
|
||
|
||
function luaIntentsArray(intents) {
|
||
return '{ ' + intents.map((it) => {
|
||
const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value != null ? it.value : 0}`];
|
||
if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`);
|
||
if (it.card != null) fields.push(`card = ${luaStr(it.card)}`);
|
||
if (it.count != null) fields.push(`count = ${it.count}`);
|
||
return `{ ${fields.join(', ')} }`;
|
||
}).join(', ') + ' }';
|
||
}
|
||
function luaEnemiesTable(enemies) {
|
||
const lines = Object.entries(enemies).map(([id, e]) =>
|
||
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
|
||
return `self.Enemies = {\n${lines.join('\n')}\n}`;
|
||
}
|
||
// Lua 직렬화 헬퍼
|
||
function luaStr(s) {
|
||
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
||
}
|
||
function luaJobsTable(jobs) {
|
||
const cls = Object.entries(jobs).map(([clsId, list]) => {
|
||
const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n');
|
||
return `\t${clsId} = {\n${items}\n\t},`;
|
||
}).join('\n');
|
||
return `self.Jobs = {\n${cls}\n}`;
|
||
}
|
||
function luaCardsTable(cards) {
|
||
const lines = Object.entries(cards).map(([id, c]) => {
|
||
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
|
||
if (c.damage != null) fields.push(`damage = ${c.damage}`);
|
||
if (c.block != null) fields.push(`block = ${c.block}`);
|
||
if (c.strength != null) fields.push(`strength = ${c.strength}`);
|
||
if (c.weak != null) fields.push(`weak = ${c.weak}`);
|
||
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
|
||
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
|
||
if (c.value != null) fields.push(`value = ${c.value}`);
|
||
if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`);
|
||
fields.push(`class = ${luaStr(c.class)}`);
|
||
fields.push(`rarity = ${luaStr(c.rarity)}`);
|
||
if (c.hits != null) fields.push(`hits = ${c.hits}`);
|
||
if (c.pierce === true) fields.push('pierce = true');
|
||
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
||
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
||
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
||
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
||
if (c.aoe === true) fields.push('aoe = true');
|
||
if (c.unplayable === true) fields.push('unplayable = true');
|
||
if (c.curse === true) fields.push('curse = true');
|
||
if (c.endTurnDamage != null) fields.push(`endTurnDamage = ${c.endTurnDamage}`);
|
||
if (c.fx != null) fields.push(`fx = ${luaStr(c.fx)}`);
|
||
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
|
||
return `\t${id} = { ${fields.join(', ')} },`;
|
||
});
|
||
return `self.Cards = {\n${lines.join('\n')}\n}`;
|
||
}
|
||
function luaDeckTable(deck) {
|
||
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
|
||
}
|
||
|
||
const UI_FILE = 'ui/DefaultGroup.ui';
|
||
const COMMON_FILE = 'Global/common.gamelogic';
|
||
const UI_ROOT = '/ui/DefaultGroup';
|
||
const GENERATED_UI_SECTIONS = [
|
||
'DeckHud',
|
||
'DeckInspectHud',
|
||
'DeckAllHud',
|
||
'CombatHud',
|
||
'RewardHud',
|
||
'MapHud',
|
||
'ShopHud',
|
||
'RestHud',
|
||
'TreasureHud',
|
||
'JobChoiceHud',
|
||
'JobSelectHud',
|
||
'MainMenu',
|
||
'CharacterSelectHud',
|
||
'LobbyHud',
|
||
'BoardHud',
|
||
'SoulShopHud',
|
||
];
|
||
const UI_APPEND_ORDER = [
|
||
'DeckHud',
|
||
'CombatHud',
|
||
'RewardHud',
|
||
'MapHud',
|
||
'ShopHud',
|
||
'RestHud',
|
||
'TreasureHud',
|
||
'JobChoiceHud',
|
||
'JobSelectHud',
|
||
'DeckInspectHud',
|
||
'DeckAllHud',
|
||
'MainMenu',
|
||
'CharacterSelectHud',
|
||
'LobbyHud',
|
||
'BoardHud',
|
||
'SoulShopHud',
|
||
];
|
||
const DISABLED_STOCK_CONTROLS = ['Button_Attack', 'Button_Jump', 'UIJoystick'];
|
||
|
||
const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
|
||
const DARK = { r: 0.08, g: 0.09, b: 0.11, a: 0.92 };
|
||
const GOLD = { r: 0.94, g: 0.74, b: 0.26, a: 1 };
|
||
const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1 };
|
||
const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1 };
|
||
const SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 };
|
||
|
||
const MAX_MONSTERS = 4;
|
||
const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산
|
||
|
||
const HP_BAR_W = 140;
|
||
const WHITE = { r: 1, g: 1, b: 1, a: 1 };
|
||
const INK = { r: 0.13, g: 0.11, b: 0.09, a: 1 }; // 밝은 배너·설명 박스 위 먹색 글자
|
||
// 카드 프레임(1054×1492 원본) 슬롯 레이아웃 — 픽셀 실측을 180×250 카드 좌표로 환산한 기준값을 폭 비례 스케일.
|
||
// 실측(워리어·메이지·밴딧 공통): 육각 중심 (120,127)→(-70,104) · 배너 본체 y55..165, x215..1015→중심 (+15,+107)
|
||
// · 설명 박스 y~1030..1480→중심 (0,-86) · 아트 영역 y260..1030→중심 (0,+17)
|
||
function cardFaceLayout(W) {
|
||
const s = W / 180;
|
||
const r = (v) => Math.round(v * s);
|
||
return {
|
||
texts: [
|
||
['Cost', { size: { x: r(40), y: r(40) }, pos: { x: r(-70), y: r(104) }, fontSize: r(24), bold: true, color: WHITE }],
|
||
['Name', { size: { x: r(132), y: r(24) }, pos: { x: r(15), y: r(107) }, fontSize: r(16), bold: true, color: INK }],
|
||
['Desc', { size: { x: r(150), y: r(62) }, pos: { x: 0, y: r(-86) }, fontSize: r(16), bold: false, color: INK }],
|
||
],
|
||
art: { size: { x: r(112), y: r(112) }, pos: { x: 0, y: r(17) } },
|
||
};
|
||
}
|
||
const CARD_W = 180;
|
||
const CARD_H = 250;
|
||
const CARD_SPACING = 200;
|
||
const CARD_XS = [-400, -200, 0, 200, 400];
|
||
|
||
const ALIGN_CENTER = 0;
|
||
const ALIGN_BOTTOM_CENTER = 6;
|
||
|
||
function guid(prefix, n) {
|
||
// 유효한 8-4-4-4-12 hex GUID 생성. prefix는 충돌 방지용 네임스페이스 바이트로 매핑.
|
||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : prefix === 'job' ? 0xe4
|
||
: prefix === 'ins2' ? 0xe5 : prefix === 'all2' ? 0xe6 : prefix === 'rwd2' ? 0xe7 : prefix === 'shp2' ? 0xe8 : prefix === 'lob' ? 0xe9 : prefix === 'brd' ? 0xea : prefix === 'soul' ? 0xeb : 0xfe;
|
||
const v = (ns * 0x100000 + n) >>> 0;
|
||
return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`;
|
||
}
|
||
|
||
function transform({ parentW, parentH, anchor, pivot, size, pos, align = 0 }) {
|
||
const offMin = { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y };
|
||
const offMax = { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y };
|
||
return {
|
||
'@type': 'MOD.Core.UITransformComponent',
|
||
ActivePlatform: 255,
|
||
AlignmentOption: align,
|
||
AnchorsMax: anchor,
|
||
AnchorsMin: anchor,
|
||
MobileOnly: false,
|
||
OffsetMax: offMax,
|
||
OffsetMin: offMin,
|
||
Pivot: pivot,
|
||
RectSize: size,
|
||
UIMode: 1,
|
||
UIScale: { x: 1, y: 1, z: 1 },
|
||
UIVersion: 2,
|
||
anchoredPosition: pos,
|
||
Position: { x: anchor.x * parentW - parentW / 2 + pos.x, y: anchor.y * parentH - parentH / 2 + pos.y, z: 0 },
|
||
QuaternionRotation: { x: 0, y: 0, z: 0, w: 1 },
|
||
Scale: { x: 1, y: 1, z: 1 },
|
||
Enable: true,
|
||
};
|
||
}
|
||
|
||
function sprite({ dataId = '', color = TRANSPARENT, type = 1, raycast = false }) {
|
||
return {
|
||
'@type': 'MOD.Core.SpriteGUIRendererComponent',
|
||
AnimClipPlayType: 0,
|
||
EndFrameIndex: 2147483647,
|
||
ImageRUID: { DataId: dataId },
|
||
LocalPosition: { x: 0, y: 0 },
|
||
LocalScale: { x: 1, y: 1 },
|
||
OverrideSorting: false,
|
||
PlayRate: 1,
|
||
PreserveSprite: 0,
|
||
StartFrameIndex: 0,
|
||
Color: color,
|
||
DropShadow: false,
|
||
DropShadowAngle: 30,
|
||
DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 },
|
||
DropShadowDistance: 32,
|
||
FillAmount: 1,
|
||
FillCenter: true,
|
||
FillClockWise: true,
|
||
FillMethod: 0,
|
||
FillOrigin: 0,
|
||
FlipX: false,
|
||
FlipY: false,
|
||
FrameColumn: 1,
|
||
FrameRate: 0,
|
||
FrameRow: 1,
|
||
Outline: false,
|
||
OutlineColor: { r: 0, g: 0, b: 0, a: 1 },
|
||
OutlineWidth: 3,
|
||
RaycastTarget: raycast,
|
||
Type: type,
|
||
Enable: true,
|
||
};
|
||
}
|
||
|
||
function button({ enabled = true } = {}) {
|
||
return {
|
||
'@type': 'MOD.Core.ButtonComponent',
|
||
Colors: {
|
||
NormalColor: { r: 1, g: 1, b: 1, a: 1 },
|
||
HighlightedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 },
|
||
PressedColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 1 },
|
||
SelectedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 },
|
||
DisabledColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 0.5019608 },
|
||
ColorMultiplier: 1,
|
||
FadeDuration: 0.1,
|
||
},
|
||
ImageRUIDs: {
|
||
HighlightedSprite: null,
|
||
PressedSprite: null,
|
||
SelectedSprite: null,
|
||
DisabledSprite: null,
|
||
},
|
||
KeyCode: 0,
|
||
OverrideSorting: false,
|
||
Transition: 1,
|
||
Enable: enabled,
|
||
};
|
||
}
|
||
|
||
function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4 }) {
|
||
return {
|
||
'@type': 'MOD.Core.TextComponent',
|
||
Alignment: alignment,
|
||
Bold: bold,
|
||
DropShadow: false,
|
||
DropShadowAngle: 30,
|
||
DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 },
|
||
DropShadowDistance: 32,
|
||
Font: 0,
|
||
FontColor: color,
|
||
FontSize: fontSize,
|
||
MaxSize: fontSize,
|
||
MinSize: 8,
|
||
OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 },
|
||
OutlineDistance: { x: 1, y: -1 },
|
||
OutlineWidth: 1,
|
||
Overflow: 0,
|
||
OverrideSorting: false,
|
||
Padding: { left: 0, right: 0, top: 0, bottom: 0 },
|
||
SizeFit: false,
|
||
Text: value,
|
||
UseOutLine: true,
|
||
Enable: true,
|
||
};
|
||
}
|
||
|
||
function scrollLayoutGroup({ cellSize, spacing, columns }) {
|
||
return {
|
||
'@type': 'MOD.Core.ScrollLayoutGroupComponent',
|
||
CellSize: cellSize,
|
||
ChildAlignment: 0,
|
||
Constraint: 1,
|
||
ConstraintCount: columns,
|
||
GridChildAlignment: 0,
|
||
GridSpacing: spacing,
|
||
HorizontalScrollBarDirection: 0,
|
||
IgnoreMapLayerCheck: false,
|
||
OrderInLayer: 0,
|
||
OverrideSorting: false,
|
||
Padding: { left: 16, right: 16, top: 16, bottom: 16 },
|
||
ReverseArrangement: false,
|
||
ScrollBarBackgroundColor: { r: 1, g: 1, b: 1, a: 0.18 },
|
||
ScrollBarBgImageRUID: { DataId: '' },
|
||
ScrollBarHandleColor: { r: 0.94, g: 0.74, b: 0.26, a: 0.9 },
|
||
ScrollBarHandleImageRUID: { DataId: '' },
|
||
ScrollBarThickness: 12,
|
||
ScrollBarVisible: 1,
|
||
SortingLayer: 'UI',
|
||
Spacing: 0,
|
||
StartAxis: 0,
|
||
StartCorner: 0,
|
||
Type: 2,
|
||
UseScroll: true,
|
||
VerticalScrollBarDirection: 1,
|
||
Enable: true,
|
||
};
|
||
}
|
||
|
||
function popupLayerFor(path) {
|
||
if (path.startsWith('/ui/DefaultGroup/DeckAllHud')) return { root: '/ui/DefaultGroup/DeckAllHud', base: 4000 };
|
||
if (path.startsWith('/ui/DefaultGroup/DeckInspectHud')) return { root: '/ui/DefaultGroup/DeckInspectHud', base: 3000 };
|
||
return null;
|
||
}
|
||
|
||
function uiOrderFor(path, displayOrder) {
|
||
const popup = popupLayerFor(path);
|
||
if (popup != null) {
|
||
const relative = path.slice(popup.root.length).split('/').filter(Boolean);
|
||
return popup.base + relative.length * 100 + displayOrder;
|
||
}
|
||
return displayOrder;
|
||
}
|
||
|
||
function displayOrderFor(path, displayOrder) {
|
||
return uiOrderFor(path, displayOrder);
|
||
}
|
||
|
||
function applySortingOverride(path, components, displayOrder) {
|
||
if (popupLayerFor(path) == null) return components;
|
||
const order = uiOrderFor(path, displayOrder);
|
||
return components.map((component) => {
|
||
if (component['@type'] !== 'MOD.Core.SpriteGUIRendererComponent' && component['@type'] !== 'MOD.Core.TextComponent') {
|
||
return component;
|
||
}
|
||
return {
|
||
...component,
|
||
OverrideSorting: true,
|
||
SortingLayer: 'UI',
|
||
OrderInLayer: order,
|
||
};
|
||
});
|
||
}
|
||
|
||
function entity({ id, path, modelId, entryId, componentNames, components, displayOrder }) {
|
||
const parts = path.split('/');
|
||
const name = parts[parts.length - 1];
|
||
const sortedComponents = applySortingOverride(path, components, displayOrder);
|
||
return {
|
||
id,
|
||
path,
|
||
componentNames,
|
||
jsonString: {
|
||
name,
|
||
path,
|
||
nameEditable: true,
|
||
enable: true,
|
||
visible: true,
|
||
localize: true,
|
||
displayOrder: displayOrderFor(path, displayOrder),
|
||
pathConstraints: '/'.repeat(parts.length - 1),
|
||
revision: 1,
|
||
origin: {
|
||
type: 'Model',
|
||
entry_id: entryId,
|
||
sub_entity_id: null,
|
||
root_entity_id: null,
|
||
replaced_model_id: null,
|
||
},
|
||
modelId,
|
||
'@components': sortedComponents,
|
||
'@version': 1,
|
||
},
|
||
};
|
||
}
|
||
|
||
function uiPath(...parts) {
|
||
return [UI_ROOT, ...parts].join('/');
|
||
}
|
||
|
||
function sectionRoot(section) {
|
||
return uiPath(section);
|
||
}
|
||
|
||
function isGeneratedUiEntity(e) {
|
||
return GENERATED_UI_SECTIONS.some((section) => e.path.startsWith(sectionRoot(section)));
|
||
}
|
||
|
||
function appendUiSection(ui, section, entities) {
|
||
if (!GENERATED_UI_SECTIONS.includes(section)) {
|
||
throw new Error(`[gen-slaydeck] unknown generated UI section: ${section}`);
|
||
}
|
||
const root = sectionRoot(section);
|
||
for (const e of entities) {
|
||
if (!e.path.startsWith(root)) {
|
||
throw new Error(`[gen-slaydeck] ${section} section emitted unexpected path: ${e.path}`);
|
||
}
|
||
}
|
||
ui.ContentProto.Entities.push(...entities);
|
||
}
|
||
|
||
function upsertUi() {
|
||
const ui = JSON.parse(readFileSync(UI_FILE, 'utf8'));
|
||
const E = ui.ContentProto.Entities;
|
||
// CardHand는 스톡 섹션이라 과거 생성된 단색판(NamePlate/CostPlate)이 잔존 → 프레임 이미지 도입으로 제거
|
||
const obsoletePlate = /^\/ui\/DefaultGroup\/CardHand\/Card\d+\/(NamePlate|CostPlate)$/;
|
||
ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e) && !obsoletePlate.test(e.path));
|
||
|
||
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
|
||
const uiSections = new Map();
|
||
const emit = (section, entities) => {
|
||
if (uiSections.has(section)) {
|
||
throw new Error(`[gen-slaydeck] duplicate generated UI section: ${section}`);
|
||
}
|
||
uiSections.set(section, entities);
|
||
};
|
||
|
||
for (const path of DISABLED_STOCK_CONTROLS.map((name) => uiPath(name))) {
|
||
const e = byPath.get(path);
|
||
if (e != null) {
|
||
e.jsonString.enable = false;
|
||
e.jsonString.visible = false;
|
||
for (const component of e.jsonString['@components'] || []) {
|
||
component.Enable = false;
|
||
if (component.RaycastTarget != null) component.RaycastTarget = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 카드 미리보기(초기 정적 표시 — 런타임 RenderHand가 덮어씀): 카드 종류를 순환해 다양성 표시
|
||
const previewIds = Object.keys(CARDS.cards);
|
||
const cards = Array.from({ length: 10 }, (_, i) => {
|
||
const c = CARDS.cards[previewIds[i % previewIds.length]];
|
||
return { name: c.name, cost: String(c.cost), desc: c.desc, frame: frameRuid(c) };
|
||
});
|
||
|
||
// 손패 슬롯 10개 (최대 손패 한도). Card1~5는 기존 엔티티, Card6~10은 신규 생성.
|
||
for (let i = 1; i <= 10; i++) {
|
||
const cardPath = `/ui/DefaultGroup/CardHand/Card${i}`;
|
||
let card = byPath.get(cardPath);
|
||
if (!card) {
|
||
card = entity({
|
||
id: guid('dck', 500 + i),
|
||
path: cardPath,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent',
|
||
displayOrder: 4,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: WHITE, type: 0, raycast: true }),
|
||
button(),
|
||
],
|
||
});
|
||
ui.ContentProto.Entities.push(card);
|
||
byPath.set(cardPath, card);
|
||
}
|
||
const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||
const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent');
|
||
const sx = -680 + (i - 1) * 150;
|
||
tr.RectSize = { x: CARD_W, y: CARD_H };
|
||
tr.anchoredPosition = { x: sx, y: 0 };
|
||
tr.OffsetMin = { x: sx - CARD_W / 2, y: -CARD_H / 2 };
|
||
tr.OffsetMax = { x: sx + CARD_W / 2, y: CARD_H / 2 };
|
||
sp.ImageRUID = { DataId: cards[i - 1].frame };
|
||
sp.Type = 0;
|
||
sp.Color = WHITE;
|
||
sp.RaycastTarget = true;
|
||
const comps = card.jsonString['@components'];
|
||
if (!comps.some((c) => c['@type'] === 'MOD.Core.ButtonComponent')) {
|
||
comps.push(button());
|
||
}
|
||
if (!card.componentNames.includes('MOD.Core.ButtonComponent')) {
|
||
card.componentNames += ',MOD.Core.ButtonComponent';
|
||
}
|
||
if (!comps.some((c) => c['@type'] === 'MOD.Core.UITouchReceiveComponent')) {
|
||
comps.push({ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true });
|
||
}
|
||
if (!card.componentNames.includes('MOD.Core.UITouchReceiveComponent')) {
|
||
card.componentNames += ',MOD.Core.UITouchReceiveComponent';
|
||
}
|
||
card.jsonString.enable = true;
|
||
card.jsonString.visible = true;
|
||
|
||
const handLayout = cardFaceLayout(CARD_W);
|
||
const previewValues = { Cost: cards[i - 1].cost, Name: cards[i - 1].name, Desc: cards[i - 1].desc };
|
||
const children = handLayout.texts.map(([suffix, cfg]) => [suffix, { ...cfg, value: previewValues[suffix] }]);
|
||
for (const [suffix, cfg] of children) {
|
||
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||
let child = byPath.get(path);
|
||
if (!child) {
|
||
child = entity({
|
||
id: guid('dck', i * 10 + children.findIndex(([s]) => s === suffix)),
|
||
path,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: dOrder,
|
||
components: [
|
||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||
],
|
||
});
|
||
ui.ContentProto.Entities.push(child);
|
||
byPath.set(path, child);
|
||
} else {
|
||
child.id = guid('dck', i * 10 + children.findIndex(([s]) => s === suffix));
|
||
child.jsonString.enable = true;
|
||
child.jsonString.visible = true;
|
||
child.jsonString.displayOrder = dOrder;
|
||
const ctr = child.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||
if (ctr) {
|
||
const pivot = { x: 0.5, y: 0.5 };
|
||
ctr.RectSize = cfg.size;
|
||
ctr.anchoredPosition = cfg.pos;
|
||
ctr.OffsetMin = { x: cfg.pos.x - pivot.x * cfg.size.x, y: cfg.pos.y - pivot.y * cfg.size.y };
|
||
ctr.OffsetMax = { x: cfg.pos.x + (1 - pivot.x) * cfg.size.x, y: cfg.pos.y + (1 - pivot.y) * cfg.size.y };
|
||
}
|
||
child.jsonString['@components'][2].Text = cfg.value;
|
||
child.jsonString['@components'][2].FontSize = cfg.fontSize;
|
||
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
|
||
child.jsonString['@components'][2].FontColor = cfg.color;
|
||
}
|
||
}
|
||
|
||
// 프레임 이미지가 이름판·코스트판을 내장하므로 Art만 유지 (잔존 NamePlate/CostPlate는 upsertUi 초입에서 제거)
|
||
const frameKids = [
|
||
['Art', 5, handLayout.art, WHITE, 0],
|
||
];
|
||
for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) {
|
||
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||
let fe = byPath.get(fPath);
|
||
if (!fe) {
|
||
fe = entity({
|
||
id: guid('dck', 200 + i * 10 + dOrder),
|
||
path: fPath,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: dOrder,
|
||
components: [
|
||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||
sprite({ color, type: spriteType, raycast: false }),
|
||
],
|
||
});
|
||
ui.ContentProto.Entities.push(fe);
|
||
byPath.set(fPath, fe);
|
||
} else {
|
||
const ftr = fe.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||
if (ftr) {
|
||
ftr.RectSize = cfg.size;
|
||
ftr.anchoredPosition = cfg.pos;
|
||
ftr.OffsetMin = { x: cfg.pos.x - cfg.size.x / 2, y: cfg.pos.y - cfg.size.y / 2 };
|
||
ftr.OffsetMax = { x: cfg.pos.x + cfg.size.x / 2, y: cfg.pos.y + cfg.size.y / 2 };
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const hud = [];
|
||
const add = (e) => hud.push(e);
|
||
|
||
add(entity({
|
||
id: guid('hud', 0),
|
||
path: '/ui/DefaultGroup/DeckHud',
|
||
modelId: 'uiempty',
|
||
entryId: 'UIEmpty',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }),
|
||
sprite({ color: TRANSPARENT }),
|
||
],
|
||
}));
|
||
|
||
for (const pile of [
|
||
{ key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.20, b: 0.25, a: 1 } },
|
||
{ key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } },
|
||
]) {
|
||
add(entity({
|
||
id: guid('hud', hud.length),
|
||
path: `/ui/DefaultGroup/DeckHud/${pile.key}`,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: pile.key === 'DrawPile' ? 0 : 1,
|
||
components: [
|
||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }),
|
||
sprite({ color: pile.color, type: 1, raycast: true }),
|
||
button(),
|
||
],
|
||
}));
|
||
add(entity({
|
||
id: guid('hud', hud.length),
|
||
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }),
|
||
],
|
||
}));
|
||
add(entity({
|
||
id: guid('hud', hud.length),
|
||
path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: pile.count, fontSize: 42, bold: true }),
|
||
],
|
||
}));
|
||
}
|
||
|
||
add(entity({
|
||
id: guid('hud', hud.length),
|
||
path: '/ui/DefaultGroup/DeckHud/EndTurnButton',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 160 }, align: ALIGN_CENTER }),
|
||
sprite({ color: DARK, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '턴 종료', fontSize: 28, bold: true, color: GOLD, alignment: 0 }),
|
||
],
|
||
}));
|
||
|
||
add(entity({
|
||
id: guid('hud', hud.length),
|
||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 3,
|
||
components: [
|
||
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 160 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }),
|
||
],
|
||
}));
|
||
add(entity({
|
||
id: guid('hud', hud.length),
|
||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
add(entity({
|
||
id: guid('hud', hud.length),
|
||
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
|
||
emit('DeckHud', hud);
|
||
|
||
const inspect = [];
|
||
const inspectHud = entity({
|
||
id: guid('ins', 0),
|
||
path: '/ui/DefaultGroup/DeckInspectHud',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 15,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
inspectHud.jsonString.enable = false;
|
||
inspect.push(inspectHud);
|
||
inspect.push(entity({
|
||
id: guid('ins', 1),
|
||
path: '/ui/DefaultGroup/DeckInspectHud/Panel',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1040, y: 760 }, pos: { x: 0, y: 10 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
|
||
],
|
||
}));
|
||
inspect.push(entity({
|
||
id: guid('ins', 2),
|
||
path: '/ui/DefaultGroup/DeckInspectHud/Title',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 54 }, pos: { x: 0, y: 350 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '\uB371 \uBCF4\uAE30', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
inspect.push(entity({
|
||
id: guid('ins', 3),
|
||
path: '/ui/DefaultGroup/DeckInspectHud/Close',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 466, y: 350 } }),
|
||
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
inspect.push(entity({
|
||
id: guid('ins', 4),
|
||
path: '/ui/DefaultGroup/DeckInspectHud/Empty',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 3,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '\uCE74\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
inspect.push(entity({
|
||
id: guid('ins', 5),
|
||
path: '/ui/DefaultGroup/DeckInspectHud/Grid',
|
||
modelId: 'uiempty',
|
||
entryId: 'UIEmpty',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
|
||
displayOrder: 4,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 950, y: 610 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
|
||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||
],
|
||
}));
|
||
const INSPECT_CARD_COUNT = 60;
|
||
const INSPECT_CARD_W = 158;
|
||
const INSPECT_CARD_H = 214;
|
||
// 카드 단위 엔티티는 v2 네임스페이스(ins2/all2/rwd2/shp2) — 자식 구성이 바뀌면 id를 통째로 새로 발급해야 함.
|
||
// 구 id를 다른 path에 재사용하면 메이커 refresh의 id 기준 in-place 병합이 꼬여 자식이 소실됨 (P13 실측).
|
||
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
|
||
const insBase = 6 + (i - 1) * 7;
|
||
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
|
||
const card = entity({
|
||
id: guid('ins2', insBase),
|
||
path: cardPath,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: i,
|
||
components: [
|
||
transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }),
|
||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
|
||
],
|
||
});
|
||
card.jsonString.enable = false;
|
||
inspect.push(card);
|
||
const inspectLayout = cardFaceLayout(INSPECT_CARD_W);
|
||
for (const [tIdx, [suffix, cfg]] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||
inspect.push(entity({
|
||
id: guid('ins2', insBase + 1 + tIdx),
|
||
path: `${cardPath}/${suffix}`,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: dOrder,
|
||
components: [
|
||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||
],
|
||
}));
|
||
}
|
||
inspect.push(entity({
|
||
id: guid('ins2', insBase + 6),
|
||
path: `${cardPath}/Art`,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: inspectLayout.art.size, pos: inspectLayout.art.pos }),
|
||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||
],
|
||
}));
|
||
}
|
||
emit('DeckInspectHud', inspect);
|
||
|
||
const allDeck = [];
|
||
const allHud = entity({
|
||
id: guid('all', 0),
|
||
path: '/ui/DefaultGroup/DeckAllHud',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 16,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
allHud.jsonString.enable = false;
|
||
allDeck.push(allHud);
|
||
allDeck.push(entity({
|
||
id: guid('all', 1),
|
||
path: '/ui/DefaultGroup/DeckAllHud/Panel',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1080, y: 800 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }),
|
||
],
|
||
}));
|
||
allDeck.push(entity({
|
||
id: guid('all', 2),
|
||
path: '/ui/DefaultGroup/DeckAllHud/Title',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 54 }, pos: { x: 0, y: 380 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '모든 덱', fontSize: 34, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
allDeck.push(entity({
|
||
id: guid('all', 3),
|
||
path: '/ui/DefaultGroup/DeckAllHud/Close',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 486, y: 380 } }),
|
||
sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
allDeck.push(entity({
|
||
id: guid('all', 4),
|
||
path: '/ui/DefaultGroup/DeckAllHud/Empty',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 3,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 40 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '덱이 없습니다', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
allDeck.push(entity({
|
||
id: guid('all', 5),
|
||
path: '/ui/DefaultGroup/DeckAllHud/Grid',
|
||
modelId: 'uiempty',
|
||
entryId: 'UIEmpty',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent',
|
||
displayOrder: 4,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 980, y: 620 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: TRANSPARENT, type: 1, raycast: true }),
|
||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||
],
|
||
}));
|
||
const ALL_DECK_CARD_COUNT = 120;
|
||
const ALL_DECK_CARD_W = 158;
|
||
const ALL_DECK_CARD_H = 214;
|
||
// 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조
|
||
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
|
||
const allBase = 6 + (i - 1) * 7;
|
||
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
|
||
const card = entity({
|
||
id: guid('all2', allBase),
|
||
path: cardPath,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: i,
|
||
components: [
|
||
transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }),
|
||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
|
||
],
|
||
});
|
||
card.jsonString.enable = false;
|
||
allDeck.push(card);
|
||
const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W);
|
||
for (const [tIdx, [suffix, cfg]] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||
allDeck.push(entity({
|
||
id: guid('all2', allBase + 1 + tIdx),
|
||
path: `${cardPath}/${suffix}`,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: dOrder,
|
||
components: [
|
||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||
],
|
||
}));
|
||
}
|
||
allDeck.push(entity({
|
||
id: guid('all2', allBase + 6),
|
||
path: `${cardPath}/Art`,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: allDeckLayout.art.size, pos: allDeckLayout.art.pos }),
|
||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||
],
|
||
}));
|
||
}
|
||
emit('DeckAllHud', allDeck);
|
||
|
||
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
|
||
const combat = [];
|
||
combat.push(entity({
|
||
id: guid('cmb', 0),
|
||
path: '/ui/DefaultGroup/CombatHud',
|
||
modelId: 'uiempty',
|
||
entryId: 'UIEmpty',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 4,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT }),
|
||
],
|
||
}));
|
||
const SLOT_W = 140, SLOT_H = 96;
|
||
for (let i = 1; i <= MAX_MONSTERS; i++) {
|
||
const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`;
|
||
const slot = entity({
|
||
id: guid('cmb', 40 + i),
|
||
path: base,
|
||
modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 20 + i,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }),
|
||
sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }),
|
||
button(),
|
||
],
|
||
});
|
||
slot.jsonString.enable = false;
|
||
combat.push(slot);
|
||
const targetFrame = entity({
|
||
id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }),
|
||
],
|
||
});
|
||
targetFrame.jsonString.enable = false;
|
||
combat.push(targetFrame);
|
||
const actFrame = entity({
|
||
id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: { r: 0.95, g: 0.3, b: 0.25, a: 0.3 }, type: 1 }),
|
||
],
|
||
});
|
||
actFrame.jsonString.enable = false;
|
||
combat.push(actFrame);
|
||
combat.push(entity({
|
||
id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 30 }, pos: { x: 0, y: 34 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 26 }, pos: { x: 0, y: 6 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 3,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }),
|
||
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 4,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }),
|
||
sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 40, y: 24 }, pos: { x: 0, y: -36 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
const dmgPop = entity({
|
||
id: guid('cmb', 250 + i), path: `${base}/DmgPop`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 9,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 30 }, pos: { x: 0, y: 60 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 24, bold: true, color: { r: 1, g: 0.35, b: 0.3, a: 1 }, alignment: 4 }),
|
||
],
|
||
});
|
||
dmgPop.jsonString.enable = false;
|
||
combat.push(dmgPop);
|
||
const mBlockBadge = entity({
|
||
id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 6,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 36 }, pos: { x: -HP_BAR_W / 2 - 30, y: -14 } }),
|
||
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
|
||
],
|
||
});
|
||
mBlockBadge.jsonString.enable = false;
|
||
combat.push(mBlockBadge);
|
||
combat.push(entity({
|
||
id: guid('cmb', 280 + i), path: `${base}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 40, parentH: 36, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 32 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '0', fontSize: 17, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 290 + i), path: `${base}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 7,
|
||
components: [
|
||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 60, y: 22 }, pos: { x: 0, y: -58 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 15, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
}
|
||
const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel';
|
||
combat.push(entity({
|
||
id: guid('cmb', 210), path: PP, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 96 }, pos: { x: -760, y: -494 }, align: ALIGN_CENTER }),
|
||
sprite({ color: PANEL_BG, type: 1 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }),
|
||
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 14 }, pos: { x: -94, y: -6 } }),
|
||
sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 3,
|
||
components: [
|
||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
const blockBadge = entity({
|
||
id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 4,
|
||
components: [
|
||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }),
|
||
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
|
||
],
|
||
});
|
||
blockBadge.jsonString.enable = false;
|
||
combat.push(blockBadge);
|
||
combat.push(entity({
|
||
id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
combat.push(entity({
|
||
id: guid('cmb', 217), path: `${PP}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 6,
|
||
components: [
|
||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 22 }, pos: { x: 0, y: -44 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 14, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
const playerDmgPop = entity({
|
||
id: guid('cmb', 260), path: `${PP}/DmgPop`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 30 }, pos: { x: 16, y: 40 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 22, bold: true, color: { r: 1, g: 0.4, b: 0.35, a: 1 }, alignment: 4 }),
|
||
],
|
||
});
|
||
playerDmgPop.jsonString.enable = false;
|
||
combat.push(playerDmgPop);
|
||
combat.push(entity({
|
||
id: guid('cmb', 200),
|
||
path: '/ui/DefaultGroup/CombatHud/TopBar',
|
||
modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 9,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }),
|
||
],
|
||
}));
|
||
const topTexts = [
|
||
['Floor', -520, 160, '막 1/3', GOLD],
|
||
['Gold', -360, 160, '메소 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
|
||
];
|
||
topTexts.forEach(([suffix, x, w, value, color], ti) => {
|
||
combat.push(entity({
|
||
id: guid('cmb', 201 + ti),
|
||
path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`,
|
||
modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: ti,
|
||
components: [
|
||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value, fontSize: 22, bold: true, color, alignment: 4 }),
|
||
],
|
||
}));
|
||
});
|
||
combat.push(entity({
|
||
id: guid('cmb', 209),
|
||
path: '/ui/DefaultGroup/CombatHud/TopBar/MesoIcon',
|
||
modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 26, y: 26 }, pos: { x: -432, y: 0 } }),
|
||
sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }),
|
||
],
|
||
}));
|
||
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 }),
|
||
],
|
||
}));
|
||
const rewardXs = [-300, 0, 300];
|
||
// 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조
|
||
for (let i = 1; i <= 3; i++) {
|
||
const rwdBase = 2 + (i - 1) * 7;
|
||
const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`;
|
||
reward.push(entity({
|
||
id: guid('rwd2', rwdBase),
|
||
path: cardPath,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent',
|
||
displayOrder: i,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: rewardXs[i - 1], y: 0 } }),
|
||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }),
|
||
button(),
|
||
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
|
||
],
|
||
}));
|
||
const rewardLayout = cardFaceLayout(CARD_W);
|
||
for (const [tIdx, [suffix, cfg]] of rewardLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]).entries()) {
|
||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||
reward.push(entity({
|
||
id: guid('rwd2', rwdBase + 1 + tIdx),
|
||
path: `${cardPath}/${suffix}`,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: dOrder,
|
||
components: [
|
||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||
],
|
||
}));
|
||
}
|
||
reward.push(entity({
|
||
id: guid('rwd2', rwdBase + 6),
|
||
path: `${cardPath}/Art`,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: rewardLayout.art.size, pos: rewardLayout.art.pos }),
|
||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||
],
|
||
}));
|
||
}
|
||
let rwdN = 2 + 3 * 7; // 구 시퀀스의 루프 종료 시점 값(23) 보존 — Skip 등 후속 id 불변
|
||
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 }),
|
||
],
|
||
}));
|
||
// 절차 생성 맵용 정적 그리드 — 가로 진행(왼→오른쪽): 행(row)=x축, 열(col)=y축 분기, 보스는 최우측 중앙.
|
||
const nodeX = (row) => -540 + (row - 1) * 150;
|
||
const nodeY = (col) => 180 - (col - 1) * 120;
|
||
const BOSS_POS = { x: nodeX(MAP_ROWS) + 150, y: 0 };
|
||
let mapN = 2;
|
||
const pushMapNode = (id, pos, size, label) => {
|
||
const nodePath = `/ui/DefaultGroup/MapHud/Node_${id}`;
|
||
const nodeEnt = entity({
|
||
id: guid('map', mapN++),
|
||
path: nodePath,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
|
||
sprite({ color: { r: 0.2, g: 0.22, b: 0.26, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
],
|
||
});
|
||
nodeEnt.jsonString.enable = false;
|
||
map.push(nodeEnt);
|
||
map.push(entity({
|
||
id: guid('map', mapN++),
|
||
path: `${nodePath}/Label`,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: size.x, parentH: size.y, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: size.x + 20, y: 30 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: label, fontSize: id === 'boss' ? 18 : 15, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
};
|
||
for (let r = 1; r <= MAP_ROWS; r++) {
|
||
for (let c = 1; c <= MAP_COLS; c++) {
|
||
pushMapNode(`r${r}c${c}`, { x: nodeX(r), y: nodeY(c) }, { x: 56, y: 56 }, '');
|
||
}
|
||
}
|
||
pushMapNode('boss', BOSS_POS, { x: 72, y: 72 }, '보스');
|
||
const pushDots = (dotId, from, to) => {
|
||
for (let k = 1; k <= 3; k++) {
|
||
const t = k / 4;
|
||
const dot = entity({
|
||
id: guid('map', mapN++),
|
||
path: `/ui/DefaultGroup/MapHud/Dot_${dotId}_${k}`,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 8, y: 8 }, pos: { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t } }),
|
||
sprite({ color: { r: 0.5, g: 0.5, b: 0.55, a: 0.8 }, type: 1 }),
|
||
],
|
||
});
|
||
dot.jsonString.enable = false;
|
||
map.push(dot);
|
||
}
|
||
};
|
||
for (let r = 1; r < MAP_ROWS; r++) {
|
||
for (let c = 1; c <= MAP_COLS; c++) {
|
||
for (let c2 = c - 1; c2 <= c + 1; c2++) {
|
||
if (c2 < 1 || c2 > MAP_COLS) continue;
|
||
pushDots(`r${r}c${c}_${c2}`, { x: nodeX(r), y: nodeY(c) }, { x: nodeX(r + 1), y: nodeY(c2) });
|
||
}
|
||
}
|
||
}
|
||
for (let c = 1; c <= MAP_COLS; c++) {
|
||
pushDots(`r${MAP_ROWS}c${c}_b`, { x: nodeX(MAP_ROWS), y: nodeY(c) }, BOSS_POS);
|
||
}
|
||
emit('MapHud', map);
|
||
|
||
const shop = [];
|
||
const shopHud = entity({
|
||
id: guid('shp', 0),
|
||
path: '/ui/DefaultGroup/ShopHud',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 8,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
shopHud.jsonString.enable = false;
|
||
shop.push(shopHud);
|
||
shop.push(entity({
|
||
id: guid('shp', 1),
|
||
path: '/ui/DefaultGroup/ShopHud/Title',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '상점', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', 2),
|
||
path: '/ui/DefaultGroup/ShopHud/Gold',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 44 }, pos: { x: 0, y: 330 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '메소 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', 3),
|
||
path: '/ui/DefaultGroup/ShopHud/MesoIcon',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 30, y: 30 }, pos: { x: -86, y: 330 } }),
|
||
sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }),
|
||
],
|
||
}));
|
||
const shopXs = [-300, 0, 300];
|
||
// 카드 단위 엔티티 v2 네임스페이스 (stride 8: Price 포함) — DeckInspectHud 주석 참조
|
||
for (let i = 1; i <= 3; i++) {
|
||
const shpBase = 3 + (i - 1) * 8;
|
||
const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`;
|
||
shop.push(entity({
|
||
id: guid('shp2', shpBase),
|
||
path: cardPath,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent',
|
||
displayOrder: i,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: shopXs[i - 1], y: 20 } }),
|
||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }),
|
||
button(),
|
||
{ '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true },
|
||
],
|
||
}));
|
||
const shopLayout = cardFaceLayout(CARD_W);
|
||
const shopTexts = shopLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]);
|
||
shopTexts.push(['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -135 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }]);
|
||
for (const [tIdx, [suffix, cfg]] of shopTexts.entries()) {
|
||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9;
|
||
shop.push(entity({
|
||
id: guid('shp2', shpBase + 1 + tIdx),
|
||
path: `${cardPath}/${suffix}`,
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: dOrder,
|
||
components: [
|
||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||
],
|
||
}));
|
||
}
|
||
shop.push(entity({
|
||
id: guid('shp2', shpBase + 7),
|
||
path: `${cardPath}/Art`,
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: shopLayout.art.size, pos: shopLayout.art.pos }),
|
||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||
],
|
||
}));
|
||
}
|
||
let shpN = 3 + 3 * 8; // 구 시퀀스의 루프 종료 시점 값(27) 보존 — Relic 등 후속 id 불변
|
||
shop.push(entity({
|
||
id: guid('shp', shpN++),
|
||
path: '/ui/DefaultGroup/ShopHud/Relic',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 9,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -190 } }),
|
||
sprite({ color: { r: 0.7, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', shpN++),
|
||
path: '/ui/DefaultGroup/ShopHud/Relic/Label',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '유물', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', shpN++),
|
||
path: '/ui/DefaultGroup/ShopHud/Relic/Price',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '60 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', shpN++),
|
||
path: '/ui/DefaultGroup/ShopHud/Potion',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 11,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -278 } }),
|
||
sprite({ color: { r: 0.45, g: 0.7, b: 0.55, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', shpN++),
|
||
path: '/ui/DefaultGroup/ShopHud/Potion/Label',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '물약', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', shpN++),
|
||
path: '/ui/DefaultGroup/ShopHud/Potion/Price',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '20 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
shop.push(entity({
|
||
id: guid('shp', shpN++),
|
||
path: '/ui/DefaultGroup/ShopHud/Leave',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 10,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 } }),
|
||
sprite({ color: DARK, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
emit('ShopHud', shop);
|
||
|
||
const rest = [];
|
||
const restHud = entity({
|
||
id: guid('rst', 0),
|
||
path: '/ui/DefaultGroup/RestHud',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 9,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.05, g: 0.08, b: 0.06, a: 0.92 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
restHud.jsonString.enable = false;
|
||
rest.push(restHud);
|
||
rest.push(entity({
|
||
id: guid('rst', 1),
|
||
path: '/ui/DefaultGroup/RestHud/Title',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 140 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '휴식', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
rest.push(entity({
|
||
id: guid('rst', 2),
|
||
path: '/ui/DefaultGroup/RestHud/Info',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: 'HP 회복', fontSize: 30, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
rest.push(entity({
|
||
id: guid('rst', 3),
|
||
path: '/ui/DefaultGroup/RestHud/Leave',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -120 } }),
|
||
sprite({ color: DARK, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
emit('RestHud', rest);
|
||
|
||
// 유물 방 — 보물 상자 (P8)
|
||
const treasure = [];
|
||
const treasureHud = entity({
|
||
id: guid('trs', 0),
|
||
path: '/ui/DefaultGroup/TreasureHud',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 8,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
treasureHud.jsonString.enable = false;
|
||
treasure.push(treasureHud);
|
||
treasure.push(entity({
|
||
id: guid('trs', 1),
|
||
path: '/ui/DefaultGroup/TreasureHud/Title',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 320 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '보물 상자', fontSize: 40, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
treasure.push(entity({
|
||
id: guid('trs', 2),
|
||
path: '/ui/DefaultGroup/TreasureHud/Chest',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 180 }, pos: { x: 0, y: 40 } }),
|
||
sprite({ dataId: CHEST_CLOSED_RUID, color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }),
|
||
button(),
|
||
],
|
||
}));
|
||
treasure.push(entity({
|
||
id: guid('trs', 3),
|
||
path: '/ui/DefaultGroup/TreasureHud/Hint',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 500, y: 34 }, pos: { x: 0, y: -90 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '상자를 클릭해 여세요', fontSize: 20, bold: false, color: { r: 0.85, g: 0.85, b: 0.9, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
const treasureReward = entity({
|
||
id: guid('trs', 4),
|
||
path: '/ui/DefaultGroup/TreasureHud/Reward',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 3,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 44 }, pos: { x: 0, y: -160 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||
],
|
||
});
|
||
treasureReward.jsonString.enable = false;
|
||
treasure.push(treasureReward);
|
||
treasure.push(entity({
|
||
id: guid('trs', 5),
|
||
path: '/ui/DefaultGroup/TreasureHud/Leave',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 4,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -280 } }),
|
||
sprite({ color: DARK, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
emit('TreasureHud', treasure);
|
||
|
||
// 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직
|
||
const jobChoice = [];
|
||
const jobChoiceHud = entity({
|
||
id: guid('job', 0),
|
||
path: '/ui/DefaultGroup/JobChoiceHud',
|
||
modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 9,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
jobChoiceHud.jsonString.enable = false;
|
||
jobChoice.push(jobChoiceHud);
|
||
jobChoice.push(entity({
|
||
id: guid('job', 1),
|
||
path: '/ui/DefaultGroup/JobChoiceHud/Title',
|
||
modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 220 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '보스 처치 보상을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
const jcButtons = [
|
||
['RelicButton', '유물 획득', -240, { r: 0.7, g: 0.55, b: 0.85, a: 1 }],
|
||
['JobButton', '2차 전직', 240, { r: 0.86, g: 0.6, b: 0.3, a: 1 }],
|
||
];
|
||
jcButtons.forEach(([suffix, label, x, color], bi) => {
|
||
jobChoice.push(entity({
|
||
id: guid('job', 2 + bi),
|
||
path: `/ui/DefaultGroup/JobChoiceHud/${suffix}`,
|
||
modelId: 'uibutton', entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1 + bi,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 140 }, pos: { x, y: 0 } }),
|
||
sprite({ color, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: label, fontSize: 32, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
});
|
||
emit('JobChoiceHud', jobChoice);
|
||
|
||
const jobSelect = [];
|
||
const jobSelectHud = entity({
|
||
id: guid('job', 10),
|
||
path: '/ui/DefaultGroup/JobSelectHud',
|
||
modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 10,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.94 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
jobSelectHud.jsonString.enable = false;
|
||
jobSelect.push(jobSelectHud);
|
||
jobSelect.push(entity({
|
||
id: guid('job', 11),
|
||
path: '/ui/DefaultGroup/JobSelectHud/Title',
|
||
modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 300 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '2차 전직 — 직업을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
// 범용 슬롯 3개 — ShowJobSelect(Lua)가 클래스별 JOBS로 텍스트를 채움 (P10 동적화)
|
||
const jobs = [
|
||
['slot1', '', '', '', -440, { r: 0.82, g: 0.4, b: 0.34, a: 1 }],
|
||
['slot2', '', '', '', 0, { r: 0.4, g: 0.55, b: 0.85, a: 1 }],
|
||
['slot3', '', '', '', 440, { r: 0.42, g: 0.72, b: 0.46, a: 1 }],
|
||
];
|
||
jobs.forEach(([jobId, name, desc, starter, x, color], ji) => {
|
||
const base = `/ui/DefaultGroup/JobSelectHud/Job_${jobId}`;
|
||
jobSelect.push(entity({
|
||
id: guid('job', 12 + ji * 4),
|
||
path: base,
|
||
modelId: 'uibutton', entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 1 + ji,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 420 }, pos: { x, y: -20 } }),
|
||
sprite({ color, type: 1, raycast: true }),
|
||
button(),
|
||
],
|
||
}));
|
||
jobSelect.push(entity({
|
||
id: guid('job', 13 + ji * 4),
|
||
path: `${base}/Name`,
|
||
modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 50 }, pos: { x: 0, y: 150 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: name, fontSize: 34, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
jobSelect.push(entity({
|
||
id: guid('job', 14 + ji * 4),
|
||
path: `${base}/Desc`,
|
||
modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 160 }, pos: { x: 0, y: 0 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: desc, fontSize: 22, bold: false, color: { r: 0.95, g: 0.95, b: 0.97, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
jobSelect.push(entity({
|
||
id: guid('job', 15 + ji * 4),
|
||
path: `${base}/Starter`,
|
||
modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 32 }, pos: { x: 0, y: -160 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: starter, fontSize: 18, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
});
|
||
emit('JobSelectHud', jobSelect);
|
||
|
||
const menu = [];
|
||
menu.push(entity({
|
||
id: guid('menu', 0),
|
||
path: '/ui/DefaultGroup/MainMenu',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 20,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.06, g: 0.09, b: 0.13, a: 1 }, type: 1, raycast: true }),
|
||
],
|
||
}));
|
||
menu.push(entity({
|
||
id: guid('menu', 50),
|
||
path: '/ui/DefaultGroup/MainMenu/OpaqueBackdrop',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||
],
|
||
}));
|
||
menu.push(entity({
|
||
id: guid('menu', 1),
|
||
path: '/ui/DefaultGroup/MainMenu/Title',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 100 }, pos: { x: 0, y: 180 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '슬레이 메이플', fontSize: 64, bold: true, color: GOLD, alignment: 0 }),
|
||
],
|
||
}));
|
||
menu.push(entity({
|
||
id: guid('menu', 2),
|
||
path: '/ui/DefaultGroup/MainMenu/Subtitle',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 48 }, pos: { x: 0, y: 104 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '카드를 뽑고, 덱을 만들고, 첨탑을 오른다', fontSize: 24, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 0 }),
|
||
],
|
||
}));
|
||
menu.push(entity({
|
||
id: guid('menu', 3),
|
||
path: '/ui/DefaultGroup/MainMenu/NewGameButton',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 68 }, pos: { x: 0, y: -20 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '새 게임', fontSize: 30, bold: true, color: GOLD, alignment: 0 }),
|
||
],
|
||
}));
|
||
// 승천 선택 (P11): [-] 라벨 [+]
|
||
menu.push(entity({
|
||
id: guid('menu', 195),
|
||
path: '/ui/DefaultGroup/MainMenu/AscMinus',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 5,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: -170, y: -185 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '-', fontSize: 30, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
menu.push(entity({
|
||
id: guid('menu', 196),
|
||
path: '/ui/DefaultGroup/MainMenu/AscLabel',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 6,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 250, y: 40 }, pos: { x: 0, y: -185 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '승천 0 / 해금 0', fontSize: 22, bold: true, color: { r: 0.85, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
menu.push(entity({
|
||
id: guid('menu', 197),
|
||
path: '/ui/DefaultGroup/MainMenu/AscPlus',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 7,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: 170, y: -185 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '+', fontSize: 30, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
menu.push(entity({
|
||
id: guid('menu', 4),
|
||
path: '/ui/DefaultGroup/MainMenu/ContinueButton',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 3,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 58 }, pos: { x: 0, y: -100 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.1, g: 0.11, b: 0.13, a: 0.78 }, type: 1, raycast: false }),
|
||
button({ enabled: false }),
|
||
text({ value: '이어하기', fontSize: 24, bold: true, color: { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 0 }),
|
||
],
|
||
}));
|
||
const select = [];
|
||
select.push(entity({
|
||
id: guid('menu', 100),
|
||
path: '/ui/DefaultGroup/CharacterSelectHud',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 21,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }),
|
||
],
|
||
}));
|
||
select.push(entity({
|
||
id: guid('menu', 190),
|
||
path: '/ui/DefaultGroup/CharacterSelectHud/OpaqueBackdrop',
|
||
modelId: 'uisprite',
|
||
entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 0,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||
],
|
||
}));
|
||
select.push(entity({
|
||
id: guid('menu', 101),
|
||
path: '/ui/DefaultGroup/CharacterSelectHud/Title',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 72 }, pos: { x: 0, y: 355 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '\uCE90\uB9AD\uD130 \uC120\uD0DD', fontSize: 42, bold: true, color: GOLD, alignment: 0 }),
|
||
],
|
||
}));
|
||
select.push(entity({
|
||
id: guid('menu', 102),
|
||
path: '/ui/DefaultGroup/CharacterSelectHud/Status',
|
||
modelId: 'uitext',
|
||
entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 680, y: 44 }, pos: { x: 0, y: 298 }, align: ALIGN_CENTER }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '\uC804\uC0AC\uB97C \uC120\uD0DD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694', fontSize: 22, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }),
|
||
],
|
||
}));
|
||
const classCards = [
|
||
{ key: 'Warrior', classId: 'warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } },
|
||
{ key: 'Thief', classId: 'bandit', label: '\uB3C4\uC801', desc: '\uB3C5\u00B7\uB2E8\uAC80\u00B7\uB4DC\uB85C\uC6B0', x: 0, enabled: true, tint: { r: 0.26, g: 0.5, b: 0.34, a: 1 } },
|
||
{ key: 'Mage', classId: 'magician', label: '\uB9C8\uBC95\uC0AC', desc: '\uB9C8\uBC95 \uC6D0\uAC70\uB9AC \uB51C\uB7EC', x: 360, enabled: true, tint: { r: 0.3, g: 0.4, b: 0.75, a: 1 } },
|
||
];
|
||
for (let i = 0; i < classCards.length; i++) {
|
||
const cls = classCards[i];
|
||
const base = `/ui/DefaultGroup/CharacterSelectHud/${cls.key}Button`;
|
||
select.push(entity({
|
||
id: guid('menu', 110 + i),
|
||
path: base,
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 10 + i,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 270, y: 330 }, pos: { x: cls.x, y: 40 }, align: ALIGN_CENTER }),
|
||
sprite({ color: cls.enabled ? { r: 0.16, g: 0.2, b: 0.26, a: 1 } : { r: 0.11, g: 0.12, b: 0.14, a: 1 }, type: 1, raycast: cls.enabled }),
|
||
button({ enabled: cls.enabled }),
|
||
],
|
||
}));
|
||
select.push(entity({
|
||
id: guid('menu', 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', 170 + i),
|
||
path: `/ui/DefaultGroup/CharacterSelectHud/${cls.key}DeckButton`,
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 18 + i,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 46 }, pos: { x: cls.x, y: -160 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }),
|
||
button({ enabled: cls.enabled }),
|
||
text({ value: '\uB371 \uBCF4\uAE30', fontSize: 20, bold: true, color: GOLD, alignment: 0 }),
|
||
],
|
||
}));
|
||
}
|
||
select.push(entity({
|
||
id: guid('menu', 180),
|
||
path: '/ui/DefaultGroup/CharacterSelectHud/StartButton',
|
||
modelId: 'uibutton',
|
||
entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||
displayOrder: 20,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 68 }, pos: { x: 720, y: -360 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '\uC2DC\uC791', fontSize: 30, bold: true, color: GOLD, alignment: 0 }),
|
||
],
|
||
}));
|
||
select[0].jsonString.enable = false;
|
||
emit('MainMenu', menu);
|
||
emit('CharacterSelectHud', select);
|
||
|
||
// ── LobbyHud — 반복 런의 허브. NPC 클릭으로 런시작/도감/영혼상점/게시판 ──
|
||
const lobby = [];
|
||
let lobId = 0;
|
||
const lobbyRoot = entity({
|
||
id: guid('lob', lobId++),
|
||
path: '/ui/DefaultGroup/LobbyHud',
|
||
modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 11,
|
||
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 }),
|
||
// 로비가 물리 맵이 됨 — 투명 + 비레이캐스트로 맵을 가리지 않고 월드 NPC 클릭이 통과되게 함.
|
||
sprite({ color: TRANSPARENT, type: 1, raycast: false }),
|
||
],
|
||
});
|
||
lobbyRoot.jsonString.enable = false;
|
||
lobby.push(lobbyRoot);
|
||
const lobTexts = [
|
||
['Title', 0, 478, 760, '메이플 로비', 40, GOLD],
|
||
['SoulLabel', 700, 478, 320, '영혼 0', 28, { r: 0.6, g: 0.85, b: 1, a: 1 }],
|
||
['AscLabel', -560, 478, 380, '승천 0 / 해금 0', 22, { r: 0.9, g: 0.7, b: 0.5, a: 1 }],
|
||
['Hint', 0, -478, 1500, 'NPC에게 다가가 ↑ 또는 클릭으로 대화 · ← → 이동 · Ctrl 공격', 20, { r: 0.72, g: 0.76, b: 0.82, a: 1 }],
|
||
];
|
||
for (const [suffix, x, y, w, value, fs, color] of lobTexts) {
|
||
lobby.push(entity({
|
||
id: guid('lob', lobId++),
|
||
path: `/ui/DefaultGroup/LobbyHud/${suffix}`,
|
||
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: w, y: 56 }, pos: { x, y } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value, fontSize: fs, bold: true, color, alignment: 4 }),
|
||
],
|
||
}));
|
||
}
|
||
for (const [suffix, x, label] of [['AscMinus', -780, '<'], ['AscPlus', -540, '>']]) {
|
||
lobby.push(entity({
|
||
id: guid('lob', lobId++),
|
||
path: `/ui/DefaultGroup/LobbyHud/${suffix}`,
|
||
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: 46, y: 42 }, pos: { x, y: 470 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: label, fontSize: 28, bold: true, color: WHITE, alignment: 4 }),
|
||
],
|
||
}));
|
||
}
|
||
// NPC 4종은 로비 물리 맵의 월드 엔티티(map/lobby.map + LobbyNpc codeblock)로 이동. UI 버튼 행 제거.
|
||
emit('LobbyHud', lobby);
|
||
|
||
// ── BoardHud — 게시판(공지/팁) ──
|
||
const board = [];
|
||
let brdId = 0;
|
||
const boardRoot = entity({
|
||
id: guid('brd', brdId++),
|
||
path: '/ui/DefaultGroup/BoardHud',
|
||
modelId: 'uisprite', entryId: 'UISprite',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||
displayOrder: 14,
|
||
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.95 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
boardRoot.jsonString.enable = false;
|
||
board.push(boardRoot);
|
||
board.push(entity({
|
||
id: guid('brd', brdId++),
|
||
path: '/ui/DefaultGroup/BoardHud/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: 700, y: 60 }, pos: { x: 0, y: 400 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '게시판', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||
],
|
||
}));
|
||
board.push(entity({
|
||
id: guid('brd', brdId++),
|
||
path: '/ui/DefaultGroup/BoardHud/Body',
|
||
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: 1100, y: 520 }, pos: { x: 0, y: 20 } }),
|
||
sprite({ color: { r: 0.1, g: 0.12, b: 0.16, a: 0.9 }, type: 1 }),
|
||
text({ value: '· 카드는 직업/등급에 따라 보상 확률이 다릅니다.\n· 몬스터는 매 턴 정해진 행동 중 하나를 무작위로 합니다.\n· 일부 몬스터는 덱에 저주 카드(상처/화상)를 넣습니다.\n· 손패는 최대 10장, 초과분은 자동으로 버려집니다.\n· 2차 전직 후 보스를 잡으면 영혼이 쌓입니다.\n· 영혼은 상인 NPC에서 덱빌딩 해금에 사용합니다.', fontSize: 24, bold: false, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }),
|
||
],
|
||
}));
|
||
board.push(entity({
|
||
id: guid('brd', brdId++),
|
||
path: '/ui/DefaultGroup/BoardHud/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: 200, y: 60 }, pos: { x: 0, y: -380 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }),
|
||
],
|
||
}));
|
||
emit('BoardHud', board);
|
||
|
||
// ── SoulShopHud — 영혼 메타 상점 (Phase 9에서 해금 항목·구매 로직 채움) ──
|
||
const soulShop = [];
|
||
let soulId = 0;
|
||
const soulRoot = entity({
|
||
id: guid('soul', soulId++),
|
||
path: '/ui/DefaultGroup/SoulShopHud',
|
||
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.05, g: 0.06, b: 0.09, a: 0.95 }, type: 1, raycast: true }),
|
||
],
|
||
});
|
||
soulRoot.jsonString.enable = false;
|
||
soulShop.push(soulRoot);
|
||
soulShop.push(entity({
|
||
id: guid('soul', soulId++),
|
||
path: '/ui/DefaultGroup/SoulShopHud/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: 700, y: 60 }, pos: { x: 0, y: 410 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '영혼 상점', fontSize: 44, bold: true, color: { r: 0.6, g: 0.85, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
soulShop.push(entity({
|
||
id: guid('soul', soulId++),
|
||
path: '/ui/DefaultGroup/SoulShopHud/Souls',
|
||
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: 400, y: 44 }, pos: { x: 0, y: 345 } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '영혼 0', fontSize: 28, bold: true, color: { r: 0.6, g: 0.85, b: 1, a: 1 }, alignment: 4 }),
|
||
],
|
||
}));
|
||
soulShop.push(entity({
|
||
id: guid('soul', soulId++),
|
||
path: '/ui/DefaultGroup/SoulShopHud/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: 200, y: 60 }, pos: { x: 0, y: -400 }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }),
|
||
],
|
||
}));
|
||
for (let i = 1; i <= 4; i++) {
|
||
const ip = `/ui/DefaultGroup/SoulShopHud/Item${i}`;
|
||
const iy = 230 - (i - 1) * 125;
|
||
soulShop.push(entity({
|
||
id: guid('soul', soulId++),
|
||
path: ip, modelId: 'uibutton', entryId: 'UIButton',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||
displayOrder: 2,
|
||
components: [
|
||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 104 }, pos: { x: 0, y: iy }, align: ALIGN_CENTER }),
|
||
sprite({ color: { r: 0.14, g: 0.16, b: 0.22, a: 1 }, type: 1, raycast: true }),
|
||
button(),
|
||
],
|
||
}));
|
||
for (const [suffix, x, y, w, fs, color] of [
|
||
['Name', -180, 22, 360, 28, GOLD],
|
||
['Desc', -180, -24, 440, 20, { r: 0.86, g: 0.9, b: 0.94, a: 1 }],
|
||
['Status', 270, 0, 220, 22, { r: 0.6, g: 0.85, b: 1, a: 1 }],
|
||
]) {
|
||
soulShop.push(entity({
|
||
id: guid('soul', soulId++),
|
||
path: `${ip}/${suffix}`, modelId: 'uitext', entryId: 'UIText',
|
||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||
displayOrder: 1,
|
||
components: [
|
||
transform({ parentW: 760, parentH: 104, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 38 }, pos: { x, y } }),
|
||
sprite({ color: TRANSPARENT }),
|
||
text({ value: '', fontSize: fs, bold: suffix === 'Name', color, alignment: 4 }),
|
||
],
|
||
}));
|
||
}
|
||
}
|
||
emit('SoulShopHud', soulShop);
|
||
|
||
for (const section of UI_APPEND_ORDER) {
|
||
const entities = uiSections.get(section);
|
||
if (entities == null) {
|
||
throw new Error(`[gen-slaydeck] missing generated UI section: ${section}`);
|
||
}
|
||
appendUiSection(ui, section, entities);
|
||
}
|
||
|
||
// 엔티티 id 유일성 검증 — 같은 id가 다른 path에 재배정되면 메이커 refresh 병합이 꼬임
|
||
const seenIds = new Map();
|
||
for (const e of ui.ContentProto.Entities) {
|
||
const prev = seenIds.get(e.id);
|
||
if (prev != null) throw new Error(`[gen-slaydeck] 엔티티 id 중복: ${e.id} (${prev} ↔ ${e.path})`);
|
||
seenIds.set(e.id, e.path);
|
||
}
|
||
|
||
JSON.parse(JSON.stringify(ui));
|
||
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
|
||
}
|
||
|
||
function prop(Type, Name, DefaultValue = 'nil') {
|
||
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };
|
||
}
|
||
|
||
function method(Name, Code, Arguments = [], ExecSpace = 0, ReturnType = 'void') {
|
||
return {
|
||
Return: { Type: ReturnType, DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null },
|
||
Arguments,
|
||
Code,
|
||
Scope: 2,
|
||
ExecSpace,
|
||
Attributes: [],
|
||
Name,
|
||
};
|
||
}
|
||
|
||
function codeblock(id, name, properties, methods) {
|
||
return {
|
||
Id: '',
|
||
GameId: '',
|
||
EntryKey: `codeblock://${id}`,
|
||
ContentType: 'x-mod/codeblock',
|
||
Content: '',
|
||
Usage: 0,
|
||
UsePublish: 1,
|
||
UseService: 0,
|
||
CoreVersion: '26.5.0.0',
|
||
StudioVersion: '',
|
||
DynamicLoading: 0,
|
||
ContentProto: {
|
||
Use: 'Json',
|
||
Json: {
|
||
CoreVersion: { Major: 0, Minor: 2 },
|
||
ScriptVersion: { Major: 1, Minor: 0 },
|
||
Description: '',
|
||
Id: id,
|
||
Language: 1,
|
||
Name: name,
|
||
Type: 1,
|
||
Source: 0,
|
||
Target: null,
|
||
Properties: properties,
|
||
Methods: methods,
|
||
EntityEventHandlers: [],
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function writeCodeblocks() {
|
||
const RUN_LENGTH = 5;
|
||
const GOLD_PER_WIN = 25;
|
||
const CARD_PRICE = 30;
|
||
const REST_HEAL = 30;
|
||
const RELIC_PRICE = 60;
|
||
const ACT_COUNT = 5;
|
||
const ACT_MAPS = ['map01', 'map02', 'map03', 'map04', 'map05'];
|
||
const LOBBY_MAP = 'lobby';
|
||
const LOBBY_SPAWN = 'Vector3(-5, 0.03, 0)'; // 정찰: map01 지면 좌측
|
||
const combat = codeblock('SlayDeckController', 'SlayDeckController', [
|
||
prop('any', 'DrawPile'),
|
||
prop('any', 'DiscardPile'),
|
||
prop('any', 'Hand'),
|
||
prop('number', 'Energy', '0'),
|
||
prop('number', 'MaxEnergy', '3'),
|
||
prop('number', 'Turn', '0'),
|
||
prop('number', 'TweenEventId', '0'),
|
||
prop('number', 'CardHoverTweenId', '0'),
|
||
prop('any', 'EndTurnHandler'),
|
||
prop('any', 'NewGameHandler'),
|
||
prop('any', 'WarriorSelectHandler'),
|
||
prop('any', 'ThiefSelectHandler'),
|
||
prop('any', 'MageSelectHandler'),
|
||
prop('any', 'WarriorDeckHandler'),
|
||
prop('any', 'ThiefDeckHandler'),
|
||
prop('any', 'MageDeckHandler'),
|
||
prop('any', 'AscMinusHandler'),
|
||
prop('any', 'AscPlusHandler'),
|
||
prop('any', 'JobOpts'),
|
||
prop('any', 'Jobs'),
|
||
prop('number', 'AscensionLevel', '0'),
|
||
prop('number', 'AscensionUnlocked', '0'),
|
||
prop('any', 'StartGameHandler'),
|
||
prop('string', 'SelectedClass', '""'),
|
||
prop('any', 'DrawPileHandler'),
|
||
prop('any', 'DiscardPileHandler'),
|
||
prop('any', 'DeckInspectCloseHandler'),
|
||
prop('any', 'AllDeckHandler'),
|
||
prop('any', 'AllDeckCloseHandler'),
|
||
prop('number', 'SoulPoints', '0'),
|
||
prop('boolean', 'LobbyBound', 'false'),
|
||
prop('number', 'LobbyTpTries', '0'),
|
||
prop('boolean', 'CodexMode', 'false'),
|
||
prop('any', 'CodexCards'),
|
||
prop('boolean', 'ClassDeckMode', 'false'),
|
||
prop('any', 'ClassDeckCards'),
|
||
prop('string', 'ClassDeckTitle', '""'),
|
||
prop('any', 'SoulUnlocks'),
|
||
prop('any', 'SoulShopDef'),
|
||
prop('boolean', 'SoulShopBound', 'false'),
|
||
prop('string', 'DeckInspectKind', '""'),
|
||
prop('boolean', 'DeckAllOpen', 'false'),
|
||
prop('any', 'Cards'),
|
||
prop('any', 'CardFrames'),
|
||
prop('any', 'ClassToFrame'),
|
||
prop('number', 'PlayerHp', '0'),
|
||
prop('number', 'PlayerMaxHp', '80'),
|
||
prop('number', 'PlayerBlock', '0'),
|
||
prop('boolean', 'CombatOver', 'false'),
|
||
prop('any', 'Monsters'),
|
||
prop('any', 'Registered'),
|
||
prop('number', 'TargetIndex', '1'),
|
||
prop('any', 'RunDeck'),
|
||
prop('number', 'Gold', '0'),
|
||
prop('number', 'Floor', '0'),
|
||
prop('number', 'RunLength', String(RUN_LENGTH)),
|
||
prop('any', 'RewardChoices'),
|
||
prop('boolean', 'RunActive', 'false'),
|
||
prop('any', 'Enemies'),
|
||
prop('any', 'MapNodes'),
|
||
prop('any', 'MapStart'),
|
||
prop('string', 'CurrentNodeId', '""'),
|
||
prop('string', 'CurrentEnemyId', '""'),
|
||
prop('any', 'ShopChoices'),
|
||
prop('any', 'ShopBought'),
|
||
prop('any', 'Relics'),
|
||
prop('any', 'RunRelics'),
|
||
prop('any', 'RelicPool'),
|
||
prop('string', 'ShopRelic', '""'),
|
||
prop('boolean', 'ShopRelicBought', 'false'),
|
||
prop('number', 'DragSlot', '0'),
|
||
prop('boolean', 'FxBusy', 'false'),
|
||
prop('boolean', 'TurnBusy', 'false'),
|
||
prop('number', 'PlayerStr', '0'),
|
||
prop('number', 'PlayerWeak', '0'),
|
||
prop('number', 'PlayerVuln', '0'),
|
||
prop('any', 'PlayerPowers'),
|
||
prop('any', 'Potions'),
|
||
prop('any', 'RunPotions'),
|
||
prop('number', 'PotionSlots', String(POTIONS.baseSlots)),
|
||
prop('string', 'ShopPotion', '""'),
|
||
prop('boolean', 'ShopPotionBought', 'false'),
|
||
prop('number', 'FightAttackCount', '0'),
|
||
prop('boolean', 'FirstHpLossDone', 'false'),
|
||
prop('number', 'ClayBlockNext', '0'),
|
||
prop('number', 'PotionMenuSlot', '0'),
|
||
prop('number', 'Depth', '0'),
|
||
prop('any', 'VisitedNodes'),
|
||
prop('boolean', 'ChestOpened', 'false'),
|
||
prop('string', 'PlayerJob', '""'),
|
||
], [
|
||
method('OnBeginPlay', `${luaCardsTable(CARDS.cards)}
|
||
${luaFramesTable()}
|
||
${luaSoulShopTable(SOUL_UNLOCKS)}
|
||
self.SoulUnlocks = {}
|
||
self.SoulPoints = self.SoulPoints or 0
|
||
self:ShowLobby()
|
||
local lp = _UserService.LocalPlayer
|
||
if lp ~= nil then
|
||
self:ReqLoadAscension(lp.PlayerComponent.UserId)
|
||
self:ReqLoadSouls(lp.PlayerComponent.UserId)
|
||
end
|
||
_InputService:ConnectEvent(KeyDownEvent, function(e)
|
||
if e.key == KeyboardKey.LeftControl then
|
||
local lp2 = _UserService.LocalPlayer
|
||
if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then
|
||
self:PlayerAttackMotion()
|
||
end
|
||
end
|
||
end)`),
|
||
method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||
local errCode, value = ds:GetAndWait("ascensionUnlocked")
|
||
local n = 0
|
||
if errCode == 0 and value ~= nil and value ~= "" then
|
||
n = tonumber(value) or 0
|
||
end
|
||
self:RecvAscension(n, userId)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }], 5),
|
||
method('RecvAscension', `self.AscensionUnlocked = n
|
||
if self.AscensionLevel > self.AscensionUnlocked then
|
||
self.AscensionLevel = self.AscensionUnlocked
|
||
end
|
||
self:RenderAscension()`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' },
|
||
], 6),
|
||
method('SaveAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||
ds:SetAndWait("ascensionUnlocked", tostring(n))`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' },
|
||
], 5),
|
||
method('AdjustAscension', `local v = self.AscensionLevel + delta
|
||
if v < 0 then v = 0 end
|
||
if v > self.AscensionUnlocked then v = self.AscensionUnlocked end
|
||
self.AscensionLevel = v
|
||
self:RenderAscension()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delta' }]),
|
||
method('RenderAscension', `self:SetText("/ui/DefaultGroup/MainMenu/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))
|
||
self:SetText("/ui/DefaultGroup/LobbyHud/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`),
|
||
method('AscHpMult', `local m = 1
|
||
if self.AscensionLevel >= 1 then m = m + 0.1 end
|
||
if self.AscensionLevel >= 6 then m = m + 0.1 end
|
||
return m`, [], 0, 'number'),
|
||
method('AscAtkMult', `local m = 1
|
||
if self.AscensionLevel >= 2 then m = m + 0.1 end
|
||
if self.AscensionLevel >= 7 then m = m + 0.1 end
|
||
return m`, [], 0, 'number'),
|
||
method('AscEliteBonus', `local b = 0
|
||
if self.AscensionLevel >= 4 then b = b + 0.2 end
|
||
if self.AscensionLevel >= 9 then b = b + 0.2 end
|
||
return b`, [], 0, 'number'),
|
||
method('AscGoldMult', `local m = 1
|
||
if self.AscensionLevel >= 5 then m = m - 0.25 end
|
||
if self.AscensionLevel >= 10 then m = m - 0.25 end
|
||
return m`, [], 0, 'number'),
|
||
method('AscStartHpPenalty', `local p = 0
|
||
if self.AscensionLevel >= 3 then p = p + 10 end
|
||
if self.AscensionLevel >= 8 then p = p + 10 end
|
||
return p`, [], 0, 'number'),
|
||
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
|
||
method('ShowState', `self:HideGameHud()
|
||
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect")
|
||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", state == "lobby")
|
||
if state == "map" then
|
||
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true)
|
||
elseif state == "combat" then
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true)
|
||
elseif state == "shop" then
|
||
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true)
|
||
elseif state == "rest" then
|
||
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true)
|
||
elseif state == "treasure" then
|
||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true)
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
|
||
method('ShowMainMenu', `self.SelectedClass = ""
|
||
self:RenderAscension()
|
||
self:ShowState("menu")
|
||
self:SetText("/ui/DefaultGroup/MainMenu/Title", "메이플 덱 어드벤처")
|
||
self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱을 만들어 모험을 시작하세요")
|
||
self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임")
|
||
self:BindMenuButtons()`),
|
||
method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton")
|
||
if buttonEntity ~= nil and buttonEntity.ButtonComponent ~= nil then
|
||
if self.NewGameHandler ~= nil then
|
||
buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler)
|
||
self.NewGameHandler = nil
|
||
end
|
||
self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowCharacterSelect() end)
|
||
end
|
||
local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
|
||
if warrior ~= nil and warrior.ButtonComponent ~= nil then
|
||
if self.WarriorSelectHandler ~= nil then
|
||
warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler)
|
||
self.WarriorSelectHandler = nil
|
||
end
|
||
self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end)
|
||
end
|
||
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
|
||
if thief ~= nil and thief.ButtonComponent ~= nil then
|
||
if self.ThiefSelectHandler ~= nil then
|
||
thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler)
|
||
self.ThiefSelectHandler = nil
|
||
end
|
||
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") end)
|
||
end
|
||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
||
if mage ~= nil and mage.ButtonComponent ~= nil then
|
||
if self.MageSelectHandler ~= nil then
|
||
mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler)
|
||
self.MageSelectHandler = nil
|
||
end
|
||
self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end)
|
||
end
|
||
local warriorDeck = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorDeckButton")
|
||
if warriorDeck ~= nil and warriorDeck.ButtonComponent ~= nil then
|
||
if self.WarriorDeckHandler ~= nil then
|
||
warriorDeck:DisconnectEvent(ButtonClickEvent, self.WarriorDeckHandler)
|
||
self.WarriorDeckHandler = nil
|
||
end
|
||
self.WarriorDeckHandler = warriorDeck:ConnectEvent(ButtonClickEvent, function() self:OpenClassDeck("warrior") end)
|
||
end
|
||
local thiefDeck = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefDeckButton")
|
||
if thiefDeck ~= nil and thiefDeck.ButtonComponent ~= nil then
|
||
if self.ThiefDeckHandler ~= nil then
|
||
thiefDeck:DisconnectEvent(ButtonClickEvent, self.ThiefDeckHandler)
|
||
self.ThiefDeckHandler = nil
|
||
end
|
||
self.ThiefDeckHandler = thiefDeck:ConnectEvent(ButtonClickEvent, function() self:OpenClassDeck("bandit") end)
|
||
end
|
||
local mageDeck = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageDeckButton")
|
||
if mageDeck ~= nil and mageDeck.ButtonComponent ~= nil then
|
||
if self.MageDeckHandler ~= nil then
|
||
mageDeck:DisconnectEvent(ButtonClickEvent, self.MageDeckHandler)
|
||
self.MageDeckHandler = nil
|
||
end
|
||
self.MageDeckHandler = mageDeck:ConnectEvent(ButtonClickEvent, function() self:OpenClassDeck("magician") end)
|
||
end
|
||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
||
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
||
if self.AllDeckCloseHandler ~= nil then
|
||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||
self.AllDeckCloseHandler = nil
|
||
end
|
||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||
end
|
||
local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton")
|
||
if start ~= nil and start.ButtonComponent ~= nil then
|
||
if self.StartGameHandler ~= nil then
|
||
start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler)
|
||
self.StartGameHandler = nil
|
||
end
|
||
self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end)
|
||
end
|
||
local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus")
|
||
if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then
|
||
if self.AscMinusHandler ~= nil then
|
||
ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler)
|
||
self.AscMinusHandler = nil
|
||
end
|
||
self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end)
|
||
end
|
||
local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus")
|
||
if ascPlus ~= nil and ascPlus.ButtonComponent ~= nil then
|
||
if self.AscPlusHandler ~= nil then
|
||
ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler)
|
||
self.AscPlusHandler = nil
|
||
end
|
||
self.AscPlusHandler = ascPlus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(1) end)
|
||
end`),
|
||
method('ShowLobby', `self.SelectedClass = ""
|
||
self:RenderAscension()
|
||
self:RenderSoulLabel()
|
||
self:ShowState("lobby")
|
||
self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)
|
||
self:BindLobbyButtons()
|
||
self:BindMenuButtons()
|
||
self:GoLobbyMap()`),
|
||
method('GoLobbyMap', `self.LobbyTpTries = 0
|
||
local eventId = 0
|
||
local function go()
|
||
self.LobbyTpTries = self.LobbyTpTries + 1
|
||
local lp = _UserService.LocalPlayer
|
||
if lp ~= nil then
|
||
if lp.CurrentMapName ~= "${LOBBY_MAP}" then
|
||
_TeleportService:TeleportToMapPosition(lp, ${LOBBY_SPAWN}, "${LOBBY_MAP}")
|
||
end
|
||
_TimerService:ClearTimer(eventId)
|
||
elseif self.LobbyTpTries > 50 then
|
||
_TimerService:ClearTimer(eventId)
|
||
end
|
||
end
|
||
eventId = _TimerService:SetTimerRepeat(go, 0.1)`),
|
||
method('OnLobbyNpcInteract', `if self.RunActive == true then
|
||
return
|
||
end
|
||
if id == "run" then
|
||
self:ShowCharacterSelect()
|
||
elseif id == "codex" then
|
||
self:ShowCodex()
|
||
elseif id == "shop" then
|
||
self:ShowSoulShop()
|
||
elseif id == "board" then
|
||
self:ShowBoard()
|
||
end`, [{ Type: 'string', DefaultValue: '""', SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||
method('RenderSoulLabel', `local s = self.SoulPoints or 0
|
||
self:SetText("/ui/DefaultGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", s))
|
||
self:SetText("/ui/DefaultGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", s))`),
|
||
method('BindLobbyButtons', `if self.LobbyBound == true then
|
||
return
|
||
end
|
||
self.LobbyBound = true
|
||
local function bindClick(path, fn)
|
||
local e = _EntityService:GetEntityByPath(path)
|
||
if e ~= nil and e.ButtonComponent ~= nil then
|
||
e:ConnectEvent(ButtonClickEvent, fn)
|
||
end
|
||
end
|
||
bindClick("/ui/DefaultGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end)
|
||
bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end)
|
||
bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end)
|
||
bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`),
|
||
method('ShowCodex', `self.CodexMode = true
|
||
self.ClassDeckMode = false
|
||
local list = {}
|
||
for id, c in pairs(self.Cards) do
|
||
if c.curse ~= true then
|
||
table.insert(list, id)
|
||
end
|
||
end
|
||
table.sort(list)
|
||
self.CodexCards = list
|
||
local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
||
if close ~= nil and close.ButtonComponent ~= nil then
|
||
if self.AllDeckCloseHandler ~= nil then
|
||
close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||
end
|
||
self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||
end
|
||
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||
if hud ~= nil then
|
||
hud.Enable = true
|
||
end
|
||
self:RenderAllDeck()`),
|
||
method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`),
|
||
method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`),
|
||
method('ShowSoulShop', `self:RenderSoulLabel()
|
||
self:RenderSoulShop()
|
||
self:BindSoulShopButtons()
|
||
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`),
|
||
method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
|
||
method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||
local e1, pts = ds:GetAndWait("soulPoints")
|
||
local e2, unl = ds:GetAndWait("soulUnlocks")
|
||
local p = 0
|
||
if e1 == 0 and pts ~= nil and pts ~= "" then p = tonumber(pts) or 0 end
|
||
local u = ""
|
||
if e2 == 0 and unl ~= nil then u = unl end
|
||
self:RecvSouls(p, u, userId)`, [{ Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
|
||
method('RecvSouls', `self.SoulPoints = p
|
||
self.SoulUnlocks = {}
|
||
if u ~= nil and u ~= "" then
|
||
for key in string.gmatch(u, "([^,]+)") do
|
||
self.SoulUnlocks[key] = true
|
||
end
|
||
end
|
||
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 6),
|
||
method('SaveSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
|
||
ds:SetAndWait("soulPoints", tostring(p))
|
||
ds:SetAndWait("soulUnlocks", u)`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
|
||
method('SerializeUnlocks', `local parts = {}
|
||
if self.SoulUnlocks ~= nil then
|
||
for k, v in pairs(self.SoulUnlocks) do
|
||
if v == true then table.insert(parts, k) end
|
||
end
|
||
end
|
||
return table.concat(parts, ",")`, [], 0, 'string'),
|
||
method('AwardSouls', `self.SoulPoints = (self.SoulPoints or 0) + n
|
||
local lp = _UserService.LocalPlayer
|
||
if lp ~= nil then
|
||
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
|
||
end
|
||
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]),
|
||
method('BuySoulUnlock', `local d = nil
|
||
if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end
|
||
if d == nil then return end
|
||
if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then
|
||
self:Toast("이미 보유 중입니다")
|
||
return
|
||
end
|
||
if (self.SoulPoints or 0) < d.cost then
|
||
self:Toast("영혼이 부족합니다")
|
||
return
|
||
end
|
||
self.SoulPoints = self.SoulPoints - d.cost
|
||
if self.SoulUnlocks == nil then self.SoulUnlocks = {} end
|
||
self.SoulUnlocks[d.key] = true
|
||
local lp = _UserService.LocalPlayer
|
||
if lp ~= nil then
|
||
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
|
||
end
|
||
self:Toast(d.name .. " 해금!")
|
||
self:RenderSoulLabel()
|
||
self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]),
|
||
method('RenderSoulShop', `local defs = self.SoulShopDef or {}
|
||
for i = 1, 4 do
|
||
local base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)
|
||
local d = defs[i]
|
||
if d == nil then
|
||
self:SetEntityEnabled(base, false)
|
||
else
|
||
self:SetEntityEnabled(base, true)
|
||
self:SetText(base .. "/Name", d.name)
|
||
self:SetText(base .. "/Desc", d.desc)
|
||
local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true
|
||
if owned then
|
||
self:SetText(base .. "/Status", "보유 중")
|
||
elseif (self.SoulPoints or 0) >= d.cost then
|
||
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매")
|
||
else
|
||
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족")
|
||
end
|
||
end
|
||
end`),
|
||
method('BindSoulShopButtons', `if self.SoulShopBound == true then
|
||
return
|
||
end
|
||
self.SoulShopBound = true
|
||
for i = 1, 4 do
|
||
local idx = i
|
||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i))
|
||
if e ~= nil and e.ButtonComponent ~= nil then
|
||
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
|
||
end
|
||
end`),
|
||
method('ApplySoulUnlocks', `if self.SoulUnlocks == nil then return end
|
||
if self.SoulUnlocks["meso"] == true then self.Gold = self.Gold + 60 end
|
||
if self.SoulUnlocks["hp"] == true then
|
||
self.PlayerMaxHp = self.PlayerMaxHp + 15
|
||
self.PlayerHp = self.PlayerMaxHp
|
||
end
|
||
if self.SoulUnlocks["trim"] == true then
|
||
for i = 1, #self.RunDeck do
|
||
local cid = self.RunDeck[i]
|
||
if cid == "Defend" or cid == "MagicGuard" or cid == "DarkSight" then
|
||
table.remove(self.RunDeck, i)
|
||
break
|
||
end
|
||
end
|
||
end
|
||
if self.SoulUnlocks["relic"] == true then
|
||
local nid = self:PickNewRelic()
|
||
if nid ~= "" then self:AddRelic(nid) end
|
||
end`),
|
||
method('ShowCharacterSelect', `self.SelectedClass = ""
|
||
self:ShowState("charselect")
|
||
self:RenderCharacterSelect()`),
|
||
method('SelectClass', `self.SelectedClass = className
|
||
self:RenderCharacterSelect()`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' },
|
||
]),
|
||
method('RenderCharacterSelect', `local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton")
|
||
if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then
|
||
if self.SelectedClass == "warrior" then
|
||
warrior.SpriteGUIRendererComponent.Color = Color(0.28, 0.36, 0.46, 1)
|
||
else
|
||
warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||
end
|
||
end
|
||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
||
if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then
|
||
if self.SelectedClass == "magician" then
|
||
mage.SpriteGUIRendererComponent.Color = Color(0.28, 0.36, 0.46, 1)
|
||
else
|
||
mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||
end
|
||
end
|
||
local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton")
|
||
if thief ~= nil and thief.SpriteGUIRendererComponent ~= nil then
|
||
if self.SelectedClass == "bandit" then
|
||
thief.SpriteGUIRendererComponent.Color = Color(0.28, 0.36, 0.46, 1)
|
||
else
|
||
thief.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||
end
|
||
end
|
||
if self.SelectedClass == "warrior" then
|
||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨")
|
||
elseif self.SelectedClass == "bandit" then
|
||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "도적 선택됨")
|
||
elseif self.SelectedClass == "magician" then
|
||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "마법사 선택됨")
|
||
else
|
||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요")
|
||
end`),
|
||
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "bandit" and self.SelectedClass ~= "magician" then
|
||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요")
|
||
return
|
||
end
|
||
self:StartRun()`),
|
||
method('SetEntityEnabled', `local e = _EntityService:GetEntityByPath(path)
|
||
if e ~= nil then
|
||
e.Enable = enabled
|
||
end`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
|
||
]),
|
||
method('StartRun', `if self.SelectedClass == "magician" then
|
||
self.PlayerMaxHp = ${CLASSES.magician.maxHp}
|
||
self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} }
|
||
elseif self.SelectedClass == "bandit" then
|
||
self.PlayerMaxHp = ${CLASSES.bandit.maxHp}
|
||
self.RunDeck = { ${CARDS.starterDecks.bandit.map(luaStr).join(', ')} }
|
||
else
|
||
self.PlayerMaxHp = ${CLASSES.warrior.maxHp}
|
||
self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} }
|
||
end
|
||
self.PlayerMaxHp = self.PlayerMaxHp - self:AscStartHpPenalty()
|
||
self.PlayerHp = self.PlayerMaxHp
|
||
self.Gold = 0
|
||
self.Floor = 1
|
||
self.RunLength = ${ACT_COUNT}
|
||
self.RunActive = true
|
||
self.RunRelics = {}
|
||
self.RunPotions = {}
|
||
self.PotionSlots = ${POTIONS.baseSlots}
|
||
${luaPotionsTable(POTIONS.potions)}
|
||
${luaRelicsTable(RELICS.relics)}
|
||
self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} }
|
||
${luaEnemiesTable(ENEMIES.enemies)}
|
||
self.CurrentNodeId = ""
|
||
self.CurrentEnemyId = ""
|
||
self.PlayerJob = ""
|
||
${luaJobsTable(JOBS)}
|
||
${luaFramesTable()}
|
||
self:GenerateMap()
|
||
self:BindButtons()
|
||
self:AddRelic("${RELICS.startingRelic}")
|
||
self:ApplySoulUnlocks()
|
||
self:RenderPotions()
|
||
self:TeleportToActMap()
|
||
self:ShowMap()`),
|
||
method('StartCombat', `self:ShowState("combat")
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)
|
||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||
self.MaxEnergy = 3
|
||
self.Turn = 0
|
||
self.PlayerBlock = 0
|
||
self.PlayerStr = 0
|
||
self.PlayerWeak = 0
|
||
self.PlayerVuln = 0
|
||
self.PlayerPowers = {}
|
||
self.FightAttackCount = 0
|
||
self.FirstHpLossDone = false
|
||
self.ClayBlockNext = 0
|
||
self.CombatOver = false
|
||
self.DiscardPile = {}
|
||
self.Hand = {}
|
||
${luaCardsTable(CARDS.cards)}
|
||
self.DrawPile = {}
|
||
for i = 1, #self.RunDeck do
|
||
self.DrawPile[i] = self.RunDeck[i]
|
||
end
|
||
self:Shuffle(self.DrawPile)
|
||
self:BuildMonsters()
|
||
self:RenderCombat()
|
||
self:StartPlayerTurn()
|
||
self:ApplyRelics("combatStart")
|
||
self:RenderCombat()`),
|
||
method('RegisterMonster', `if self.Registered == nil then
|
||
self.Registered = {}
|
||
end
|
||
local g = group
|
||
if g == nil or g == "" then g = "combat" end
|
||
local mp = mapName
|
||
if mp == nil then mp = "" end
|
||
table.insert(self.Registered, { entity = monster, enemyId = enemyId, group = g, map = mp })`, [
|
||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'group' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'mapName' },
|
||
]),
|
||
method('BuildMonsters', `self.Monsters = {}
|
||
local g = "combat"
|
||
local node = self.MapNodes[self.CurrentNodeId]
|
||
if node ~= nil and node.type ~= nil then g = node.type end
|
||
local pmap = ""
|
||
local lp = _UserService.LocalPlayer
|
||
if lp ~= nil and lp.CurrentMapName ~= nil then pmap = lp.CurrentMapName end
|
||
local reg = self.Registered or {}
|
||
for i = 1, #reg do
|
||
if reg[i].entity ~= nil and isvalid(reg[i].entity) then
|
||
reg[i].entity:SetVisible(false)
|
||
end
|
||
end
|
||
local byGroup = {}
|
||
for i = 1, #reg do
|
||
local r = reg[i]
|
||
if r.entity ~= nil and isvalid(r.entity) and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then
|
||
local gg = r.group
|
||
if gg == nil or gg == "" then gg = "combat" end
|
||
if byGroup[gg] == nil then byGroup[gg] = {} end
|
||
local x = 0
|
||
if r.entity.TransformComponent ~= nil then
|
||
x = r.entity.TransformComponent.WorldPosition.x
|
||
end
|
||
table.insert(byGroup[gg], { entity = r.entity, enemyId = r.enemyId, x = x })
|
||
end
|
||
end
|
||
-- 노드 타입별 랜덤 구성: 일반 1~3 / 엘리트 1+일반0~2 / 보스 1
|
||
local chosen = {}
|
||
local function takeFrom(key, k)
|
||
local src = byGroup[key] or {}
|
||
local pool = {}
|
||
for i = 1, #src do pool[i] = src[i] end
|
||
self:Shuffle(pool)
|
||
local taken = 0
|
||
for i = 1, #pool do
|
||
if taken >= k then break end
|
||
table.insert(chosen, pool[i])
|
||
taken = taken + 1
|
||
end
|
||
end
|
||
if g == "boss" then
|
||
takeFrom("boss", 1)
|
||
elseif g == "elite" then
|
||
takeFrom("elite", 1)
|
||
takeFrom("combat", math.random(0, 2))
|
||
else
|
||
takeFrom("combat", math.random(1, 3))
|
||
end
|
||
if #chosen == 0 then takeFrom(g, 1) end
|
||
if #chosen == 0 then takeFrom("combat", 1) end
|
||
table.sort(chosen, function(a, b) return a.x < b.x end)
|
||
local mult = 1 + (self.Floor - 1) * 0.45
|
||
if g == "elite" or g == "boss" then
|
||
mult = mult + self:AscEliteBonus()
|
||
end
|
||
local n = #chosen
|
||
if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end
|
||
for i = 1, n do
|
||
local item = chosen[i]
|
||
local e = self.Enemies[item.enemyId]
|
||
if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end
|
||
local intents = {}
|
||
for k = 1, #e.intents do
|
||
local v = e.intents[k].value or 0
|
||
if e.intents[k].kind == "Attack" then
|
||
v = math.floor(v * mult * self:AscAtkMult())
|
||
elseif e.intents[k].kind ~= "Debuff" then
|
||
v = math.floor(v * mult)
|
||
end
|
||
intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect, card = e.intents[k].card, count = e.intents[k].count }
|
||
end
|
||
local maxHp = math.floor(e.maxHp * mult * self:AscHpMult())
|
||
local hitClip = nil
|
||
local standClip = nil
|
||
if item.entity.StateAnimationComponent ~= nil then
|
||
pcall(function()
|
||
hitClip = item.entity.StateAnimationComponent.ActionSheet["hit"]
|
||
standClip = item.entity.StateAnimationComponent.ActionSheet["stand"]
|
||
end)
|
||
end
|
||
local startIdx = 1
|
||
if #intents > 0 then startIdx = math.random(1, #intents) end
|
||
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
||
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0,
|
||
hitClip = hitClip, standClip = standClip, motionBusy = false,
|
||
intents = intents, intentIdx = startIdx, alive = true, slot = i }
|
||
self:ReviveMonsterEntity(item.entity)
|
||
self:PositionMonsterSlot(i)
|
||
end
|
||
self.TargetIndex = 1`),
|
||
method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then
|
||
return
|
||
end
|
||
monster:SetEnable(true)
|
||
monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]),
|
||
method('Shuffle', `if list == nil then
|
||
\treturn
|
||
end
|
||
for i = #list, 2, -1 do
|
||
\tlocal j = math.random(1, i)
|
||
\tlist[i], list[j] = list[j], list[i]
|
||
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]),
|
||
method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton")
|
||
if endTurn ~= nil and endTurn.ButtonComponent ~= nil then
|
||
if self.EndTurnHandler ~= nil then
|
||
endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
|
||
self.EndTurnHandler = nil
|
||
end
|
||
self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
|
||
end
|
||
local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile")
|
||
if drawPile ~= nil and drawPile.ButtonComponent ~= nil then
|
||
if self.DrawPileHandler ~= nil then
|
||
drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)
|
||
self.DrawPileHandler = nil
|
||
end
|
||
self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end)
|
||
end
|
||
local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile")
|
||
if discardPile ~= nil and discardPile.ButtonComponent ~= nil then
|
||
if self.DiscardPileHandler ~= nil then
|
||
discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)
|
||
self.DiscardPileHandler = nil
|
||
end
|
||
self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end)
|
||
end
|
||
local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close")
|
||
if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then
|
||
if self.DeckInspectCloseHandler ~= nil then
|
||
inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)
|
||
self.DeckInspectCloseHandler = nil
|
||
end
|
||
self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)
|
||
end
|
||
local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton")
|
||
if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then
|
||
if self.AllDeckHandler ~= nil then
|
||
allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)
|
||
self.AllDeckHandler = nil
|
||
end
|
||
self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)
|
||
end
|
||
local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
|
||
if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then
|
||
if self.AllDeckCloseHandler ~= nil then
|
||
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
|
||
self.AllDeckCloseHandler = nil
|
||
end
|
||
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
|
||
end
|
||
for i = 1, 10 do
|
||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
|
||
local cardPath = "/ui/DefaultGroup/CardHand/Card" .. tostring(i)
|
||
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
|
||
cardEntity:ConnectEvent(UITouchDragEvent, function(ev) self:OnCardDrag(i, ev.TouchPoint) end)
|
||
cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(i, ev.TouchPoint) end)
|
||
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:HoverCard(i) end)
|
||
cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end)
|
||
end
|
||
end
|
||
for i = 1, 3 do
|
||
local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i))
|
||
if rc ~= nil and rc.ButtonComponent ~= nil then
|
||
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
|
||
if rc.UITouchReceiveComponent ~= nil then
|
||
local cardPath = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(i)
|
||
rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||
rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||
end
|
||
end
|
||
end
|
||
local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip")
|
||
if skip ~= nil and skip.ButtonComponent ~= nil then
|
||
skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)
|
||
end
|
||
local mapNodeIds = {}
|
||
for r = 1, ${MAP_ROWS} do
|
||
for c = 1, ${MAP_COLS} do
|
||
table.insert(mapNodeIds, "r" .. tostring(r) .. "c" .. tostring(c))
|
||
end
|
||
end
|
||
table.insert(mapNodeIds, "boss")
|
||
for i = 1, #mapNodeIds do
|
||
local nid = mapNodeIds[i]
|
||
local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid)
|
||
if mn ~= nil and mn.ButtonComponent ~= nil then
|
||
mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)
|
||
end
|
||
end
|
||
for i = 1, 3 do
|
||
local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i))
|
||
if sc ~= nil and sc.ButtonComponent ~= nil then
|
||
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
|
||
if sc.UITouchReceiveComponent ~= nil then
|
||
local cardPath = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
||
sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
|
||
sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
|
||
end
|
||
end
|
||
end
|
||
local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave")
|
||
if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then
|
||
shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||
end
|
||
local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
||
if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then
|
||
shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)
|
||
end
|
||
local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave")
|
||
if restLeave ~= nil and restLeave.ButtonComponent ~= nil then
|
||
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||
end
|
||
for i = 1, ${MAX_MONSTERS} do
|
||
local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i))
|
||
if ms ~= nil and ms.ButtonComponent ~= nil then
|
||
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
|
||
end
|
||
end
|
||
for i = 1, 10 do
|
||
local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i))
|
||
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
|
||
local idx = i
|
||
rs:ConnectEvent(UITouchEnterEvent, function()
|
||
local rid = nil
|
||
if self.RunRelics ~= nil then rid = self.RunRelics[idx] end
|
||
if rid ~= nil and self.Relics[rid] ~= nil then
|
||
self:ShowTooltip(self.Relics[rid].name, self.Relics[rid].desc, -240 + (idx - 1) * 48)
|
||
end
|
||
end)
|
||
rs:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
|
||
end
|
||
end
|
||
for i = 1, 5 do
|
||
local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i))
|
||
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
|
||
local idx = i
|
||
ps:ConnectEvent(UITouchEnterEvent, function()
|
||
local pid = nil
|
||
if self.RunPotions ~= nil then pid = self.RunPotions[idx] end
|
||
if pid ~= nil and self.Potions[pid] ~= nil then
|
||
self:ShowTooltip(self.Potions[pid].name, self.Potions[pid].desc, 240 + (idx - 1) * 44)
|
||
end
|
||
end)
|
||
ps:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end)
|
||
ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end)
|
||
end
|
||
end
|
||
local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use")
|
||
if pmUse ~= nil and pmUse.ButtonComponent ~= nil then
|
||
pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end)
|
||
end
|
||
local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss")
|
||
if pmToss ~= nil and pmToss.ButtonComponent ~= nil then
|
||
pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end)
|
||
end
|
||
local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close")
|
||
if pmClose ~= nil and pmClose.ButtonComponent ~= nil then
|
||
pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end)
|
||
end
|
||
local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
||
if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then
|
||
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
|
||
end
|
||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||
if chest ~= nil and chest.ButtonComponent ~= nil then
|
||
chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end)
|
||
end
|
||
local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave")
|
||
if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then
|
||
treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||
end
|
||
local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton")
|
||
if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then
|
||
jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end)
|
||
end
|
||
local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton")
|
||
if jcJob ~= nil and jcJob.ButtonComponent ~= nil then
|
||
jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end)
|
||
end
|
||
for i = 1, 3 do
|
||
local slotIdx = i
|
||
local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i))
|
||
if jb ~= nil and jb.ButtonComponent ~= nil then
|
||
jb:ConnectEvent(ButtonClickEvent, function()
|
||
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
|
||
self:SetJob(self.JobOpts[slotIdx].id)
|
||
end
|
||
end)
|
||
end
|
||
end`),
|
||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||
self.Energy = self.MaxEnergy
|
||
self:ApplyRelics("turnStart")
|
||
self.PlayerBlock = 0
|
||
if self.ClayBlockNext > 0 then
|
||
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
|
||
self.ClayBlockNext = 0
|
||
end
|
||
if self.PlayerPowers ~= nil then
|
||
for i = 1, #self.PlayerPowers do
|
||
local pc = self.Cards[self.PlayerPowers[i]]
|
||
if pc ~= nil then
|
||
if pc.powerEffect == "strengthPerTurn" then
|
||
self.PlayerStr = self.PlayerStr + pc.value
|
||
elseif pc.powerEffect == "energyPerTurn" then
|
||
self.Energy = self.Energy + pc.value
|
||
elseif pc.powerEffect == "blockPerTurn" then
|
||
self.PlayerBlock = self.PlayerBlock + pc.value
|
||
end
|
||
end
|
||
end
|
||
end
|
||
self:DrawCards(5)
|
||
self:RenderHand(true)
|
||
self:RenderCombat()`),
|
||
method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||
return
|
||
end
|
||
local burn = 0
|
||
for bi = 1, #self.Hand do
|
||
\tlocal hc = self.Cards[self.Hand[bi]]
|
||
\tif hc ~= nil and hc.endTurnDamage ~= nil then burn = burn + hc.endTurnDamage end
|
||
end
|
||
if burn > 0 then
|
||
\tself.PlayerHp = self.PlayerHp - burn
|
||
\tif self.PlayerHp < 0 then self.PlayerHp = 0 end
|
||
\tself:ShowPlayerDmgPop(burn)
|
||
\tself:RenderCombat()
|
||
end
|
||
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', `local drawnSlots = {}
|
||
for i = 1, amount do
|
||
\tif #self.DrawPile <= 0 then
|
||
\t\tself:RecycleDiscardIntoDraw()
|
||
\tend
|
||
\tif #self.DrawPile <= 0 then
|
||
\t\tbreak
|
||
\tend
|
||
\tlocal cardId = table.remove(self.DrawPile)
|
||
\tif #self.Hand >= 10 then
|
||
\t\ttable.insert(self.DiscardPile, cardId)
|
||
\telse
|
||
\t\ttable.insert(self.Hand, cardId)
|
||
\t\tif #self.Hand <= 5 then
|
||
\t\t\ttable.insert(drawnSlots, #self.Hand)
|
||
\t\tend
|
||
\tend
|
||
end
|
||
self:RenderPiles()
|
||
if animate == true and #drawnSlots > 0 then
|
||
\tself:RenderHand(false)
|
||
\tlocal drawStart = Vector2(-590, 8)
|
||
\tfor i = 1, #drawnSlots do
|
||
\t\tlocal slot = drawnSlots[i]
|
||
\t\tself:AnimateCardFrom(slot, drawStart, Vector2((slot - 3) * 200, 0), 0.08 + i * 0.045)
|
||
\tend
|
||
end`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
|
||
]),
|
||
method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then
|
||
\treturn
|
||
end
|
||
self.DrawPile = {}
|
||
for i = 1, #self.DiscardPile do
|
||
\tself.DrawPile[i] = self.DiscardPile[i]
|
||
end
|
||
self.DiscardPile = {}
|
||
self:Shuffle(self.DrawPile)`),
|
||
method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", tostring(#self.DrawPile))
|
||
self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", tostring(#self.DiscardPile))
|
||
self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
|
||
local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||
if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then
|
||
self:OpenDeckInspect(self.DeckInspectKind)
|
||
end`),
|
||
method('OpenDeckInspect', `self.DeckInspectKind = kind
|
||
if self.DeckAllOpen == true then
|
||
self.DeckAllOpen = false
|
||
local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||
if allHud ~= nil then
|
||
allHud.Enable = false
|
||
end
|
||
end
|
||
local pile = {}
|
||
local title = ""
|
||
if kind == "discard" then
|
||
pile = self.DiscardPile or {}
|
||
title = "버린 덱"
|
||
else
|
||
pile = self.DrawPile or {}
|
||
title = "뽑을 덱"
|
||
end
|
||
self:RenderDeckInspect(pile, title)
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||
if hud ~= nil then
|
||
hud.Enable = true
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||
method('CloseDeckInspect', `self.DeckInspectKind = ""
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||
if hud ~= nil then
|
||
hud.Enable = false
|
||
end`),
|
||
method('RenderDeckInspect', `local count = 0
|
||
if pile ~= nil then
|
||
count = #pile
|
||
end
|
||
local suffix = " (" .. tostring(count) .. ")"
|
||
if count > 60 then
|
||
suffix = suffix .. " - 60장까지 표시"
|
||
end
|
||
self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix)
|
||
local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty")
|
||
if empty ~= nil then
|
||
empty.Enable = count <= 0
|
||
end
|
||
for i = 1, 60 do
|
||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i))
|
||
if e ~= nil then
|
||
local cardId = nil
|
||
if pile ~= nil then
|
||
cardId = pile[i]
|
||
end
|
||
if cardId == nil then
|
||
e.Enable = false
|
||
else
|
||
e.Enable = true
|
||
self:ApplyInspectCardVisual(i, cardId)
|
||
end
|
||
end
|
||
end`, [
|
||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
|
||
]),
|
||
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||
]),
|
||
method('OpenClassDeck', `self.CodexMode = false
|
||
self.ClassDeckMode = true
|
||
self.ClassDeckCards = {}
|
||
self.ClassDeckTitle = "직업 덱"
|
||
local allowed = {}
|
||
if className == "warrior" then
|
||
allowed["warrior"] = true
|
||
allowed["fighter"] = true
|
||
allowed["page"] = true
|
||
allowed["spearman"] = true
|
||
self.ClassDeckTitle = "전사 전체 덱"
|
||
elseif className == "magician" then
|
||
allowed["magician"] = true
|
||
allowed["firepoison"] = true
|
||
allowed["icelightning"] = true
|
||
allowed["cleric"] = true
|
||
self.ClassDeckTitle = "마법사 전체 덱"
|
||
else
|
||
allowed["bandit"] = true
|
||
allowed["shiv"] = true
|
||
allowed["poisoner"] = true
|
||
allowed["trickster"] = true
|
||
self.ClassDeckTitle = "도적 전체 덱"
|
||
end
|
||
for id, c in pairs(self.Cards) do
|
||
if c ~= nil and c.curse ~= true and allowed[c.class] == true then
|
||
table.insert(self.ClassDeckCards, id)
|
||
end
|
||
end
|
||
table.sort(self.ClassDeckCards, function(a, b)
|
||
local ca = self.Cards[a]
|
||
local cb = self.Cards[b]
|
||
local na = a
|
||
local nb = b
|
||
if ca ~= nil and ca.name ~= nil then na = ca.name end
|
||
if cb ~= nil and cb.name ~= nil then nb = cb.name end
|
||
if na == nb then return a < b end
|
||
return na < nb
|
||
end)
|
||
self.DeckAllOpen = true
|
||
self:RenderAllDeck()
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||
if hud ~= nil then
|
||
hud.Enable = true
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
|
||
method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud")
|
||
if inspectHud ~= nil then
|
||
inspectHud.Enable = false
|
||
end
|
||
self.DeckInspectKind = ""
|
||
self.ClassDeckMode = false
|
||
self.DeckAllOpen = true
|
||
self:RenderAllDeck()
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||
if hud ~= nil then
|
||
hud.Enable = true
|
||
end`),
|
||
method('CloseAllDeck', `self.DeckAllOpen = false
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
|
||
if hud ~= nil then
|
||
hud.Enable = false
|
||
end
|
||
if self.ClassDeckMode == true then
|
||
self.ClassDeckMode = false
|
||
self.ClassDeckCards = {}
|
||
self.ClassDeckTitle = ""
|
||
end
|
||
if self.CodexMode == true then
|
||
self.CodexMode = false
|
||
self:ShowLobby()
|
||
end`),
|
||
method('RenderAllDeck', `local pile = self.RunDeck or {}
|
||
local title = "모든 덱"
|
||
if self.ClassDeckMode == true then
|
||
pile = self.ClassDeckCards or {}
|
||
title = self.ClassDeckTitle
|
||
elseif self.CodexMode == true then
|
||
pile = self.CodexCards or {}
|
||
title = "카드 도감"
|
||
end
|
||
local count = #pile
|
||
self:SetText("/ui/DefaultGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")")
|
||
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 n = #self.Hand
|
||
local spacing = 175
|
||
if n > 8 then spacing = math.floor(1400 / n) end
|
||
local startX = -((n - 1) * spacing) / 2
|
||
local drawStart = Vector2(-590, 8)
|
||
for i = 1, 10 do
|
||
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||
\tif cardEntity ~= nil then
|
||
\t\tlocal cardId = self.Hand[i]
|
||
\t\tif cardId == nil then
|
||
\t\t\tcardEntity.Enable = false
|
||
\t\telse
|
||
\t\t\tcardEntity.Enable = true
|
||
\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end
|
||
\t\t\tself:ApplyCardVisual(i, cardId)
|
||
\t\t\tlocal tx = startX + (i - 1) * spacing
|
||
\t\t\tif animate == true then
|
||
\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03)
|
||
\t\t\telse
|
||
\t\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.anchoredPosition = Vector2(tx, 0) end
|
||
\t\t\tend
|
||
\t\tend
|
||
\tend
|
||
end
|
||
self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]),
|
||
method('ApplyCardFace', `local c = self.Cards[cardId]
|
||
if c == nil then
|
||
c = { name = cardId, cost = 0, desc = "", kind = "Skill", class = "warrior", rarity = "normal" }
|
||
end
|
||
local e = _EntityService:GetEntityByPath(base)
|
||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||
if e.UITransformComponent ~= nil then
|
||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||
end
|
||
local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"]
|
||
local ruid = nil
|
||
if frames ~= nil then
|
||
ruid = frames[c.rarity or "normal"]
|
||
end
|
||
if ruid ~= nil then
|
||
e.SpriteGUIRendererComponent.ImageRUID = ruid
|
||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||
end
|
||
end
|
||
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||
self:SetText(base .. "/Name", c.name)
|
||
self:SetText(base .. "/Desc", c.desc)
|
||
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
||
if art ~= nil then
|
||
if c.image ~= nil and c.image ~= "" then
|
||
art.Enable = true
|
||
if art.SpriteGUIRendererComponent ~= nil then
|
||
art.SpriteGUIRendererComponent.ImageRUID = c.image
|
||
end
|
||
else
|
||
art.Enable = false
|
||
end
|
||
end`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||
]),
|
||
method('SetCardHover', `local prefix = ""
|
||
local count = 0
|
||
local xs = {}
|
||
local baseY = 0
|
||
local hoverIndex = 0
|
||
local push = 110
|
||
if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then
|
||
if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||
return
|
||
end
|
||
prefix = "/ui/DefaultGroup/CardHand/Card"
|
||
count = 5
|
||
xs = { ${CARD_XS.join(', ')} }
|
||
baseY = 0
|
||
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
||
elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then
|
||
prefix = "/ui/DefaultGroup/RewardHud/Reward"
|
||
count = 3
|
||
xs = { -300, 0, 300 }
|
||
baseY = 0
|
||
hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0
|
||
elseif string.find(path, "/ui/DefaultGroup/ShopHud/Card") == 1 then
|
||
prefix = "/ui/DefaultGroup/ShopHud/Card"
|
||
count = 3
|
||
xs = { -300, 0, 300 }
|
||
baseY = 20
|
||
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
|
||
end
|
||
if count <= 0 then
|
||
return
|
||
end
|
||
if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
|
||
_TimerService:ClearTimer(self.CardHoverTweenId)
|
||
self.CardHoverTweenId = 0
|
||
end
|
||
local items = {}
|
||
for i = 1, count do
|
||
local e = _EntityService:GetEntityByPath(prefix .. tostring(i))
|
||
if e ~= nil and e.UITransformComponent ~= nil then
|
||
local tr = e.UITransformComponent
|
||
local tx = xs[i]
|
||
local ty = baseY
|
||
local sc = 1
|
||
if hover == true and hoverIndex > 0 then
|
||
if i == hoverIndex and e.Enable == true then
|
||
sc = 1.5
|
||
elseif i < hoverIndex then
|
||
tx = tx - push
|
||
elseif i > hoverIndex then
|
||
tx = tx + push
|
||
end
|
||
end
|
||
table.insert(items, { tr = tr, sx = tr.anchoredPosition.x, sy = tr.anchoredPosition.y, ss = tr.UIScale.x, tx = tx, ty = ty, ts = sc })
|
||
end
|
||
end
|
||
local elapsed = 0
|
||
local duration = 0.12
|
||
local eventId = 0
|
||
eventId = _TimerService:SetTimerRepeat(function()
|
||
elapsed = elapsed + 1 / 60
|
||
local t = math.min(elapsed / duration, 1)
|
||
local eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)
|
||
for i = 1, #items do
|
||
local it = items[i]
|
||
local x = it.sx + (it.tx - it.sx) * eased
|
||
local y = it.sy + (it.ty - it.sy) * eased
|
||
local s = it.ss + (it.ts - it.ss) * eased
|
||
it.tr.anchoredPosition = Vector2(x, y)
|
||
it.tr.UIScale = Vector3(s, s, 1)
|
||
end
|
||
if t >= 1 then
|
||
_TimerService:ClearTimer(eventId)
|
||
if self.CardHoverTweenId == eventId then
|
||
self.CardHoverTweenId = 0
|
||
end
|
||
end
|
||
end, 1 / 60)
|
||
self.CardHoverTweenId = eventId`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' },
|
||
]),
|
||
method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||
]),
|
||
method('SetText', `local entity = _EntityService:GetEntityByPath(path)
|
||
if entity ~= nil and entity.TextComponent ~= nil then
|
||
\tentity.TextComponent.Text = value
|
||
end`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' },
|
||
]),
|
||
method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||
if cardEntity == nil or cardEntity.UITransformComponent == nil then
|
||
\treturn
|
||
end
|
||
local tr = cardEntity.UITransformComponent
|
||
tr.anchoredPosition = fromPos
|
||
local elapsed = 0
|
||
local eventId = 0
|
||
eventId = _TimerService:SetTimerRepeat(function()
|
||
\telapsed = elapsed + 1 / 60
|
||
\tlocal t = math.min(elapsed / duration, 1)
|
||
\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)
|
||
\ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased)
|
||
\tif t >= 1 then
|
||
\t\t_TimerService:ClearTimer(eventId)
|
||
\tend
|
||
end, 1 / 60)`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' },
|
||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
|
||
]),
|
||
method('CalcPlayerAttack', `local base2 = base
|
||
self.FightAttackCount = self.FightAttackCount + 1
|
||
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
|
||
base2 = base2 + 8
|
||
end
|
||
local dmg = base2 + self.PlayerStr
|
||
if self:HasRelic("penNib") and self.FightAttackCount % 10 == 0 then
|
||
dmg = dmg * 2
|
||
end
|
||
if self.PlayerWeak > 0 then
|
||
dmg = math.floor(dmg * 0.75)
|
||
end
|
||
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
|
||
dmg = 5
|
||
end
|
||
if dmg < 0 then
|
||
dmg = 0
|
||
end
|
||
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||
method('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 c.unplayable == true then
|
||
self:Toast("사용할 수 없는 카드입니다")
|
||
return
|
||
end
|
||
if self.Energy < c.cost then
|
||
self:Toast("에너지가 부족합니다")
|
||
return
|
||
end
|
||
self.Energy = self.Energy - c.cost
|
||
if c.kind == "Attack" then
|
||
if c.damage ~= nil then
|
||
self:PlayerAttackMotion()
|
||
local total = 0
|
||
local hitN = c.hits or 1
|
||
for h = 1, hitN do
|
||
total = total + self:CalcPlayerAttack(c.damage)
|
||
end
|
||
if c.aoe == true then
|
||
self:PlayAoeFx(c.fx or c.image, total)
|
||
else
|
||
self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true)
|
||
end
|
||
end
|
||
if c.block ~= nil then
|
||
self.PlayerBlock = self.PlayerBlock + c.block
|
||
end
|
||
self:ApplyRelics("cardPlayed")
|
||
elseif c.kind == "Skill" then
|
||
if c.block ~= nil then
|
||
self.PlayerBlock = self.PlayerBlock + c.block
|
||
end
|
||
elseif c.kind == "Power" then
|
||
if c.powerEffect ~= nil then
|
||
table.insert(self.PlayerPowers, cardId)
|
||
end
|
||
end
|
||
if c.strength ~= nil then
|
||
self.PlayerStr = self.PlayerStr + c.strength
|
||
end
|
||
if c.selfVuln ~= nil then
|
||
self.PlayerVuln = self.PlayerVuln + c.selfVuln
|
||
end
|
||
if c.heal ~= nil then
|
||
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
||
end
|
||
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
|
||
local tm = self.Monsters[self.TargetIndex]
|
||
if tm ~= nil and tm.alive == true then
|
||
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
|
||
if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end
|
||
if c.vuln ~= nil then
|
||
tm.vuln = tm.vuln + c.vuln
|
||
if self:HasRelic("championBelt") then
|
||
tm.weak = tm.weak + 1
|
||
end
|
||
end
|
||
end
|
||
end
|
||
table.remove(self.Hand, slot)
|
||
if c.kind ~= "Power" then
|
||
table.insert(self.DiscardPile, cardId)
|
||
end
|
||
if c.draw ~= nil then
|
||
self:DrawCards(c.draw, true)
|
||
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
|
||
if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
|
||
_TimerService:ClearTimer(self.CardHoverTweenId)
|
||
self.CardHoverTweenId = 0
|
||
end
|
||
local cardXs = { ${CARD_XS.join(', ')} }
|
||
for i = 1, 5 do
|
||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
|
||
if e ~= nil and e.UITransformComponent ~= nil then
|
||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||
e.UITransformComponent.anchoredPosition = Vector2(cardXs[i], 0)
|
||
end
|
||
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)
|
||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||
end
|
||
self:ResolveCardDrop(slot, touchPoint)`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||
]),
|
||
method('ResolveCardDrop', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||
return
|
||
end
|
||
local cardId = self.Hand[slot]
|
||
if cardId == nil then
|
||
return
|
||
end
|
||
local c = self.Cards[cardId]
|
||
if c == nil then
|
||
return
|
||
end
|
||
if c.kind == "Attack" then
|
||
local best = 0
|
||
local bestDist = 200
|
||
for i = 1, #self.Monsters do
|
||
local m = self.Monsters[i]
|
||
if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
|
||
local wp = m.entity.TransformComponent.WorldPosition
|
||
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
|
||
local dx = sp.x - touchPoint.x
|
||
local dy = sp.y - touchPoint.y
|
||
local d = math.sqrt(dx * dx + dy * dy)
|
||
if d < bestDist then
|
||
bestDist = d
|
||
best = i
|
||
end
|
||
end
|
||
end
|
||
if best > 0 then
|
||
self.TargetIndex = best
|
||
self:PlayCard(slot)
|
||
end
|
||
else
|
||
local ui = _UILogic:ScreenToUIPosition(touchPoint)
|
||
if ui.y > -180 then
|
||
self:PlayCard(slot)
|
||
end
|
||
end`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||
]),
|
||
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
||
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
||
if m == nil or m.alive ~= true then
|
||
m = nil
|
||
for i = 1, #self.Monsters do
|
||
if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end
|
||
end
|
||
end
|
||
if m == nil then
|
||
return
|
||
end
|
||
local dmg = amount
|
||
if m.vuln > 0 then
|
||
dmg = math.floor(dmg * 1.5)
|
||
end
|
||
if m.block > 0 and pierce ~= true then
|
||
local absorbed = math.min(m.block, dmg)
|
||
m.block = m.block - absorbed
|
||
dmg = dmg - absorbed
|
||
end
|
||
m.hp = m.hp - dmg
|
||
self:MonsterHitMotion(m.slot)
|
||
if m.hp <= 0 then
|
||
m.hp = 0
|
||
self:KillMonster(m.slot)
|
||
end`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||
]),
|
||
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
|
||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||
self:DealDamageToTarget(damage, pierce)
|
||
self:RenderCombat()
|
||
self:CheckCombatEnd()
|
||
return
|
||
end
|
||
self.FxBusy = true
|
||
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
|
||
if fx ~= nil then
|
||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||
end
|
||
if fx.UITransformComponent ~= nil and m.entity.TransformComponent ~= nil then
|
||
local wp = m.entity.TransformComponent.WorldPosition
|
||
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
|
||
fx.UITransformComponent.anchoredPosition = _UILogic:ScreenToUIPosition(sp)
|
||
end
|
||
fx.Enable = true
|
||
end
|
||
_TimerService:SetTimerOnce(function()
|
||
if fx ~= nil then fx.Enable = false end
|
||
self.FxBusy = false
|
||
local shown = damage
|
||
local mt = self.Monsters[targetIndex]
|
||
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
||
shown = math.floor(damage * 1.5)
|
||
end
|
||
self:DealDamageToTarget(damage, pierce)
|
||
self:ShowDmgPop(targetIndex, shown)
|
||
self:RenderCombat()
|
||
self:CheckCombatEnd()
|
||
end, 0.35)`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||
]),
|
||
method('PlayAoeFx', `self.FxBusy = true
|
||
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
|
||
if fx ~= nil then
|
||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||
end
|
||
if fx.UITransformComponent ~= nil then
|
||
fx.UITransformComponent.anchoredPosition = Vector2(300, 60)
|
||
end
|
||
fx.Enable = true
|
||
end
|
||
_TimerService:SetTimerOnce(function()
|
||
if fx ~= nil then fx.Enable = false end
|
||
self.FxBusy = false
|
||
for i = 1, #self.Monsters do
|
||
local m = self.Monsters[i]
|
||
if m ~= nil and m.alive == true then
|
||
local dmg = damage
|
||
if m.vuln > 0 then
|
||
dmg = math.floor(dmg * 1.5)
|
||
end
|
||
if m.block > 0 then
|
||
local absorbed = math.min(m.block, dmg)
|
||
m.block = m.block - absorbed
|
||
dmg = dmg - absorbed
|
||
end
|
||
m.hp = m.hp - dmg
|
||
self:ShowDmgPop(i, dmg)
|
||
self:MonsterHitMotion(i)
|
||
if m.hp <= 0 then
|
||
m.hp = 0
|
||
self:KillMonster(m.slot)
|
||
end
|
||
end
|
||
end
|
||
self:RenderCombat()
|
||
self:CheckCombatEnd()
|
||
end, 0.35)`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||
]),
|
||
method('KillMonster', `local m = self.Monsters[slot]
|
||
if m == nil then
|
||
return
|
||
end
|
||
m.alive = false
|
||
if m.entity ~= nil and isvalid(m.entity) then
|
||
local ent = m.entity
|
||
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
|
||
end
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false)
|
||
for i = 1, #self.Monsters do
|
||
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
|
||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('DealDamageToPlayer', `local dmg = amount
|
||
if self.PlayerBlock > 0 then
|
||
local absorbed = math.min(self.PlayerBlock, dmg)
|
||
self.PlayerBlock = self.PlayerBlock - absorbed
|
||
dmg = dmg - absorbed
|
||
end
|
||
if dmg > 0 then
|
||
self.PlayerHp = self.PlayerHp - dmg
|
||
if self:HasRelic("bronzeScales") and attackerSlot ~= nil and attackerSlot > 0 then
|
||
local am = self.Monsters[attackerSlot]
|
||
if am ~= nil and am.alive == true then
|
||
am.hp = am.hp - 3
|
||
self:MonsterHitMotion(am.slot)
|
||
if am.hp <= 0 then
|
||
am.hp = 0
|
||
self:KillMonster(am.slot)
|
||
end
|
||
end
|
||
end
|
||
if self:HasRelic("selfFormingClay") then
|
||
self.ClayBlockNext = self.ClayBlockNext + 3
|
||
end
|
||
if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then
|
||
self.FirstHpLossDone = true
|
||
self:DrawCards(3)
|
||
self:RenderHand(false)
|
||
end
|
||
end
|
||
if self.PlayerHp < 0 then
|
||
self.PlayerHp = 0
|
||
end`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'attackerSlot' },
|
||
]),
|
||
method('EnemyTurn', `self.TurnBusy = true
|
||
self:EnemyActStep(1)`),
|
||
method('EnemyActStep', `local idx = 0
|
||
for i = fromIndex, #self.Monsters do
|
||
if self.Monsters[i].alive == true then idx = i; break end
|
||
end
|
||
if idx == 0 or self.PlayerHp <= 0 then
|
||
self:FinishEnemyTurn()
|
||
return
|
||
end
|
||
local m = self.Monsters[idx]
|
||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx)
|
||
self:SetEntityEnabled(base .. "/ActFrame", true)
|
||
_TimerService:SetTimerOnce(function()
|
||
if m.poison ~= nil and m.poison > 0 then
|
||
m.hp = m.hp - m.poison
|
||
self:ShowDmgPop(idx, m.poison)
|
||
self:MonsterHitMotion(idx)
|
||
m.poison = m.poison - 1
|
||
if m.hp <= 0 then
|
||
m.hp = 0
|
||
self:KillMonster(m.slot)
|
||
self:RenderCombat()
|
||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
||
return
|
||
end
|
||
end
|
||
m.block = 0
|
||
local intent = m.intents[m.intentIdx]
|
||
if intent ~= nil then
|
||
if intent.kind == "Attack" then
|
||
self:MonsterLunge(idx)
|
||
local atk = intent.value + m.str
|
||
if m.weak > 0 then
|
||
atk = math.floor(atk * 0.75)
|
||
end
|
||
if self.PlayerVuln > 0 then
|
||
atk = math.floor(atk * 1.5)
|
||
end
|
||
local before = self.PlayerHp
|
||
self:DealDamageToPlayer(atk, idx)
|
||
self:ShowPlayerDmgPop(before - self.PlayerHp)
|
||
self:PlayerHitMotion()
|
||
elseif intent.kind == "Defend" then
|
||
m.block = m.block + intent.value
|
||
elseif intent.kind == "Debuff" then
|
||
if intent.effect == "weak" then
|
||
self.PlayerWeak = self.PlayerWeak + intent.value
|
||
elseif intent.effect == "vuln" then
|
||
self.PlayerVuln = self.PlayerVuln + intent.value
|
||
end
|
||
elseif intent.kind == "AddCard" then
|
||
local cnt = intent.count or 1
|
||
for ci = 1, cnt do
|
||
table.insert(self.DiscardPile, intent.card)
|
||
end
|
||
self:RenderPiles()
|
||
local cn = intent.card
|
||
local cc = self.Cards[intent.card]
|
||
if cc ~= nil then cn = cc.name end
|
||
self:Toast(m.name .. ": " .. cn .. " 추가!")
|
||
end
|
||
end
|
||
if #m.intents > 0 then
|
||
m.intentIdx = math.random(1, #m.intents)
|
||
end
|
||
if m.weak > 0 then m.weak = m.weak - 1 end
|
||
if m.vuln > 0 then m.vuln = m.vuln - 1 end
|
||
self:RenderCombat()
|
||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
||
end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]),
|
||
method('FinishEnemyTurn', `self.TurnBusy = false
|
||
self:CheckCombatEnd()
|
||
if self.CombatOver == true then
|
||
return
|
||
end
|
||
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`),
|
||
method('CheckCombatEnd', `local anyAlive = false
|
||
for i = 1, #self.Monsters do
|
||
if self.Monsters[i].alive == true then anyAlive = true; break end
|
||
end
|
||
if anyAlive == false then
|
||
self.CombatOver = true
|
||
self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult())
|
||
self:ApplyRelics("combatEnd")
|
||
self:ApplyRelics("combatReward")
|
||
self:MaybeDropPotion()
|
||
self:RenderRun()
|
||
local node = self.MapNodes[self.CurrentNodeId]
|
||
if node ~= nil and node.type == "elite" then
|
||
self.Gold = self.Gold + 15
|
||
local nid = self:PickNewRelic()
|
||
if nid ~= "" then
|
||
self:AddRelic(nid)
|
||
local nr = self.Relics[nid]
|
||
if nr ~= nil then
|
||
self:Toast("유물 획득: " .. nr.name)
|
||
end
|
||
end
|
||
end
|
||
if node ~= nil and node.type == "boss" then
|
||
if self.PlayerJob == "" and self.Floor < self.RunLength then
|
||
self:ShowJobChoice()
|
||
else
|
||
if self.PlayerJob ~= "" then self:AwardSouls(1) end
|
||
local bid = self:PickNewRelic()
|
||
if bid ~= "" then
|
||
self:AddRelic(bid)
|
||
local br = self.Relics[bid]
|
||
if br ~= nil then
|
||
self:Toast("유물 획득: " .. br.name)
|
||
end
|
||
end
|
||
self:ContinueAfterBoss()
|
||
end
|
||
else
|
||
self:OfferReward()
|
||
end
|
||
elseif self.PlayerHp <= 0 then
|
||
self.CombatOver = true
|
||
self:EndRun("패배...")
|
||
end`),
|
||
method('ContinueAfterBoss', `if self.Floor < self.RunLength then
|
||
self.Floor = self.Floor + 1
|
||
self.CurrentNodeId = ""
|
||
self.CurrentEnemyId = ""
|
||
self:GenerateMap()
|
||
self:RenderRun()
|
||
self:TeleportToActMap()
|
||
self:ShowMap()
|
||
else
|
||
self:EndRun("런 클리어!")
|
||
end`),
|
||
method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`),
|
||
method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false)
|
||
if kind == "relic" then
|
||
local bid = self:PickNewRelic()
|
||
if bid ~= "" then
|
||
self:AddRelic(bid)
|
||
local br = self.Relics[bid]
|
||
if br ~= nil then
|
||
self:Toast("유물 획득: " .. br.name)
|
||
end
|
||
end
|
||
self:ContinueAfterBoss()
|
||
else
|
||
self:ShowJobSelect()
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||
method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass]
|
||
if opts == nil then
|
||
opts = self.Jobs["warrior"]
|
||
end
|
||
self.JobOpts = opts
|
||
for i = 1, 3 do
|
||
local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)
|
||
local o = opts[i]
|
||
if o ~= nil then
|
||
self:SetEntityEnabled(base, true)
|
||
self:SetText(base .. "/Name", o.name)
|
||
self:SetText(base .. "/Desc", o.desc)
|
||
local sc = self.Cards[o.starter]
|
||
if sc ~= nil then
|
||
self:SetText(base .. "/Starter", "대표 카드: " .. sc.name)
|
||
end
|
||
else
|
||
self:SetEntityEnabled(base, false)
|
||
end
|
||
end
|
||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`),
|
||
method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then
|
||
for cls, list in pairs(self.Jobs) do
|
||
for i = 1, #list do
|
||
if list[i].id == self.PlayerJob then
|
||
return list[i].name
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if self.SelectedClass == "warrior" then
|
||
return "전사"
|
||
elseif self.SelectedClass == "bandit" then
|
||
return "도적"
|
||
elseif self.SelectedClass == "magician" then
|
||
return "마법사"
|
||
end
|
||
return "플레이어"`, [], 0, 'string'),
|
||
method('SetJob', `self.PlayerJob = jobId
|
||
local starter = ""
|
||
local opts = self.Jobs[self.SelectedClass] or {}
|
||
for i = 1, #opts do
|
||
if opts[i].id == jobId then
|
||
starter = opts[i].starter
|
||
end
|
||
end
|
||
if starter ~= "" then
|
||
table.insert(self.RunDeck, starter)
|
||
local sc = self.Cards[starter]
|
||
if sc ~= nil then
|
||
self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name)
|
||
end
|
||
end
|
||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
|
||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false)
|
||
self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]),
|
||
method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} }
|
||
local target = maps[self.Floor]
|
||
if target == nil then
|
||
return
|
||
end
|
||
local lp = _UserService.LocalPlayer
|
||
if lp == nil then
|
||
return
|
||
end
|
||
if lp.CurrentMapName == target then
|
||
return
|
||
end
|
||
_TeleportService:TeleportToMapPosition(lp, Vector3(-6, 0.03, 0), target)`),
|
||
method('ShowResult', `self:SetText("/ui/DefaultGroup/CombatHud/Result", text)
|
||
local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/Result")
|
||
if entity ~= nil then
|
||
entity.Enable = true
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||
method('EndRun', `local msg = text
|
||
if text == "런 클리어!" and self.AscensionLevel >= self.AscensionUnlocked and self.AscensionUnlocked < 10 then
|
||
self.AscensionUnlocked = self.AscensionUnlocked + 1
|
||
local lp = _UserService.LocalPlayer
|
||
if lp ~= nil then
|
||
self:SaveAscension(self.AscensionUnlocked, lp.PlayerComponent.UserId)
|
||
end
|
||
self:RenderAscension()
|
||
msg = "런 클리어! 승천 " .. string.format("%d", self.AscensionUnlocked) .. " 해금!"
|
||
end
|
||
self:ShowResult(msg)
|
||
self.RunActive = false
|
||
_TimerService:SetTimerOnce(function() self:ShowLobby() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||
method('BuffsLabel', `local parts = {}
|
||
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
|
||
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
|
||
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
|
||
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
|
||
return table.concat(parts, " ")`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
|
||
], 0, 'string'),
|
||
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
|
||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)
|
||
local m = self.Monsters[i]
|
||
if m ~= nil and m.alive == true then
|
||
self:SetEntityEnabled(base, true)
|
||
self:SetText(base .. "/Name", m.name)
|
||
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
|
||
local intent = m.intents[m.intentIdx]
|
||
local t = ""
|
||
if intent ~= nil then
|
||
if intent.kind == "Attack" then
|
||
local atk = intent.value + m.str
|
||
if m.weak > 0 then atk = math.floor(atk * 0.75) end
|
||
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
|
||
t = "공격 " .. tostring(atk)
|
||
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
|
||
elseif intent.kind == "Debuff" then
|
||
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
|
||
else t = "취약 " .. tostring(intent.value) .. " 부여" end
|
||
elseif intent.kind == "AddCard" then
|
||
t = "저주 카드 추가"
|
||
end
|
||
end
|
||
self:SetText(base .. "/Intent", t)
|
||
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)
|
||
elseif intent.kind == "AddCard" then
|
||
intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1)
|
||
else
|
||
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
|
||
end
|
||
end
|
||
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
|
||
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
|
||
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
|
||
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0))
|
||
else
|
||
self:SetEntityEnabled(base, false)
|
||
end
|
||
end
|
||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
|
||
self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
|
||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
|
||
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
|
||
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
|
||
local names = {}
|
||
for i = 1, #self.PlayerPowers do
|
||
local pc = self.Cards[self.PlayerPowers[i]]
|
||
if pc ~= nil then table.insert(names, pc.name) end
|
||
end
|
||
if pb ~= "" then pb = pb .. " · " end
|
||
pb = pb .. table.concat(names, " ")
|
||
end
|
||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb)
|
||
self:RenderRun()`),
|
||
method('ShowDmgPop', `local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot) .. "/DmgPop"
|
||
self:SetText(base, "-" .. string.format("%d", amount))
|
||
self:SetEntityEnabled(base, true)
|
||
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||
]),
|
||
method('ShowPlayerDmgPop', `local base = "/ui/DefaultGroup/CombatHud/PlayerPanel/DmgPop"
|
||
if amount > 0 then
|
||
self:SetText(base, "-" .. string.format("%d", amount))
|
||
else
|
||
self:SetText(base, "막음")
|
||
end
|
||
self:SetEntityEnabled(base, true)
|
||
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
|
||
if lp == nil or lp.StateComponent == nil then
|
||
return
|
||
end
|
||
pcall(function() lp.StateComponent:ChangeState("ATTACK") end)
|
||
_TimerService:SetTimerOnce(function()
|
||
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then
|
||
pcall(function() lp.StateComponent:ChangeState("IDLE") end)
|
||
end
|
||
end, 0.5)`),
|
||
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer
|
||
if lp == nil then
|
||
return
|
||
end
|
||
if lp.StateComponent ~= nil then
|
||
pcall(function() lp.StateComponent:ChangeState("HIT") end)
|
||
end
|
||
local tr = lp.TransformComponent
|
||
if tr == nil then
|
||
return
|
||
end
|
||
local p = tr.Position
|
||
tr.Position = Vector3(p.x - 0.15, p.y, p.z)
|
||
_TimerService:SetTimerOnce(function()
|
||
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then
|
||
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z)
|
||
end
|
||
end, 0.15)`),
|
||
method('MonsterLunge', `local m = self.Monsters[idx]
|
||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||
return
|
||
end
|
||
if m.motionBusy == true then
|
||
return
|
||
end
|
||
m.motionBusy = true
|
||
local e = m.entity
|
||
local tr = e.TransformComponent
|
||
if tr == nil then
|
||
m.motionBusy = false
|
||
return
|
||
end
|
||
local p = tr.Position
|
||
tr.Position = Vector3(p.x - 0.35, p.y, p.z)
|
||
_TimerService:SetTimerOnce(function()
|
||
if isvalid(e) and e.TransformComponent ~= nil then
|
||
e.TransformComponent.Position = Vector3(p.x, p.y, p.z)
|
||
end
|
||
m.motionBusy = false
|
||
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]),
|
||
method('MonsterHitMotion', `local m = self.Monsters[slot]
|
||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||
return
|
||
end
|
||
local e = m.entity
|
||
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then
|
||
e.SpriteRendererComponent.SpriteRUID = m.hitClip
|
||
_TimerService:SetTimerOnce(function()
|
||
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then
|
||
e.SpriteRendererComponent.SpriteRUID = m.standClip
|
||
end
|
||
end, 0.5)
|
||
else
|
||
if m.motionBusy == true then
|
||
return
|
||
end
|
||
m.motionBusy = true
|
||
local tr = e.TransformComponent
|
||
if tr == nil then
|
||
m.motionBusy = false
|
||
return
|
||
end
|
||
local p = tr.Position
|
||
local seq = { 0.12, -0.12, 0 }
|
||
for i = 1, #seq do
|
||
local dx = seq[i]
|
||
_TimerService:SetTimerOnce(function()
|
||
if isvalid(e) and e.TransformComponent ~= nil then
|
||
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z)
|
||
end
|
||
if i == #seq then
|
||
m.motionBusy = false
|
||
end
|
||
end, 0.06 * i)
|
||
end
|
||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
|
||
if e == nil or e.UITransformComponent == nil then
|
||
return
|
||
end
|
||
local ratio = 0
|
||
if maxHp > 0 then ratio = hp / maxHp end
|
||
if ratio < 0 then ratio = 0 end
|
||
local w = width * ratio
|
||
e.UITransformComponent.RectSize = Vector2(w, 14)`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
|
||
]),
|
||
method('PositionMonsterSlot', `local m = self.Monsters[slot]
|
||
if m == nil or m.entity == nil or not isvalid(m.entity) then
|
||
return
|
||
end
|
||
local tr = m.entity.TransformComponent
|
||
if tr == nil then
|
||
return
|
||
end
|
||
local wp = tr.WorldPosition
|
||
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y}))
|
||
local uipos = _UILogic:ScreenToUIPosition(screen)
|
||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot))
|
||
if e ~= nil and e.UITransformComponent ~= nil then
|
||
e.UITransformComponent.anchoredPosition = uipos
|
||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
|
||
self.TargetIndex = slot
|
||
self:RenderCombat()
|
||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층"
|
||
if self.AscensionLevel > 0 then
|
||
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
|
||
end
|
||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Floor", floorText)
|
||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
|
||
method('CardPool', `local pool = {}
|
||
for id, c in pairs(self.Cards) do
|
||
if c.class == self.SelectedClass or (self.PlayerJob ~= "" and c.class == self.PlayerJob) then
|
||
table.insert(pool, id)
|
||
end
|
||
end
|
||
table.sort(pool)
|
||
return pool`, [], 0, 'any'),
|
||
method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
||
local pool = self:CardPool()
|
||
local byRarity = {}
|
||
for _, id in ipairs(pool) do
|
||
local r = self.Cards[id].rarity or "normal"
|
||
if byRarity[r] == nil then byRarity[r] = {} end
|
||
table.insert(byRarity[r], id)
|
||
end
|
||
self.RewardChoices = {}
|
||
for i = 1, 3 do
|
||
local roll = math.random(1, 100)
|
||
local want = "normal"
|
||
if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end
|
||
local bucket = byRarity[want]
|
||
if bucket == nil or #bucket == 0 then bucket = pool end
|
||
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
|
||
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
||
end
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
||
if hud ~= nil then
|
||
hud.Enable = true
|
||
end`),
|
||
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||
]),
|
||
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
||
return
|
||
end
|
||
if slot ~= 0 and self.RewardChoices ~= nil then
|
||
local id = self.RewardChoices[slot]
|
||
if id ~= nil then
|
||
table.insert(self.RunDeck, id)
|
||
end
|
||
end
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
||
if hud ~= nil then
|
||
hud.Enable = false
|
||
end
|
||
self:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('HasRelic', `if self.RunRelics == nil then
|
||
return false
|
||
end
|
||
for i = 1, #self.RunRelics do
|
||
if self.RunRelics[i] == id then
|
||
return true
|
||
end
|
||
end
|
||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
||
method('ApplyRelics', `if self.RunRelics == nil then
|
||
return
|
||
end
|
||
for i = 1, #self.RunRelics do
|
||
local r = self.Relics[self.RunRelics[i]]
|
||
if r ~= nil and r.hook == hook then
|
||
if r.effect == "block" then
|
||
self.PlayerBlock = self.PlayerBlock + r.value
|
||
elseif r.effect == "energy" then
|
||
self.Energy = self.Energy + r.value
|
||
elseif r.effect == "strength" then
|
||
self.PlayerStr = self.PlayerStr + r.value
|
||
elseif r.effect == "draw" then
|
||
self:DrawCards(r.value)
|
||
self:RenderHand(false)
|
||
elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then
|
||
self.PlayerHp = self.PlayerHp + r.value
|
||
if self.PlayerHp > self.PlayerMaxHp then
|
||
self.PlayerHp = self.PlayerMaxHp
|
||
end
|
||
elseif r.effect == "healIfLow" then
|
||
if self.PlayerHp * 2 <= self.PlayerMaxHp then
|
||
self.PlayerHp = self.PlayerHp + r.value
|
||
if self.PlayerHp > self.PlayerMaxHp then
|
||
self.PlayerHp = self.PlayerMaxHp
|
||
end
|
||
end
|
||
elseif r.effect == "gold" then
|
||
self.Gold = self.Gold + r.value
|
||
end
|
||
end
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hook' }]),
|
||
method('AddRelic', `if self.RunRelics == nil then
|
||
self.RunRelics = {}
|
||
end
|
||
table.insert(self.RunRelics, id)
|
||
local r = self.Relics[id]
|
||
if r ~= nil and r.hook == "passive" then
|
||
if r.effect == "potionSlots" then
|
||
self.PotionSlots = r.value
|
||
self:RenderPotions()
|
||
elseif r.effect == "maxHp" then
|
||
self.PlayerMaxHp = self.PlayerMaxHp + r.value
|
||
self.PlayerHp = self.PlayerHp + r.value
|
||
end
|
||
end
|
||
self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||
method('PickNewRelic', `local pool = {}
|
||
for i = 1, #self.RelicPool do
|
||
if self:HasRelic(self.RelicPool[i]) == false then
|
||
table.insert(pool, self.RelicPool[i])
|
||
end
|
||
end
|
||
if #pool == 0 then
|
||
self.Gold = self.Gold + 25
|
||
self:Toast("유물을 모두 모았습니다! 메소 +25")
|
||
return ""
|
||
end
|
||
return pool[math.random(1, #pool)]`, [], 0, 'string'),
|
||
method('AddPotion', `if self.RunPotions == nil then
|
||
self.RunPotions = {}
|
||
end
|
||
if #self.RunPotions >= self.PotionSlots then
|
||
self:Toast("물약 슬롯이 가득 찼습니다")
|
||
return false
|
||
end
|
||
table.insert(self.RunPotions, pid)
|
||
self:RenderPotions()
|
||
return true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pid' }], 0, 'boolean'),
|
||
method('MaybeDropPotion', `if math.random() > ${POTIONS.dropChance} then
|
||
return
|
||
end
|
||
local keys = {}
|
||
for pid, _ in pairs(self.Potions) do
|
||
table.insert(keys, pid)
|
||
end
|
||
table.sort(keys)
|
||
local pid = keys[math.random(1, #keys)]
|
||
if self:AddPotion(pid) == true then
|
||
local p = self.Potions[pid]
|
||
self:Toast("물약 획득: " .. p.name)
|
||
end`),
|
||
method('RenderPotions', `for i = 1, 5 do
|
||
local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)
|
||
local e = _EntityService:GetEntityByPath(base)
|
||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||
local pid = nil
|
||
if self.RunPotions ~= nil then
|
||
pid = self.RunPotions[i]
|
||
end
|
||
if pid ~= nil and self.Potions[pid] ~= nil then
|
||
e.SpriteGUIRendererComponent.ImageRUID = self.Potions[pid].icon
|
||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||
elseif i > self.PotionSlots then
|
||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||
e.SpriteGUIRendererComponent.Color = Color(0.1, 0.1, 0.12, 0.85)
|
||
else
|
||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||
e.SpriteGUIRendererComponent.Color = Color(0.22, 0.25, 0.3, 0.9)
|
||
end
|
||
end
|
||
end`),
|
||
method('OpenPotionMenu', `if self.RunPotions == nil or self.RunPotions[slot] == nil then
|
||
return
|
||
end
|
||
self.PotionMenuSlot = slot
|
||
local pid = self.RunPotions[slot]
|
||
local p = self.Potions[pid]
|
||
if p ~= nil then
|
||
self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc)
|
||
end
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('ClosePotionMenu', `self.PotionMenuSlot = 0
|
||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`),
|
||
method('UsePotion', `if self.PotionMenuSlot <= 0 then
|
||
return
|
||
end
|
||
if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then
|
||
self:Toast("지금은 사용할 수 없습니다")
|
||
return
|
||
end
|
||
local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud")
|
||
local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand")
|
||
if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then
|
||
self:Toast("전투 중에만 사용할 수 있습니다")
|
||
return
|
||
end
|
||
local pid = self.RunPotions[self.PotionMenuSlot]
|
||
if pid == nil then
|
||
return
|
||
end
|
||
local p = self.Potions[pid]
|
||
if p == nil then
|
||
return
|
||
end
|
||
if p.effect == "heal" then
|
||
self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp)
|
||
elseif p.effect == "damage" then
|
||
self:DealDamageToTarget(p.value, false)
|
||
self:ShowDmgPop(self.TargetIndex, p.value)
|
||
elseif p.effect == "strength" then
|
||
self.PlayerStr = self.PlayerStr + p.value
|
||
elseif p.effect == "block" then
|
||
self.PlayerBlock = self.PlayerBlock + p.value
|
||
elseif p.effect == "energy" then
|
||
self.Energy = self.Energy + p.value
|
||
elseif p.effect == "weak" then
|
||
local tm = self.Monsters[self.TargetIndex]
|
||
if tm ~= nil and tm.alive == true then
|
||
tm.weak = tm.weak + p.value
|
||
end
|
||
end
|
||
table.remove(self.RunPotions, self.PotionMenuSlot)
|
||
self:Toast("물약 사용: " .. p.name)
|
||
self:ClosePotionMenu()
|
||
self:RenderPotions()
|
||
self:RenderPiles()
|
||
self:RenderCombat()
|
||
self:CheckCombatEnd()`),
|
||
method('TossPotion', `if self.PotionMenuSlot <= 0 then
|
||
return
|
||
end
|
||
local pid = self.RunPotions[self.PotionMenuSlot]
|
||
if pid ~= nil then
|
||
local p = self.Potions[pid]
|
||
table.remove(self.RunPotions, self.PotionMenuSlot)
|
||
if p ~= nil then
|
||
self:Toast("물약 버림: " .. p.name)
|
||
end
|
||
end
|
||
self:ClosePotionMenu()
|
||
self:RenderPotions()`),
|
||
method('RenderRelics', `local count = 0
|
||
if self.RunRelics ~= nil then
|
||
count = #self.RunRelics
|
||
end
|
||
for i = 1, 10 do
|
||
local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)
|
||
local e = _EntityService:GetEntityByPath(base)
|
||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||
local rid = nil
|
||
if self.RunRelics ~= nil then
|
||
rid = self.RunRelics[i]
|
||
end
|
||
if rid ~= nil and self.Relics[rid] ~= nil and (i < 10 or count <= 10) then
|
||
e.SpriteGUIRendererComponent.ImageRUID = self.Relics[rid].icon
|
||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||
else
|
||
e.SpriteGUIRendererComponent.ImageRUID = ""
|
||
e.SpriteGUIRendererComponent.Color = Color(0.15, 0.16, 0.2, 0.6)
|
||
end
|
||
end
|
||
end
|
||
local of = ""
|
||
if count > 10 then
|
||
of = "+" .. tostring(count - 9)
|
||
end
|
||
self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`),
|
||
method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then
|
||
return
|
||
end
|
||
local cardId = self.Hand[slot]
|
||
if cardId == nil then
|
||
return
|
||
end
|
||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||
local tx = 0
|
||
if e ~= nil and e.UITransformComponent ~= nil then
|
||
tx = e.UITransformComponent.anchoredPosition.x
|
||
e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1)
|
||
end
|
||
local c = self.Cards[cardId]
|
||
if c ~= nil then
|
||
self:ShowTooltip(c.name, c.desc, tx)
|
||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
||
if e ~= nil and e.UITransformComponent ~= nil then
|
||
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
|
||
end
|
||
self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('ShowTooltip', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name)
|
||
self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc)
|
||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox")
|
||
if e ~= nil then
|
||
if e.UITransformComponent ~= nil then
|
||
e.UITransformComponent.anchoredPosition = Vector2(x, 400)
|
||
end
|
||
e.Enable = true
|
||
end`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
|
||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
|
||
]),
|
||
method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`),
|
||
method('ShowMap', `self:ShowState("map")
|
||
self:RenderMap()`),
|
||
method('GenerateMap', `-- 절차 생성 — tools/map/rogue-map.mjs(JS 미러)와 로직 동기화 유지
|
||
self.MapNodes = {}
|
||
self.MapStart = {}
|
||
self.VisitedNodes = {}
|
||
self.Depth = 0
|
||
self.MapNodes["boss"] = { type = "boss", row = ${MAP_ROWS} + 1, col = 0, next = {} }
|
||
local cols = { 1, 2, 3, 4 }
|
||
for i = #cols, 2, -1 do
|
||
local j = math.random(1, i)
|
||
cols[i], cols[j] = cols[j], cols[i]
|
||
end
|
||
local starts = { cols[1], cols[2], math.random(1, ${MAP_COLS}), math.random(1, ${MAP_COLS}) }
|
||
for p = 1, 4 do
|
||
local c = starts[p]
|
||
local sid = "r1c" .. tostring(c)
|
||
if self.MapNodes[sid] == nil then
|
||
self.MapNodes[sid] = { type = "combat", row = 1, col = c, next = {} }
|
||
end
|
||
local found = false
|
||
for i = 1, #self.MapStart do
|
||
if self.MapStart[i] == sid then found = true end
|
||
end
|
||
if found == false then
|
||
table.insert(self.MapStart, sid)
|
||
end
|
||
for r = 1, ${MAP_ROWS} - 1 do
|
||
local nc = c + math.random(-1, 1)
|
||
if nc < 1 then nc = 1 end
|
||
if nc > ${MAP_COLS} then nc = ${MAP_COLS} end
|
||
local nid = "r" .. tostring(r + 1) .. "c" .. tostring(nc)
|
||
if self.MapNodes[nid] == nil then
|
||
self.MapNodes[nid] = { type = "combat", row = r + 1, col = nc, next = {} }
|
||
end
|
||
local fid = "r" .. tostring(r) .. "c" .. tostring(c)
|
||
local dup = false
|
||
for i = 1, #self.MapNodes[fid].next do
|
||
if self.MapNodes[fid].next[i] == nid then dup = true end
|
||
end
|
||
if dup == false then
|
||
table.insert(self.MapNodes[fid].next, nid)
|
||
end
|
||
c = nc
|
||
end
|
||
local lid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c)
|
||
local bdup = false
|
||
for i = 1, #self.MapNodes[lid].next do
|
||
if self.MapNodes[lid].next[i] == "boss" then bdup = true end
|
||
end
|
||
if bdup == false then
|
||
table.insert(self.MapNodes[lid].next, "boss")
|
||
end
|
||
end
|
||
for r = 3, ${MAP_ROWS} do
|
||
for c = 1, ${MAP_COLS} do
|
||
local id = "r" .. tostring(r) .. "c" .. tostring(c)
|
||
local node = self.MapNodes[id]
|
||
if node ~= nil then
|
||
-- 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지)
|
||
local parentTypes = {}
|
||
for pid, pn in pairs(self.MapNodes) do
|
||
if pn.row == r - 1 then
|
||
for i = 1, #pn.next do
|
||
if pn.next[i] == id then parentTypes[pn.type] = true end
|
||
end
|
||
end
|
||
end
|
||
local w
|
||
if r == ${MAP_ROWS} then
|
||
w = { { "rest", 50 }, { "combat", 25 }, { "shop", 10 }, { "elite", 8 }, { "treasure", 7 } }
|
||
elseif r >= 4 then
|
||
w = { { "combat", 45 }, { "elite", 16 }, { "shop", 12 }, { "rest", 12 }, { "treasure", 15 } }
|
||
else
|
||
w = { { "combat", 45 }, { "shop", 12 }, { "rest", 12 } }
|
||
end
|
||
local total = 0
|
||
for i = 1, #w do
|
||
local t = w[i][1]
|
||
if (t == "elite" or t == "rest" or t == "shop") and parentTypes[t] == true then
|
||
w[i][2] = 0
|
||
end
|
||
total = total + w[i][2]
|
||
end
|
||
local roll = math.random() * total
|
||
local acc = 0
|
||
for i = 1, #w do
|
||
acc = acc + w[i][2]
|
||
if roll <= acc then
|
||
node.type = w[i][1]
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end`),
|
||
method('IsReachable', `local list
|
||
if self.CurrentNodeId == "" then
|
||
list = self.MapStart
|
||
else
|
||
local node = self.MapNodes[self.CurrentNodeId]
|
||
if node == nil then
|
||
return false
|
||
end
|
||
list = node.next
|
||
end
|
||
for i = 1, #list do
|
||
if list[i] == id then
|
||
return true
|
||
end
|
||
end
|
||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
|
||
method('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id
|
||
local e = _EntityService:GetEntityByPath(base)
|
||
if e == nil then
|
||
return
|
||
end
|
||
local node = self.MapNodes[id]
|
||
if node == nil then
|
||
e.Enable = false
|
||
return
|
||
end
|
||
e.Enable = true
|
||
local tname = "전투"
|
||
local r0 = 0.78
|
||
local g0 = 0.36
|
||
local b0 = 0.32
|
||
if node.type == "elite" then
|
||
tname = "엘리트"
|
||
r0 = 0.62
|
||
g0 = 0.4
|
||
b0 = 0.85
|
||
elseif node.type == "shop" then
|
||
tname = "상점"
|
||
r0 = 0.9
|
||
g0 = 0.75
|
||
b0 = 0.35
|
||
elseif node.type == "rest" then
|
||
tname = "휴식"
|
||
r0 = 0.4
|
||
g0 = 0.75
|
||
b0 = 0.45
|
||
elseif node.type == "treasure" then
|
||
tname = "보물"
|
||
r0 = 0.35
|
||
g0 = 0.7
|
||
b0 = 0.75
|
||
elseif node.type == "boss" then
|
||
tname = "보스"
|
||
r0 = 0.85
|
||
g0 = 0.25
|
||
b0 = 0.25
|
||
end
|
||
self:SetText(base .. "/Label", tname)
|
||
local reachable = self:IsReachable(id)
|
||
local visited = false
|
||
if self.VisitedNodes ~= nil then
|
||
for i = 1, #self.VisitedNodes do
|
||
if self.VisitedNodes[i] == id then visited = true end
|
||
end
|
||
end
|
||
if e.SpriteGUIRendererComponent ~= nil then
|
||
if id == self.CurrentNodeId then
|
||
e.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1)
|
||
elseif visited == true then
|
||
e.SpriteGUIRendererComponent.Color = Color(0.18, 0.19, 0.22, 0.9)
|
||
elseif reachable == true then
|
||
e.SpriteGUIRendererComponent.Color = Color(r0, g0, b0, 1)
|
||
else
|
||
e.SpriteGUIRendererComponent.Color = Color(r0 * 0.45, g0 * 0.45, b0 * 0.45, 0.55)
|
||
end
|
||
end
|
||
if e.ButtonComponent ~= nil then
|
||
e.ButtonComponent.Enable = reachable
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||
method('RenderMapDots', `local node = self.MapNodes[fromId]
|
||
local has = false
|
||
if node ~= nil then
|
||
for i = 1, #node.next do
|
||
if node.next[i] == toId then has = true end
|
||
end
|
||
end
|
||
for k = 1, 3 do
|
||
local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
|
||
if d ~= nil then
|
||
d.Enable = has
|
||
if has == true and d.SpriteGUIRendererComponent ~= nil then
|
||
if fromId == self.CurrentNodeId then
|
||
d.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1)
|
||
else
|
||
d.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.8)
|
||
end
|
||
end
|
||
end
|
||
end`, [
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'dotId' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromId' },
|
||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toId' },
|
||
]),
|
||
method('RenderMap', `for r = 1, ${MAP_ROWS} do
|
||
for c = 1, ${MAP_COLS} do
|
||
self:RenderMapNode("r" .. tostring(r) .. "c" .. tostring(c))
|
||
end
|
||
end
|
||
self:RenderMapNode("boss")
|
||
for r = 1, ${MAP_ROWS} - 1 do
|
||
for c = 1, ${MAP_COLS} do
|
||
local fid = "r" .. tostring(r) .. "c" .. tostring(c)
|
||
for c2 = c - 1, c + 1 do
|
||
if c2 >= 1 and c2 <= ${MAP_COLS} then
|
||
self:RenderMapDots(fid .. "_" .. tostring(c2), fid, "r" .. tostring(r + 1) .. "c" .. tostring(c2))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
for c = 1, ${MAP_COLS} do
|
||
local fid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c)
|
||
self:RenderMapDots(fid .. "_b", fid, "boss")
|
||
end
|
||
`),
|
||
method('PickNode', `if self.RunActive ~= true then
|
||
return
|
||
end
|
||
if self:IsReachable(id) ~= true then
|
||
return
|
||
end
|
||
self.CurrentNodeId = id
|
||
if self.VisitedNodes == nil then
|
||
self.VisitedNodes = {}
|
||
end
|
||
table.insert(self.VisitedNodes, id)
|
||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud")
|
||
if hud ~= nil then
|
||
hud.Enable = false
|
||
end
|
||
local node = self.MapNodes[id]
|
||
self.Depth = node.row
|
||
self:RenderRun()
|
||
if node.type == "shop" then
|
||
self:ShowShop()
|
||
elseif node.type == "rest" then
|
||
self:ShowRest()
|
||
elseif node.type == "treasure" then
|
||
self:ShowTreasure()
|
||
else
|
||
self.CurrentEnemyId = ""
|
||
self:StartCombat()
|
||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
|
||
method('ShowShop', `local pool = self:CardPool()
|
||
self.ShopChoices = {}
|
||
self.ShopBought = { false, false, false }
|
||
for i = 1, 3 do
|
||
self.ShopChoices[i] = pool[math.random(1, #pool)]
|
||
end
|
||
self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)]
|
||
self.ShopRelicBought = false
|
||
local pkeys = {}
|
||
for pid, _ in pairs(self.Potions) do
|
||
table.insert(pkeys, pid)
|
||
end
|
||
table.sort(pkeys)
|
||
self.ShopPotion = pkeys[math.random(1, #pkeys)]
|
||
self.ShopPotionBought = false
|
||
self:RenderShop()
|
||
self:ShowState("shop")`),
|
||
method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold))
|
||
for i = 1, 3 do
|
||
local cid = self.ShopChoices[i]
|
||
local c = self.Cards[cid]
|
||
local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
||
if c ~= nil then
|
||
self:ApplyCardFace(base, cid)
|
||
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
|
||
local e = _EntityService:GetEntityByPath(base)
|
||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||
if self.ShopBought[i] == true then
|
||
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
local rr = self.Relics[self.ShopRelic]
|
||
if rr ~= nil then
|
||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
|
||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
|
||
local re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
||
if re ~= nil and re.SpriteGUIRendererComponent ~= nil then
|
||
if self.ShopRelicBought == true then
|
||
re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||
else
|
||
re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)
|
||
end
|
||
end
|
||
end
|
||
local pp = self.Potions[self.ShopPotion]
|
||
if pp ~= nil then
|
||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc)
|
||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소")
|
||
local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
||
if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then
|
||
if self.ShopPotionBought == true then
|
||
pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||
else
|
||
pe.SpriteGUIRendererComponent.Color = Color(0.45, 0.7, 0.55, 1)
|
||
end
|
||
end
|
||
end`),
|
||
method('BuyRelic', `if self.ShopRelicBought == true then
|
||
return
|
||
end
|
||
if self.Gold < ${RELIC_PRICE} then
|
||
return
|
||
end
|
||
self.Gold = self.Gold - ${RELIC_PRICE}
|
||
self:AddRelic(self.ShopRelic)
|
||
self.ShopRelicBought = true
|
||
self:RenderShop()
|
||
self:RenderRun()`),
|
||
method('BuyPotion', `if self.ShopPotionBought == true then
|
||
return
|
||
end
|
||
if self.Gold < ${POTIONS.shopPrice} then
|
||
return
|
||
end
|
||
if self.RunPotions ~= nil and #self.RunPotions >= self.PotionSlots then
|
||
self:Toast("물약 슬롯이 가득 찼습니다")
|
||
return
|
||
end
|
||
if self:AddPotion(self.ShopPotion) == true then
|
||
self.Gold = self.Gold - ${POTIONS.shopPrice}
|
||
self.ShopPotionBought = true
|
||
end
|
||
self:RenderShop()
|
||
self:RenderRun()`),
|
||
method('BuyCard', `if self.ShopBought == nil or self.ShopBought[slot] == true then
|
||
return
|
||
end
|
||
if self.Gold < ${CARD_PRICE} then
|
||
return
|
||
end
|
||
self.Gold = self.Gold - ${CARD_PRICE}
|
||
table.insert(self.RunDeck, self.ShopChoices[slot])
|
||
self.ShopBought[slot] = true
|
||
self:RenderShop()
|
||
self:RenderRun()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||
method('ShowRest', `local old = self.PlayerHp
|
||
self.PlayerHp = self.PlayerHp + ${REST_HEAL}
|
||
if self.PlayerHp > self.PlayerMaxHp then
|
||
self.PlayerHp = self.PlayerMaxHp
|
||
end
|
||
local healed = self.PlayerHp - old
|
||
self:SetText("/ui/DefaultGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")")
|
||
self:RenderCombat()
|
||
self:ShowState("rest")`),
|
||
method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud")
|
||
if s ~= nil then
|
||
s.Enable = false
|
||
end
|
||
local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud")
|
||
if r ~= nil then
|
||
r.Enable = false
|
||
end
|
||
local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud")
|
||
if t ~= nil then
|
||
t.Enable = false
|
||
end
|
||
self:ShowMap()`),
|
||
method('ShowTreasure', `self.ChestOpened = false
|
||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||
if chest ~= nil then
|
||
if chest.SpriteGUIRendererComponent ~= nil then
|
||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}"
|
||
end
|
||
if chest.UITransformComponent ~= nil then
|
||
chest.UITransformComponent.anchoredPosition = Vector2(0, 40)
|
||
end
|
||
end
|
||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true)
|
||
self:ShowState("treasure")`),
|
||
method('OpenChest', `if self.ChestOpened == true then
|
||
return
|
||
end
|
||
self.ChestOpened = true
|
||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false)
|
||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||
local steps = { 10, -10, 8, -8, 5, 0 }
|
||
for i = 1, #steps do
|
||
local dx = steps[i]
|
||
_TimerService:SetTimerOnce(function()
|
||
if chest ~= nil and isvalid(chest) and chest.UITransformComponent ~= nil then
|
||
chest.UITransformComponent.anchoredPosition = Vector2(dx, 40)
|
||
end
|
||
end, 0.08 * i)
|
||
end
|
||
_TimerService:SetTimerOnce(function()
|
||
if chest ~= nil and isvalid(chest) and chest.SpriteGUIRendererComponent ~= nil then
|
||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_OPEN_RUID}"
|
||
end
|
||
local g = 40 + math.random(0, 20)
|
||
local nid = self:PickNewRelic()
|
||
local msg = ""
|
||
if nid ~= "" then
|
||
self:AddRelic(nid)
|
||
local nr = self.Relics[nid]
|
||
msg = "유물 획득: " .. nr.name .. " · 메소 +" .. tostring(g)
|
||
else
|
||
g = g + 30
|
||
msg = "메소 +" .. tostring(g)
|
||
end
|
||
self.Gold = self.Gold + g
|
||
self:RenderRun()
|
||
self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg)
|
||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true)
|
||
end, 0.55)`),
|
||
]);
|
||
for (const m of combat.ContentProto.Json.Methods) {
|
||
if (m.ExecSpace === 0) m.ExecSpace = 6; // 기본은 ClientOnly(6), 서버 RPC(Server=1·Client=2) 명시값은 보존
|
||
}
|
||
writeFileSync('RootDesk/MyDesk/SlayDeckController.codeblock', JSON.stringify(combat, null, 2), 'utf8');
|
||
}
|
||
|
||
function patchCommon() {
|
||
const common = JSON.parse(readFileSync(COMMON_FILE, 'utf8'));
|
||
const entity = common.ContentProto.Entities.find((e) => e.path === '/common');
|
||
entity.componentNames = 'script.SlayDeckController';
|
||
entity.jsonString['@components'] = [
|
||
{ '@type': 'script.SlayDeckController', Enable: true, Energy: 0, MaxEnergy: 3, Turn: 0, TweenEventId: 0 },
|
||
];
|
||
JSON.parse(JSON.stringify(common));
|
||
writeFileSync(COMMON_FILE, JSON.stringify(common, null, 2), 'utf8');
|
||
}
|
||
|
||
upsertUi();
|
||
writeCodeblocks();
|
||
patchCommon();
|
||
|
||
console.log('Slay deck UI and combat codeblocks generated.');
|