15 Commits

6 changed files with 11893 additions and 6775 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -124,10 +124,81 @@ export function simulateCombat(data, rng, stats) {
if (drawPile.length === 0) break; if (drawPile.length === 0) break;
const card = drawPile.pop(); const card = drawPile.pop();
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화) // 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
if (hand.length >= 10) discard.push(card); else hand.push(card); if (hand.length >= 10) {
discard.push(card);
triggerSly(card);
} else hand.push(card);
} }
} }
const aliveList = () => mob.filter((m) => m.alive); const aliveList = () => mob.filter((m) => m.alive);
function resolveCardEffects(id, c, costSpent, recordStats = true) {
const alive = aliveList();
let dmg = 0;
if (c.kind === 'Attack') {
if (alive.length && c.damage) {
const target = chooseTarget(alive, calcAttack(c.damage || 0, pStr, pWeak, 0));
if (c.weak) target.weak += c.weak;
if (c.vuln) target.vuln += c.vuln;
const hitN = c.hits || 1;
let totalNv = 0;
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
dmg = totalNv;
if (c.aoe === true) {
for (const m2 of aliveList()) {
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
const r2 = applyDamage(m2.hp, m2.block, d2);
m2.hp = r2.hp; m2.block = r2.block;
if (m2.hp <= 0) m2.alive = false;
}
} else {
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
if (c.pierce === true) {
target.hp -= dmg;
if (target.hp < 0) target.hp = 0;
} else {
const r = applyDamage(target.hp, target.block, dmg);
target.hp = r.hp; target.block = r.block;
}
if (target.hp <= 0) target.alive = false;
}
}
if (c.block) pBlock += c.block;
} else if (c.kind === 'Power') {
if (c.powerEffect && recordStats) powers.push(id);
} else {
pBlock += c.block || 0;
if ((c.weak || c.vuln || c.poison) && alive.length) {
const target = chooseTarget(alive, 0);
if (c.weak) target.weak += c.weak;
if (c.vuln) target.vuln += c.vuln;
if (c.poison) target.poison += c.poison;
}
}
if (c.strength) pStr += c.strength;
if (c.selfVuln) pVuln += c.selfVuln;
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
if (c.draw) draw(c.draw);
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, c.block || 0);
}
function triggerSly(id) {
const c = cards[id];
if (!c?.sly) return;
resolveCardEffects(id, c, 0, false);
}
function discardHandCard(idx, trigger = true) {
const [id] = hand.splice(idx, 1);
if (!id) return;
discard.push(id);
if (trigger) triggerSly(id);
}
function applyDiscardEffects(c) {
if (c.discardAll) {
while (hand.length) discardHandCard(hand.length - 1, true);
} else if (c.discard) {
const n = Math.min(c.discard, hand.length);
for (let i = 0; i < n; i++) discardHandCard(hand.length - 1, true);
}
}
while (turns < MAX_TURNS) { while (turns < MAX_TURNS) {
turns++; turns++;
@@ -141,7 +212,7 @@ export function simulateCombat(data, rng, stats) {
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value; else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value; else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
} }
let energy = ENERGY + energyBonus; hand = []; draw(HAND_SIZE); let energy = ENERGY + energyBonus; draw(HAND_SIZE);
while (true) { while (true) {
const alive = aliveList(); const alive = aliveList();
if (alive.length === 0) break; if (alive.length === 0) break;
@@ -149,66 +220,23 @@ export function simulateCombat(data, rng, stats) {
if (idx < 0) break; if (idx < 0) break;
const id = hand[idx], c = cards[id]; const id = hand[idx], c = cards[id];
energy -= c.cost; energy -= c.cost;
if (c.kind === 'Attack') { resolveCardEffects(id, c, c.cost);
const target = chooseTarget(alive, calcAttack(c.damage || 0, pStr, pWeak, 0));
// 카드 디버프는 피해보다 먼저 적용 — Lua PlayCard(즉시 부여) + 지연 데미지(0.35s) 동기화
if (c.weak) target.weak += c.weak;
if (c.vuln) target.vuln += c.vuln;
// 다단히트: 타격마다 힘·약화 적용 합산, 취약은 합산값에 1회 (Lua 동기화)
const hitN = c.hits || 1;
let totalNv = 0;
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
let dmg = totalNv; // 통계 보고용 (aoe는 1대상 기준)
if (c.aoe === true) {
// 전체 공격 — 대상마다 취약/방어 개별 적용 (Lua PlayAoeFx 동기화)
for (const m2 of aliveList()) {
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
const r2 = applyDamage(m2.hp, m2.block, d2);
m2.hp = r2.hp; m2.block = r2.block;
if (m2.hp <= 0) m2.alive = false;
}
} else {
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
if (c.pierce === true) {
target.hp -= dmg; // 방어 무시
if (target.hp < 0) target.hp = 0;
} else {
const r = applyDamage(target.hp, target.block, dmg);
target.hp = r.hp; target.block = r.block;
}
if (target.hp <= 0) target.alive = false;
}
if (c.block) pBlock += c.block;
if (c.strength) pStr += c.strength;
if (c.selfVuln) pVuln += c.selfVuln;
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
if (stats) stats[id] = bump(stats[id], c.cost, dmg, c.block || 0);
} else if (c.kind === 'Power') {
if (c.powerEffect) powers.push(id);
if (stats) stats[id] = bump(stats[id], c.cost, 0, 0);
} else {
pBlock += c.block || 0;
if (c.strength) pStr += c.strength;
if (c.selfVuln) pVuln += c.selfVuln;
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
if (c.weak || c.vuln || c.poison) {
const target = chooseTarget(alive, 0);
if (c.weak) target.weak += c.weak;
if (c.vuln) target.vuln += c.vuln;
if (c.poison) target.poison += c.poison;
}
if (stats) stats[id] = bump(stats[id], c.cost, 0, c.block || 0);
}
hand.splice(idx, 1); hand.splice(idx, 1);
if (c.kind !== 'Power') discard.push(id); // 파워는 소멸 — Lua 동기화 if (c.kind !== 'Power') discard.push(id);
if (c.draw) draw(c.draw); applyDiscardEffects(c);
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp }; if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
} }
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화) // 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
let burn = 0; let burn = 0;
for (const hid of hand) { const hc = cards[hid]; if (hc && hc.endTurnDamage) burn += hc.endTurnDamage; } for (const hid of hand) { const hc = cards[hid]; if (hc && hc.endTurnDamage) burn += hc.endTurnDamage; }
if (burn > 0) { pHp -= burn; if (pHp < 0) pHp = 0; } if (burn > 0) { pHp -= burn; if (pHp < 0) pHp = 0; }
discard.push(...hand); hand = []; const kept = [];
for (const hid of hand) {
const hc = cards[hid];
if (hc?.retain === true) kept.push(hid);
else discard.push(hid);
}
hand = kept;
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 }; if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
// 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전) // 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전)
if (pWeak > 0) pWeak--; if (pWeak > 0) pWeak--;

View File

@@ -375,3 +375,33 @@ test('simulateCombat: endTurnDamage(화상)이 턴 종료 시 누적 피해', ()
assert.equal(r.win, false); assert.equal(r.win, false);
assert.notEqual(r.draw, true); assert.notEqual(r.draw, true);
}); });
test("simulateCombat: sly discarded card resolves for free", () => {
const data = {
cards: {
Toss: { name: "Toss", cost: 1, kind: "Skill", discardAll: true },
SlyHit: { name: "SlyHit", cost: 99, kind: "Attack", damage: 10, sly: true },
Blank: { name: "Blank", cost: 99, kind: "Skill", block: 0 },
},
starterDeck: ["Toss", "SlyHit", "Blank", "Blank", "Blank"],
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Defend", value: 0 }] }],
};
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.win, true);
assert.equal(r.turns, 1);
});
test("simulateCombat: retain keeps card in hand across turns", () => {
const data = {
cards: {
Boost: { name: "Boost", cost: 3, kind: "Power", powerEffect: "energyPerTurn", value: 98 },
Hold: { name: "Hold", cost: 100, kind: "Attack", damage: 10, retain: true },
Blank: { name: "Blank", cost: 99, kind: "Skill", block: 0 },
},
starterDeck: ["Blank", "Blank", "Blank", "Blank", "Blank", "Boost", "Hold", "Blank", "Blank", "Blank"],
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Defend", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.turns, 2);
});

View File

@@ -149,6 +149,10 @@ function luaCardsTable(cards) {
if (c.draw != null) fields.push(`draw = ${c.draw}`); if (c.draw != null) fields.push(`draw = ${c.draw}`);
if (c.heal != null) fields.push(`heal = ${c.heal}`); if (c.heal != null) fields.push(`heal = ${c.heal}`);
if (c.poison != null) fields.push(`poison = ${c.poison}`); if (c.poison != null) fields.push(`poison = ${c.poison}`);
if (c.discard != null) fields.push(`discard = ${c.discard}`);
if (c.discardAll === true) fields.push('discardAll = true');
if (c.sly === true) fields.push('sly = true');
if (c.retain === true) fields.push('retain = true');
if (c.aoe === true) fields.push('aoe = true'); if (c.aoe === true) fields.push('aoe = true');
if (c.unplayable === true) fields.push('unplayable = true'); if (c.unplayable === true) fields.push('unplayable = true');
if (c.curse === true) fields.push('curse = true'); if (c.curse === true) fields.push('curse = true');
@@ -210,13 +214,30 @@ 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 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 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 SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 };
const DAMAGE_DIGIT_RUIDS = [
'b94c19830538447f81617035d89bcc05',
'01b023122a6f4a5789e1d4c61ff8f430',
'57ff71d1b9eb471b9feb1c15348770c9',
'cab92837798a42ad9143c67e93f999e1',
'366f271f9ca94a0684083aad9298efad',
'5c7a6ad38491466aa84bf450e0fdcf25',
'7d82a6838e1b4f4a8a0f7420db34c985',
'c0765bb1e47d46ffbe1df4ac19ea9b1b',
'6ea0bfed61e149f88a9b3f22dd79774f',
'82ad2acaae4e4b3fb87bf73635250d22',
];
const DAMAGE_POP_MAX_DIGITS = 5;
const DAMAGE_POP_DIGIT_W = 22;
const DAMAGE_POP_DIGIT_H = 32;
const DAMAGE_POP_DIGIT_SPACING = 18;
const MAX_MONSTERS = 4; const MAX_MONSTERS = 4;
const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산 const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산
const HP_BAR_W = 140; const HP_BAR_W = 140;
const WHITE = { r: 1, g: 1, b: 1, a: 1 }; const WHITE = { r: 1, g: 1, b: 1, a: 1 };
const INK = { r: 0.13, g: 0.11, b: 0.09, a: 1 }; // 밝은 배너·설명 박스 위 먹색 글자 const CARD_NAME_TEXT = { r: 1, g: 0.92, b: 0.62, a: 1 };
const CARD_DESC_TEXT = { r: 0.98, g: 0.96, b: 0.9, a: 1 };
// 카드 프레임(1054×1492 원본) 슬롯 레이아웃 — 픽셀 실측을 180×250 카드 좌표로 환산한 기준값을 폭 비례 스케일. // 카드 프레임(1054×1492 원본) 슬롯 레이아웃 — 픽셀 실측을 180×250 카드 좌표로 환산한 기준값을 폭 비례 스케일.
// 실측(워리어·메이지·밴딧 공통): 육각 중심 (120,127)→(-70,104) · 배너 본체 y55..165, x215..1015→중심 (+15,+107) // 실측(워리어·메이지·밴딧 공통): 육각 중심 (120,127)→(-70,104) · 배너 본체 y55..165, x215..1015→중심 (+15,+107)
// · 설명 박스 y~1030..1480→중심 (0,-86) · 아트 영역 y260..1030→중심 (0,+17) // · 설명 박스 y~1030..1480→중심 (0,-86) · 아트 영역 y260..1030→중심 (0,+17)
@@ -225,9 +246,9 @@ function cardFaceLayout(W) {
const r = (v) => Math.round(v * s); const r = (v) => Math.round(v * s);
return { return {
texts: [ texts: [
['Cost', { size: { x: r(40), y: r(40) }, pos: { x: r(-70), y: r(104) }, fontSize: r(24), bold: true, color: WHITE }], ['Cost', { size: { x: r(40), y: r(40) }, pos: { x: r(-70), y: r(104) }, fontSize: r(24), bold: true, color: WHITE, dropShadow: false, outlineWidth: 2 }],
['Name', { size: { x: r(132), y: r(24) }, pos: { x: r(15), y: r(107) }, fontSize: r(16), bold: true, color: INK }], ['Name', { size: { x: r(142), y: r(28) }, pos: { x: r(15), y: r(106) }, fontSize: r(17), bold: true, color: CARD_NAME_TEXT, dropShadow: false, outlineWidth: 2 }],
['Desc', { size: { x: r(150), y: r(62) }, pos: { x: 0, y: r(-86) }, fontSize: r(16), bold: false, color: INK }], ['Desc', { size: { x: r(158), y: r(78) }, pos: { x: 0, y: r(-82) }, fontSize: r(14), bold: true, color: CARD_DESC_TEXT, dropShadow: false, outlineWidth: 2 }],
], ],
art: { size: { x: r(112), y: r(112) }, pos: { x: 0, y: r(17) } }, art: { size: { x: r(112), y: r(112) }, pos: { x: 0, y: r(17) } },
}; };
@@ -334,15 +355,15 @@ function button({ enabled = true } = {}) {
}; };
} }
function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4 }) { function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4, dropShadow = false, outlineWidth = 1 }) {
return { return {
'@type': 'MOD.Core.TextComponent', '@type': 'MOD.Core.TextComponent',
Alignment: alignment, Alignment: alignment,
Bold: bold, Bold: bold,
DropShadow: false, DropShadow: dropShadow,
DropShadowAngle: 30, DropShadowAngle: 30,
DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 },
DropShadowDistance: 32, DropShadowDistance: dropShadow ? 18 : 32,
Font: 0, Font: 0,
FontColor: color, FontColor: color,
FontSize: fontSize, FontSize: fontSize,
@@ -350,7 +371,7 @@ function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1
MinSize: 8, MinSize: 8,
OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 }, OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 },
OutlineDistance: { x: 1, y: -1 }, OutlineDistance: { x: 1, y: -1 },
OutlineWidth: 1, OutlineWidth: outlineWidth,
Overflow: 0, Overflow: 0,
OverrideSorting: false, OverrideSorting: false,
Padding: { left: 0, right: 0, top: 0, bottom: 0 }, Padding: { left: 0, right: 0, top: 0, bottom: 0 },
@@ -586,7 +607,7 @@ function upsertUi() {
components: [ 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 }), 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 }), sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
], ],
}); });
ui.ContentProto.Entities.push(child); ui.ContentProto.Entities.push(child);
@@ -608,6 +629,10 @@ function upsertUi() {
child.jsonString['@components'][2].FontSize = cfg.fontSize; child.jsonString['@components'][2].FontSize = cfg.fontSize;
child.jsonString['@components'][2].MaxSize = cfg.fontSize; child.jsonString['@components'][2].MaxSize = cfg.fontSize;
child.jsonString['@components'][2].FontColor = cfg.color; child.jsonString['@components'][2].FontColor = cfg.color;
child.jsonString['@components'][2].Bold = cfg.bold;
child.jsonString['@components'][2].DropShadow = cfg.dropShadow === true;
child.jsonString['@components'][2].DropShadowDistance = cfg.dropShadow === true ? 18 : 32;
child.jsonString['@components'][2].OutlineWidth = cfg.outlineWidth || 1;
} }
} }
@@ -877,7 +902,7 @@ function upsertUi() {
components: [ 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 }), 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 }), sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
], ],
})); }));
} }
@@ -1032,7 +1057,7 @@ function upsertUi() {
components: [ 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 }), 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 }), sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
], ],
})); }));
} }
@@ -1093,6 +1118,30 @@ function upsertUi() {
}); });
targetFrame.jsonString.enable = false; targetFrame.jsonString.enable = false;
combat.push(targetFrame); combat.push(targetFrame);
const targetMarker = entity({
id: guid('cmb', 360 + i), path: `${base}/TargetMarker`, 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: 116, y: 116 }, pos: { x: 0, y: 2 } }),
sprite({ color: { r: 0.95, g: 0.08, b: 0.05, a: 0.92 }, type: 1 }),
text({ value: '+', fontSize: 72, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 4 }),
],
});
targetMarker.jsonString.enable = false;
combat.push(targetMarker);
const targetLabel = entity({
id: guid('cmb', 370 + i), path: `${base}/TargetMarker/Label`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 10,
components: [
transform({ parentW: 116, parentH: 116, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 112, y: 28 }, pos: { x: 0, y: -52 } }),
sprite({ color: { r: 0.08, g: 0.02, b: 0.02, a: 0.86 }, type: 1 }),
text({ value: 'TARGET', fontSize: 18, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 3 }),
],
});
targetLabel.jsonString.enable = false;
combat.push(targetLabel);
const actFrame = entity({ const actFrame = entity({
id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite', id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
@@ -1153,17 +1202,27 @@ function upsertUi() {
], ],
})); }));
const dmgPop = entity({ const dmgPop = entity({
id: guid('cmb', 250 + i), path: `${base}/DmgPop`, modelId: 'uitext', entryId: 'UIText', id: guid('cmb', 250 + i), path: `${base}/DmgPop`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 9, displayOrder: 11,
components: [ 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 } }), transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 192, y: 56 }, pos: { x: 0, y: 76 } }),
sprite({ color: TRANSPARENT }), sprite({ color: { r: 0.12, g: 0.02, b: 0.02, a: 0.72 }, type: 1 }),
text({ value: '', fontSize: 24, bold: true, color: { r: 1, g: 0.35, b: 0.3, a: 1 }, alignment: 4 }),
], ],
}); });
dmgPop.jsonString.enable = false; dmgPop.jsonString.enable = false;
combat.push(dmgPop); combat.push(dmgPop);
for (let d = 0; d < DAMAGE_POP_MAX_DIGITS; d++) {
combat.push(entity({
id: guid('cmb', 380 + i * 10 + d), path: `${base}/DmgPop/Digit${d + 1}`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 12,
components: [
transform({ parentW: 192, parentH: 56, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: DAMAGE_POP_DIGIT_W, y: DAMAGE_POP_DIGIT_H }, pos: { x: 0, y: 0 } }),
sprite({ dataId: DAMAGE_DIGIT_RUIDS[0], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0 }),
],
}));
}
const mBlockBadge = entity({ const mBlockBadge = entity({
id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite', id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
@@ -1404,6 +1463,21 @@ function upsertUi() {
text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 4 }), text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 4 }),
], ],
})); }));
const discardPrompt = entity({
id: guid('cmb', 333),
path: '/ui/DefaultGroup/CombatHud/DiscardPrompt',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 22,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 520, y: 48 }, pos: { x: 0, y: -260 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.86 }, type: 1 }),
text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }),
],
});
discardPrompt.jsonString.enable = false;
combat.push(discardPrompt);
const potionMenu = entity({ const potionMenu = entity({
id: guid('cmb', 340), id: guid('cmb', 340),
path: '/ui/DefaultGroup/CombatHud/PotionMenu', path: '/ui/DefaultGroup/CombatHud/PotionMenu',
@@ -1551,7 +1625,7 @@ function upsertUi() {
components: [ 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 }), 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 }), sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
], ],
})); }));
} }
@@ -1775,7 +1849,7 @@ function upsertUi() {
components: [ 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 }), 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 }), sprite({ color: TRANSPARENT }),
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }),
], ],
})); }));
} }
@@ -2805,6 +2879,7 @@ function writeCodeblocks() {
prop('string', 'ShopRelic', '""'), prop('string', 'ShopRelic', '""'),
prop('boolean', 'ShopRelicBought', 'false'), prop('boolean', 'ShopRelicBought', 'false'),
prop('number', 'DragSlot', '0'), prop('number', 'DragSlot', '0'),
prop('number', 'DragTargetIndex', '0'),
prop('boolean', 'FxBusy', 'false'), prop('boolean', 'FxBusy', 'false'),
prop('boolean', 'TurnBusy', 'false'), prop('boolean', 'TurnBusy', 'false'),
prop('number', 'PlayerStr', '0'), prop('number', 'PlayerStr', '0'),
@@ -2824,6 +2899,8 @@ function writeCodeblocks() {
prop('any', 'VisitedNodes'), prop('any', 'VisitedNodes'),
prop('boolean', 'ChestOpened', 'false'), prop('boolean', 'ChestOpened', 'false'),
prop('string', 'PlayerJob', '""'), prop('string', 'PlayerJob', '""'),
prop('number', 'DiscardSelectRemaining', '0'),
prop('number', 'DiscardSelectTotal', '0'),
], [ ], [
method('OnBeginPlay', `${luaCardsTable(CARDS.cards)} method('OnBeginPlay', `${luaCardsTable(CARDS.cards)}
${luaFramesTable()} ${luaFramesTable()}
@@ -3076,15 +3153,7 @@ bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1
bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end) bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end)
bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`), bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`),
method('ShowCodex', `self.CodexMode = true method('ShowCodex', `self.CodexMode = true
self.ClassDeckMode = false self.ClassDeckMode = true
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") local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close")
if close ~= nil and close.ButtonComponent ~= nil then if close ~= nil and close.ButtonComponent ~= nil then
if self.AllDeckCloseHandler ~= nil then if self.AllDeckCloseHandler ~= nil then
@@ -3092,7 +3161,9 @@ if close ~= nil and close.ButtonComponent ~= nil then
end end
self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
end end
self:BindClassDeckTabs()
self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false) self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false)
self:SetClassDeckTab("warrior")
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud")
if hud ~= nil then if hud ~= nil then
hud.Enable = true hud.Enable = true
@@ -3299,6 +3370,7 @@ self:ShowMap()`),
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/DiscardPrompt", false)
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
self.MaxEnergy = 3 self.MaxEnergy = 3
self.Turn = 0 self.Turn = 0
@@ -3310,6 +3382,8 @@ self.PlayerPowers = {}
self.FightAttackCount = 0 self.FightAttackCount = 0
self.FirstHpLossDone = false self.FirstHpLossDone = false
self.ClayBlockNext = 0 self.ClayBlockNext = 0
self.DiscardSelectRemaining = 0
self.DiscardSelectTotal = 0
self.CombatOver = false self.CombatOver = false
self.DiscardPile = {} self.DiscardPile = {}
self.Hand = {} self.Hand = {}
@@ -3500,6 +3574,9 @@ for i = 1, 10 do
cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(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(UITouchEnterEvent, function() self:HoverCard(i) end)
cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end) cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end)
if cardEntity.ButtonComponent ~= nil then
cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end)
end
end end
end end
for i = 1, 3 do for i = 1, 3 do
@@ -3660,6 +3737,10 @@ self:RenderCombat()`),
method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return return
end end
if self:IsDiscardSelecting() == true then
self:Toast("버릴 카드를 먼저 선택하세요")
return
end
local burn = 0 local burn = 0
for bi = 1, #self.Hand do for bi = 1, #self.Hand do
\tlocal hc = self.Cards[self.Hand[bi]] \tlocal hc = self.Cards[self.Hand[bi]]
@@ -3671,10 +3752,17 @@ if burn > 0 then
\tself:ShowPlayerDmgPop(burn) \tself:ShowPlayerDmgPop(burn)
\tself:RenderCombat() \tself:RenderCombat()
end end
local kept = {}
for i = 1, #self.Hand do for i = 1, #self.Hand do
\ttable.insert(self.DiscardPile, self.Hand[i]) \tlocal cardId = self.Hand[i]
\tlocal c = self.Cards[cardId]
\tif c ~= nil and c.retain == true then
\t\ttable.insert(kept, cardId)
\telse
\t\ttable.insert(self.DiscardPile, cardId)
\tend
end end
self.Hand = {} self.Hand = kept
if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end
if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
self:RenderHand(false) self:RenderHand(false)
@@ -3691,6 +3779,7 @@ for i = 1, amount do
\tlocal cardId = table.remove(self.DrawPile) \tlocal cardId = table.remove(self.DrawPile)
\tif #self.Hand >= 10 then \tif #self.Hand >= 10 then
\t\ttable.insert(self.DiscardPile, cardId) \t\ttable.insert(self.DiscardPile, cardId)
\t\tself:TriggerSly(cardId)
\telse \telse
\t\ttable.insert(self.Hand, cardId) \t\ttable.insert(self.Hand, cardId)
\t\tif #self.Hand <= 5 then \t\tif #self.Hand <= 5 then
@@ -3704,7 +3793,7 @@ if animate == true and #drawnSlots > 0 then
\tlocal drawStart = Vector2(-590, 8) \tlocal drawStart = Vector2(-590, 8)
\tfor i = 1, #drawnSlots do \tfor i = 1, #drawnSlots do
\t\tlocal slot = drawnSlots[i] \t\tlocal slot = drawnSlots[i]
\t\tself:AnimateCardFrom(slot, drawStart, Vector2((slot - 3) * 200, 0), 0.08 + i * 0.045) \t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
\tend \tend
end`, [ end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
@@ -3946,6 +4035,17 @@ end`),
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]), ]),
method('GetHandSlotX', `local n = 0
if self.Hand ~= nil then
n = #self.Hand
end
if n <= 0 then
return 0
end
local spacing = 175
if n > 8 then spacing = math.floor(1400 / n) end
local startX = -((n - 1) * spacing) / 2
return startX + (slot - 1) * spacing`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'),
method('RenderHand', `local n = #self.Hand method('RenderHand', `local n = #self.Hand
local spacing = 175 local spacing = 175
if n > 8 then spacing = math.floor(1400 / n) end if n > 8 then spacing = math.floor(1400 / n) end
@@ -3961,7 +4061,7 @@ for i = 1, 10 do
\t\t\tcardEntity.Enable = true \t\t\tcardEntity.Enable = true
\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end \t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end
\t\t\tself:ApplyCardVisual(i, cardId) \t\t\tself:ApplyCardVisual(i, cardId)
\t\t\tlocal tx = startX + (i - 1) * spacing \t\t\tlocal tx = self:GetHandSlotX(i)
\t\t\tif animate == true then \t\t\tif animate == true then
\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03) \t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03)
\t\t\telse \t\t\telse
@@ -4018,8 +4118,11 @@ if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then
return return
end end
prefix = "/ui/DefaultGroup/CardHand/Card" prefix = "/ui/DefaultGroup/CardHand/Card"
count = 5 count = 0
xs = { ${CARD_XS.join(', ')} } if self.Hand ~= nil then count = #self.Hand end
for i = 1, count do
xs[i] = self:GetHandSlotX(i)
end
baseY = 0 baseY = 0
hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0 hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0
elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then
@@ -4140,7 +4243,151 @@ if dmg < 0 then
dmg = 0 dmg = 0
end end
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), 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 method('ResolveCardEffects', `if c == nil then
return
end
if c.kind == "Attack" then
if c.damage ~= nil then
self:PlayerAttackMotion()
local total = 0
local hitN = c.hits or 1
for h = 1, hitN do
total = total + self:CalcPlayerAttack(c.damage)
end
if c.aoe == true then
self:PlayAoeFx(c.fx or c.image, total)
else
self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true)
end
end
if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block
end
if free ~= true then
self:ApplyRelics("cardPlayed")
end
elseif c.kind == "Skill" then
if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block
end
elseif c.kind == "Power" then
if c.powerEffect ~= nil and free ~= true then
table.insert(self.PlayerPowers, cardId)
end
end
if c.strength ~= nil then
self.PlayerStr = self.PlayerStr + c.strength
end
if c.selfVuln ~= nil then
self.PlayerVuln = self.PlayerVuln + c.selfVuln
end
if c.heal ~= nil then
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
end
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
local tm = self.Monsters[self.TargetIndex]
if tm == nil or tm.alive ~= true then
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
end
end
if tm ~= nil and tm.alive == true then
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end
if c.vuln ~= nil then
tm.vuln = tm.vuln + c.vuln
if self:HasRelic("championBelt") then
tm.weak = tm.weak + 1
end
end
end
end
if c.draw ~= nil then
self:DrawCards(c.draw, true)
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' },
]),
method('TriggerSly', `local c = self.Cards[cardId]
if c == nil or c.sly ~= true then
return
end
self:Toast("교활 발동: " .. c.name)
self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
method('DiscardHandCard', `if self.Hand == nil then
return
end
local cardId = self.Hand[slot]
if cardId == nil then
return
end
table.remove(self.Hand, slot)
table.insert(self.DiscardPile, cardId)
if triggerSly == true then
self:TriggerSly(cardId)
end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'triggerSly' },
]),
method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'),
method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/DiscardPrompt")
if e == nil then
return
end
if self:IsDiscardSelecting() == true then
local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining
self:SetText("/ui/DefaultGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. tostring(picked + 1) .. "/" .. tostring(self.DiscardSelectTotal))
e.Enable = true
else
e.Enable = false
end`),
method('BeginDiscardSelection', `if c == nil or self.Hand == nil then
return false
end
local n = 0
if c.discardAll == true then
n = #self.Hand
elseif c.discard ~= nil then
n = math.min(c.discard, #self.Hand)
end
if n <= 0 then
return false
end
self.DiscardSelectRemaining = n
self.DiscardSelectTotal = n
self:UpdateDiscardPrompt()
self:Toast("버릴 카드를 선택하세요")
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0
self.DiscardSelectTotal = 0
self:UpdateDiscardPrompt()
self:RenderHand(false)
self:RenderPiles()
self:RenderCombat()
self:CheckCombatEnd()`),
method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then
return false
end
if self.Hand == nil or self.Hand[slot] == nil then
return true
end
self:DiscardHandCard(slot, true)
self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1
if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then
self:FinishDiscardSelection()
else
self:UpdateDiscardPrompt()
self:RenderHand(false)
self:RenderPiles()
self:RenderCombat()
end
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
method('PlayCard', `if self:IsDiscardSelecting() == true then
self:SelectDiscardSlot(slot)
return
end
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return return
end end
if self.Hand == nil then if self.Hand == nil then
@@ -4163,66 +4410,52 @@ if self.Energy < c.cost then
return return
end end
self.Energy = self.Energy - c.cost self.Energy = self.Energy - c.cost
if c.kind == "Attack" then self:ResolveCardEffects(cardId, c, false)
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) table.remove(self.Hand, slot)
if c.kind ~= "Power" then if c.kind ~= "Power" then
table.insert(self.DiscardPile, cardId) table.insert(self.DiscardPile, cardId)
end end
if c.draw ~= nil then self:RenderHand(false)
self:DrawCards(c.draw, true) self:RenderPiles()
self:RenderCombat()
if self:BeginDiscardSelection(c) == true then
return
end end
self:RenderHand(false) self:RenderHand(false)
self:RenderPiles() self:RenderPiles()
self:RenderCombat() self:RenderCombat()
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('OnCardButton', `if self:IsDiscardSelecting() == true then
self:SelectDiscardSlot(slot)
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('FindMonsterAtTouch', `local best = 0
local bestDist = 200
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
local wp = m.entity.TransformComponent.WorldPosition
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
local dx = sp.x - touchPoint.x
local dy = sp.y - touchPoint.y
local d = math.sqrt(dx * dx + dy * dy)
if d < bestDist then
bestDist = d
best = i
end
end
end
return best`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }], 0, 'number'),
method('RenderTargetFrames', `local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
local shownTarget = self.TargetIndex
if dragActive == true then shownTarget = self.DragTargetIndex end
for i = 1, #self.Monsters do
local m = self.Monsters[i]
local active = false
if m ~= nil and m.alive == true and i == shownTarget then active = true end
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetFrame", active)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
end`),
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return return
end end
@@ -4233,15 +4466,16 @@ if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
_TimerService:ClearTimer(self.CardHoverTweenId) _TimerService:ClearTimer(self.CardHoverTweenId)
self.CardHoverTweenId = 0 self.CardHoverTweenId = 0
end end
local cardXs = { ${CARD_XS.join(', ')} } for i = 1, 10 do
for i = 1, 5 do
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.UIScale = Vector3(1, 1, 1) e.UITransformComponent.UIScale = Vector3(1, 1, 1)
e.UITransformComponent.anchoredPosition = Vector2(cardXs[i], 0) e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
end end
end end
self.DragSlot = slot`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), self.DragSlot = slot
self.DragTargetIndex = 0
self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('OnCardDrag', `if self.DragSlot ~= slot then method('OnCardDrag', `if self.DragSlot ~= slot then
return return
end end
@@ -4249,6 +4483,16 @@ local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tos
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
local ui = _UILogic:ScreenToUIPosition(touchPoint) local ui = _UILogic:ScreenToUIPosition(touchPoint)
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360) e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
end
local cardId = self.Hand[slot]
local c = nil
if cardId ~= nil then c = self.Cards[cardId] end
if c ~= nil and c.kind == "Attack" then
local best = self:FindMonsterAtTouch(touchPoint)
if best ~= self.DragTargetIndex then
self.DragTargetIndex = best
self:RenderTargetFrames()
end
end`, [ end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
@@ -4257,17 +4501,20 @@ end`, [
return return
end end
self.DragSlot = 0 self.DragSlot = 0
local cardXs = { ${CARD_XS.join(', ')} }
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(cardXs[slot], 0) e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
e.UITransformComponent.UIScale = Vector3(1, 1, 1) e.UITransformComponent.UIScale = Vector3(1, 1, 1)
end end
self:ResolveCardDrop(slot, touchPoint)`, [ self:ResolveCardDrop(slot, touchPoint)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]), ]),
method('ResolveCardDrop', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then method('ResolveCardDrop', `if self:IsDiscardSelecting() == true then
self:SelectDiscardSlot(slot)
return
end
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return return
end end
local cardId = self.Hand[slot] local cardId = self.Hand[slot]
@@ -4279,27 +4526,18 @@ if c == nil then
return return
end end
if c.kind == "Attack" then if c.kind == "Attack" then
local best = 0 local best = self.DragTargetIndex or 0
local bestDist = 200 if best <= 0 then best = self:FindMonsterAtTouch(touchPoint) end
for i = 1, #self.Monsters do self.DragTargetIndex = 0
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 if best > 0 then
self.TargetIndex = best self.TargetIndex = best
self:PlayCard(slot) self:PlayCard(slot)
else
self:RenderTargetFrames()
end end
else else
self.DragTargetIndex = 0
self:RenderTargetFrames()
local ui = _UILogic:ScreenToUIPosition(touchPoint) local ui = _UILogic:ScreenToUIPosition(touchPoint)
if ui.y > -180 then if ui.y > -180 then
self:PlayCard(slot) self:PlayCard(slot)
@@ -4542,12 +4780,21 @@ if self.CombatOver == true then
return return
end end
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`), _TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`),
method('ClearCombatCards', `self.DrawPile = {}
self.DiscardPile = {}
self.Hand = {}
self.DiscardSelectRemaining = 0
self.DiscardSelectTotal = 0
self:UpdateDiscardPrompt()
self:RenderHand(false)
self:RenderPiles()`),
method('CheckCombatEnd', `local anyAlive = false method('CheckCombatEnd', `local anyAlive = false
for i = 1, #self.Monsters do for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then anyAlive = true; break end if self.Monsters[i].alive == true then anyAlive = true; break end
end end
if anyAlive == false then if anyAlive == false then
self.CombatOver = true self.CombatOver = true
self:ClearCombatCards()
self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult()) self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult())
self:ApplyRelics("combatEnd") self:ApplyRelics("combatEnd")
self:ApplyRelics("combatReward") self:ApplyRelics("combatReward")
@@ -4737,7 +4984,12 @@ return table.concat(parts, " ")`, [
end end
end end
self:SetText(base .. "/Intent", t) self:SetText(base .. "/Intent", t)
self:SetEntityEnabled(base .. "/TargetFrame", i == self.TargetIndex) local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
local shownTarget = self.TargetIndex
if dragActive == true then shownTarget = self.DragTargetIndex end
self:SetEntityEnabled(base .. "/TargetFrame", i == shownTarget)
self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive)
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent") local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
if intent.kind == "Attack" then if intent.kind == "Attack" then
@@ -4775,9 +5027,51 @@ end
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb) self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb)
self:RenderRun()`), self:RenderRun()`),
method('ShowDmgPop', `local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot) .. "/DmgPop" method('ShowDmgPop', `local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot) .. "/DmgPop"
self:SetText(base, "-" .. string.format("%d", amount)) local pop = _EntityService:GetEntityByPath(base)
self:SetText(base, "")
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [ local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
local shown = tostring(math.max(0, math.floor(amount)))
if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
end
local digits = {}
for i = 1, string.len(shown) do
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
end
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitPath = base .. "/Digit" .. tostring(i)
local digitEntity = _EntityService:GetEntityByPath(digitPath)
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
if digits[i] ~= nil then
digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
if digitEntity.UITransformComponent ~= nil then
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
end
self:SetEntityEnabled(digitPath, true)
else
self:SetEntityEnabled(digitPath, false)
end
end
end
if pop ~= nil and pop.UITransformComponent ~= nil then
pop.UITransformComponent.anchoredPosition = Vector2(0, 76)
end
local startY = 76
for i = 1, 6 do
_TimerService:SetTimerOnce(function()
local p = _EntityService:GetEntityByPath(base)
if p ~= nil and p.UITransformComponent ~= nil then
p.UITransformComponent.anchoredPosition = Vector2(0, startY + i * 7)
end
end, 0.045 * i)
end
_TimerService:SetTimerOnce(function()
self:SetEntityEnabled(base, false)
end, 0.48)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
]), ]),

File diff suppressed because it is too large Load Diff