5 Commits

15 changed files with 393 additions and 42 deletions

File diff suppressed because one or more lines are too long

View File

@@ -14,7 +14,7 @@
"Defend": { "Defend": {
"name": "아이언 바디", "name": "아이언 바디",
"cost": 1, "cost": 1,
"kind": "Skill", "kind": "Attack",
"block": 5, "block": 5,
"desc": "방어도 5", "desc": "방어도 5",
"image": "7648c3b8e1ca44fc8ec353561207a670", "image": "7648c3b8e1ca44fc8ec353561207a670",
@@ -89,8 +89,8 @@
"name": "분노", "name": "분노",
"cost": 1, "cost": 1,
"kind": "Power", "kind": "Power",
"powerEffect": "strengthPerTurn", "aoe": true,
"value": 1, "damage": 4,
"desc": "매 턴 시작 시 힘 +1", "desc": "매 턴 시작 시 힘 +1",
"image": "379d86e3de064959aa4612f71e84ccfb", "image": "379d86e3de064959aa4612f71e84ccfb",
"class": "warrior", "class": "warrior",
@@ -479,6 +479,7 @@
"desc": "피해를 8 줍니다. 약화를 1 부여합니다.", "desc": "피해를 8 줍니다. 약화를 1 부여합니다.",
"weak": 1, "weak": 1,
"damage": 8, "damage": 8,
"cardPlayedDamage": 2,
"image": "92a5020c978c46bdabab910598118b86" "image": "92a5020c978c46bdabab910598118b86"
}, },
"LeadingStrike": { "LeadingStrike": {
@@ -652,6 +653,8 @@
"class": "bandit", "class": "bandit",
"rarity": "unique", "rarity": "unique",
"desc": "피해를 8만큼 X번 줍니다.", "desc": "피해를 8만큼 X번 줍니다.",
"useAllEnergy": true,
"xDamagePerEnergy": 8,
"draw": 1, "draw": 1,
"image": "92a5020c978c46bdabab910598118b86" "image": "92a5020c978c46bdabab910598118b86"
}, },
@@ -1076,6 +1079,7 @@
"rarity": "legend", "rarity": "legend",
"desc": "피해를 1 줍니다. 이번 전투 동안 뽑은 카드 1장당 피해량이 1 증가합니다.", "desc": "피해를 1 줍니다. 이번 전투 동안 뽑은 카드 1장당 피해량이 1 증가합니다.",
"damage": 1, "damage": 1,
"damagePerCardDrawnThisCombat": 1,
"image": "b1360ed0c4b942309d240634b8f36872" "image": "b1360ed0c4b942309d240634b8f36872"
}, },
"Malaise": { "Malaise": {
@@ -1085,7 +1089,8 @@
"class": "bandit", "class": "bandit",
"rarity": "legend", "rarity": "legend",
"desc": "적이 힘을 X 잃습니다. 약화를 X 부여합니다. 소멸.", "desc": "적이 힘을 X 잃습니다. 약화를 X 부여합니다. 소멸.",
"weak": 3, "useAllEnergy": true,
"xWeakPerEnergy": 1,
"image": "0946f69d84464df29b24b94c744c868d" "image": "0946f69d84464df29b24b94c744c868d"
}, },
"Adrenaline": { "Adrenaline": {
@@ -1233,9 +1238,7 @@
"class": "bandit", "class": "bandit",
"rarity": "legend", "rarity": "legend",
"desc": "공격 카드가 막히지 않은 피해를 줄 때마다, 중독을 1 부여합니다.", "desc": "공격 카드가 막히지 않은 피해를 줄 때마다, 중독을 1 부여합니다.",
"poison": 1, "attackPoison": 1,
"powerEffect": "strengthPerTurn",
"value": 1,
"image": "19361e72087946b1888684185b40d935" "image": "19361e72087946b1888684185b40d935"
}, },
"MasterPlanner": { "MasterPlanner": {
@@ -1279,9 +1282,7 @@
"class": "bandit", "class": "bandit",
"rarity": "legend", "rarity": "legend",
"desc": "카드를 사용할 때마다, 무작위 적에게 피해를 4 줍니다.", "desc": "카드를 사용할 때마다, 무작위 적에게 피해를 4 줍니다.",
"powerEffect": "strengthPerTurn", "cardPlayedRandomDamage": 4,
"value": 1,
"damage": 4,
"image": "19361e72087946b1888684185b40d935" "image": "19361e72087946b1888684185b40d935"
}, },
"Abrasive": { "Abrasive": {
@@ -1315,8 +1316,8 @@
"class": "bandit", "class": "bandit",
"rarity": "legend", "rarity": "legend",
"desc": "불가침을 2 얻습니다. 내 턴 종료 시 민첩을 1 잃습니다.", "desc": "불가침을 2 얻습니다. 내 턴 종료 시 민첩을 1 잃습니다.",
"powerEffect": "blockPerTurn", "intangible": 2,
"value": 8, "endTurnDexLoss": 1,
"image": "0946f69d84464df29b24b94c744c868d" "image": "0946f69d84464df29b24b94c744c868d"
} }
}, },

10
docs/attack-poison.md Normal file
View File

@@ -0,0 +1,10 @@
# 공격 적중 독
`attackPoison`은 전투 중 파워가 들고 있는 공용 필드입니다.
동작:
- 공격 카드가 실제 피해를 주면 독을 부여합니다.
- `aoe` 공격이면 모든 적에게 같은 양의 독을 붙입니다.
- `Envenom` 같은 카드가 이 필드를 사용합니다.

5
docs/card-play-damage.md Normal file
View File

@@ -0,0 +1,5 @@
# 카드 사용 시 피해
`cardPlayedDamage`는 카드를 사용할 때마다 현재 대상에게 체력을 직접 깎는 공용 효과입니다. 방어도는 무시하고, 같은 필드를 다른 카드에도 그대로 붙여 재사용할 수 있습니다.
`cardPlayedRandomDamage`는 같은 시점에 살아 있는 적 하나를 랜덤으로 골라 체력을 직접 깎습니다. `Strangle``SerpentForm` 같은 카드가 이 계열을 씁니다.

8
docs/draw-count.md Normal file
View File

@@ -0,0 +1,8 @@
# 전투 드로우 누적
`damagePerCardDrawnThisCombat`은 이번 전투 동안 실제로 뽑힌 카드 수를 기준으로 공격력을 올리는 공용 필드입니다.
적용 예시:
- `Murder`: 이번 전투 동안 뽑은 카드 1장당 피해량이 1 증가

5
docs/intangible.md Normal file
View File

@@ -0,0 +1,5 @@
# 불가침
`intangible`는 카드를 사용할 때 플레이어에게 불가침 수치를 부여하는 공용 필드입니다. 불가침이 남아 있는 동안 받는 피해는 1로 줄어들고, 턴이 끝날 때 1씩 감소합니다.
`endTurnDexLoss`는 그 카드가 활성화된 동안 매 턴 종료 시 민첩을 잃게 만드는 공용 필드입니다. `WraithForm` 같은 카드가 이 조합을 사용합니다.

14
docs/x-cost.md Normal file
View File

@@ -0,0 +1,14 @@
# X 코스트 카드
`useAllEnergy`는 카드가 사용될 때 남은 에너지를 전부 쓰는 공용 필드입니다.
연동 필드:
- `xDamagePerEnergy`: 에너지 1당 피해량
- `xWeakPerEnergy`: 에너지 1당 약화량
적용 예시:
- `Skewer`: 남은 에너지 전부를 써서 `8 * energy` 피해
- `Malaise`: 남은 에너지 전부를 써서 약화 부여

View File

@@ -95,11 +95,12 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
if (!card || card.unplayable || !canPlayCardNow(card, ctx)) return false; if (!card || card.unplayable || !canPlayCardNow(card, ctx)) return false;
let effectiveCost = card.cost || 0; let effectiveCost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) effectiveCost = 0; if (ctx.handCostZeroThisTurn === true) effectiveCost = 0;
else if (card.useAllEnergy === true) effectiveCost = 1;
else if (card.kind === 'Skill') { else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) effectiveCost = 0; if (ctx.nextSkillCostZero === true) effectiveCost = 0;
else effectiveCost = Math.max(0, effectiveCost - (ctx.skillCostReductionThisTurn || 0)); else effectiveCost = Math.max(0, effectiveCost - (ctx.skillCostReductionThisTurn || 0));
} }
return effectiveCost <= energy; return card.useAllEnergy === true ? true : effectiveCost <= energy;
}); });
const powers = entries.filter((x) => cards[x.id].kind === 'Power'); const powers = entries.filter((x) => cards[x.id].kind === 'Power');
const attacks = entries.filter((x) => cards[x.id].kind === 'Attack'); const attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
@@ -107,6 +108,7 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
const effectiveCost = (card) => { const effectiveCost = (card) => {
let cost = card.cost || 0; let cost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) cost = 0; if (ctx.handCostZeroThisTurn === true) cost = 0;
else if (card.useAllEnergy === true) cost = 1;
else if (card.kind === 'Skill') { else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) cost = 0; if (ctx.nextSkillCostZero === true) cost = 0;
else cost = Math.max(0, cost - (ctx.skillCostReductionThisTurn || 0)); else cost = Math.max(0, cost - (ctx.skillCostReductionThisTurn || 0));
@@ -146,7 +148,7 @@ export function simulateCombat(data, rng, stats) {
const exhaust = []; const exhaust = [];
let hand = []; let hand = [];
let pHp = PLAYER_HP, pBlock = 0; let pHp = PLAYER_HP, pBlock = 0;
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0; let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0, pIntangible = 0;
let blockGainMultiplier = 1; let blockGainMultiplier = 1;
let handCostZeroThisTurn = false; let handCostZeroThisTurn = false;
let drawDisabledThisTurn = false; let drawDisabledThisTurn = false;
@@ -156,6 +158,7 @@ export function simulateCombat(data, rng, stats) {
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1; let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
let nextTurnAddCards = []; let nextTurnAddCards = [];
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0; let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
let cardsDrawnThisCombat = 0;
let energy = 0; let energy = 0;
const powers = []; const powers = [];
const mob = monsters.map((m) => ({ const mob = monsters.map((m) => ({
@@ -172,6 +175,7 @@ export function simulateCombat(data, rng, stats) {
if (drawPile.length === 0) break; if (drawPile.length === 0) break;
const card = drawPile.pop(); const card = drawPile.pop();
drawn.push(card); drawn.push(card);
cardsDrawnThisCombat++;
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화) // 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
if (hand.length >= 10) { if (hand.length >= 10) {
discard.push(card); discard.push(card);
@@ -228,6 +232,7 @@ export function simulateCombat(data, rng, stats) {
if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn; if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn;
if (c.damagePerDiscardedThisTurn) base += turnDiscardedCards * c.damagePerDiscardedThisTurn; if (c.damagePerDiscardedThisTurn) base += turnDiscardedCards * c.damagePerDiscardedThisTurn;
if (c.damagePerSkillInHand) base += countOtherHandSkills(id) * c.damagePerSkillInHand; if (c.damagePerSkillInHand) base += countOtherHandSkills(id) * c.damagePerSkillInHand;
if (c.damagePerCardDrawnThisCombat) base += cardsDrawnThisCombat * c.damagePerCardDrawnThisCombat;
if (base < 0) base = 0; if (base < 0) base = 0;
return base; return base;
} }
@@ -277,9 +282,10 @@ export function simulateCombat(data, rng, stats) {
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn; if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true; if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true; if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
const xEnergy = costSpent || 0;
if (c.kind === 'Attack') { if (c.kind === 'Attack') {
if (alive.length && c.damage) { if (alive.length && (c.damage || c.xDamagePerEnergy)) {
const baseDamage = attackBaseForCard(id, c); const baseDamage = c.xDamagePerEnergy ? xEnergy * c.xDamagePerEnergy : attackBaseForCard(id, c);
const bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast) const bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast)
? c.bonusHitsWhenOtherHandAtLeast : 0; ? c.bonusHitsWhenOtherHandAtLeast : 0;
const hitN = (c.hits || 1) + bonusHits; const hitN = (c.hits || 1) + bonusHits;
@@ -295,6 +301,8 @@ export function simulateCombat(data, rng, stats) {
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv; const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
const r2 = applyDamage(m2.hp, m2.block, d2); const r2 = applyDamage(m2.hp, m2.block, d2);
m2.hp = r2.hp; m2.block = r2.block; m2.hp = r2.hp; m2.block = r2.block;
const attackPoison = powerFieldTotal('attackPoison');
if (d2 > 0 && attackPoison > 0) m2.poison += attackPoison;
if (m2.hp <= 0) m2.alive = false; if (m2.hp <= 0) m2.alive = false;
} }
} else { } else {
@@ -306,6 +314,8 @@ export function simulateCombat(data, rng, stats) {
const r = applyDamage(target.hp, target.block, dmg); const r = applyDamage(target.hp, target.block, dmg);
target.hp = r.hp; target.block = r.block; target.hp = r.hp; target.block = r.block;
} }
const attackPoison = powerFieldTotal('attackPoison');
if (dmg > 0 && attackPoison > 0) target.poison += attackPoison;
if (target.hp <= 0) target.alive = false; if (target.hp <= 0) target.alive = false;
} }
} }
@@ -314,9 +324,10 @@ export function simulateCombat(data, rng, stats) {
if (recordStats) powers.push(id); if (recordStats) powers.push(id);
} else { } else {
if (c.block) blockGained = addBlock(c.block); if (c.block) blockGained = addBlock(c.block);
if ((c.weak || c.vuln || c.poison) && alive.length) { const weakAmount = (c.weak || 0) + (c.xWeakPerEnergy || 0) * xEnergy;
if ((weakAmount || c.vuln || c.poison) && alive.length) {
const target = chooseTarget(alive, 0); const target = chooseTarget(alive, 0);
if (c.weak) target.weak += c.weak; if (weakAmount) target.weak += weakAmount;
if (c.vuln) target.vuln += c.vuln; if (c.vuln) target.vuln += c.vuln;
if (c.poison) target.poison += c.poison; if (c.poison) target.poison += c.poison;
} }
@@ -327,6 +338,7 @@ export function simulateCombat(data, rng, stats) {
if (c.selfVuln) pVuln += c.selfVuln; if (c.selfVuln) pVuln += c.selfVuln;
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP); if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
if (c.gainEnergy) energy += c.gainEnergy; if (c.gainEnergy) energy += c.gainEnergy;
if (c.intangible) pIntangible += c.intangible;
queueNextTurnEffects(c); queueNextTurnEffects(c);
let drawnCards = []; let drawnCards = [];
if (c.draw) drawnCards = drawnCards.concat(draw(c.draw)); if (c.draw) drawnCards = drawnCards.concat(draw(c.draw));
@@ -340,6 +352,25 @@ export function simulateCombat(data, rng, stats) {
} }
} }
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv); if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
if (c.cardPlayedDamage && alive.length) {
const target = chooseTarget(aliveList(), 0);
if (target && target.alive) {
target.hp -= c.cardPlayedDamage;
dmg += c.cardPlayedDamage;
if (target.hp <= 0) target.alive = false;
}
}
if (c.cardPlayedRandomDamage && alive.length) {
const pool = aliveList();
if (pool.length) {
const target = pool[Math.floor(rng() * pool.length)];
if (target) {
target.hp -= c.cardPlayedRandomDamage;
dmg += c.cardPlayedRandomDamage;
if (target.hp <= 0) target.alive = false;
}
}
}
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained); if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
} }
function triggerSly(id) { function triggerSly(id) {
@@ -421,7 +452,7 @@ export function simulateCombat(data, rng, stats) {
const id = hand[idx], c = cards[id]; const id = hand[idx], c = cards[id];
const skillFree = c.kind === 'Skill' && nextSkillCostZero === true; const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
const baseCost = c.cost || 0; const baseCost = c.cost || 0;
const cost = handCostZeroThisTurn === true ? 0 : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost)); const cost = handCostZeroThisTurn === true ? 0 : (c.useAllEnergy === true ? energy : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost)));
energy -= cost; energy -= cost;
resolveCardEffects(id, c, cost); resolveCardEffects(id, c, cost);
if (c.kind === 'Attack') turnAttackCardsPlayed++; if (c.kind === 'Attack') turnAttackCardsPlayed++;
@@ -446,6 +477,14 @@ export function simulateCombat(data, rng, stats) {
else discard.push(hid); else discard.push(hid);
} }
hand = kept; hand = kept;
for (const pid of powers) {
const pc = cards[pid];
if (pc?.endTurnDexLoss) {
pDex -= pc.endTurnDexLoss;
if (pDex < 0) pDex = 0;
}
}
if (pIntangible > 0) pIntangible--;
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--;
@@ -465,7 +504,9 @@ export function simulateCombat(data, rng, stats) {
if (it.kind === 'Attack') { if (it.kind === 'Attack') {
const atk = calcAttack(it.value, m.str, m.weak, pVuln); const atk = calcAttack(it.value, m.str, m.weak, pVuln);
const beforeHp = pHp; const beforeHp = pHp;
const r = applyDamage(pHp, pBlock, atk); pHp = r.hp; pBlock = r.block; let incoming = atk;
if (pIntangible > 0 && incoming > 1) incoming = 1;
const r = applyDamage(pHp, pBlock, incoming); pHp = r.hp; pBlock = r.block;
if (beforeHp > pHp && pThorns > 0) { if (beforeHp > pHp && pThorns > 0) {
m.hp -= pThorns; m.hp -= pThorns;
if (m.hp <= 0) m.alive = false; if (m.hp <= 0) m.alive = false;

View File

@@ -729,6 +729,13 @@ test("chooseAction: handCostZeroThisTurn lets expensive cards be played", () =>
assert.equal(chooseAction(["Burst"], cards, 0, {}), -1); assert.equal(chooseAction(["Burst"], cards, 0, {}), -1);
}); });
test("chooseAction: useAllEnergy cards remain playable at zero energy", () => {
const cards = {
Skewer: { name: "Skewer", cost: 2, kind: "Attack", useAllEnergy: true, xDamagePerEnergy: 8 },
};
assert.equal(chooseAction(["Skewer"], cards, 0, {}), 0);
});
test("simulateCombat: drawSkillBlock grants block for each drawn skill", () => { test("simulateCombat: drawSkillBlock grants block for each drawn skill", () => {
const data = { const data = {
cards: { cards: {
@@ -777,3 +784,98 @@ test("simulateCombat: damagePerTurn powers damage all enemies at turn start", ()
const r = simulateCombat(data, () => 0.999999); const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true); assert.equal(r.win, true);
}); });
test("simulateCombat: attackPoison power applies poison on attack damage", () => {
const data = {
cards: {
Venom: { name: "Envenom", cost: 2, kind: "Power", attackPoison: 2 },
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 1 },
},
starterDeck: ["Venom", "Strike"],
monsters: [{ name: "Dummy", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.turns, 1);
});
test("simulateCombat: cardPlayedDamage hits the target whenever a card is played", () => {
const data = {
cards: {
Strangle: { name: "Strangle", cost: 1, kind: "Attack", damage: 8, cardPlayedDamage: 2 },
},
starterDeck: ["Strangle"],
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: cardPlayedRandomDamage hits a random enemy on card play", () => {
const data = {
cards: {
SerpentForm: { name: "SerpentForm", cost: 3, kind: "Power", cardPlayedRandomDamage: 4 },
},
starterDeck: ["SerpentForm"],
monsters: [{ name: "Dummy", maxHp: 4, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: intangible cards reduce incoming damage and persist across turns", () => {
const data = {
cards: {
Wraith: { name: "WraithForm", cost: 3, kind: "Power", intangible: 2, endTurnDexLoss: 1, innate: true },
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 1 },
},
starterDeck: ["Wraith", "Strike"],
monsters: [{ name: "Dummy", maxHp: 1, intents: [{ kind: "Attack", value: 10 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: useAllEnergy skewer consumes all energy for damage", () => {
const data = {
cards: {
Skewer: { name: "Skewer", cost: 2, kind: "Attack", useAllEnergy: true, xDamagePerEnergy: 8 },
},
starterDeck: ["Skewer"],
monsters: [{ name: "Dummy", maxHp: 24, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: useAllEnergy malaise scales weak with energy spent", () => {
const data = {
cards: {
Malaise: { name: "Malaise", cost: 2, kind: "Skill", useAllEnergy: true, xWeakPerEnergy: 1 },
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 1 },
},
starterDeck: ["Malaise", "Strike"],
monsters: [{ name: "Dummy", maxHp: 1, intents: [{ kind: "Attack", value: 10 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: damagePerCardDrawnThisCombat scales murder", () => {
const data = {
cards: {
Murder: { name: "Murder", cost: 3, kind: "Attack", damage: 1, damagePerCardDrawnThisCombat: 1 },
Filler1: { name: "Filler1", cost: 99, kind: "Skill" },
Filler2: { name: "Filler2", cost: 99, kind: "Skill" },
Filler3: { name: "Filler3", cost: 99, kind: "Skill" },
Filler4: { name: "Filler4", cost: 99, kind: "Skill" },
Filler5: { name: "Filler5", cost: 99, kind: "Skill" },
},
starterDeck: ["Murder", "Filler1", "Filler2", "Filler3", "Filler4", "Filler5"],
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
};
const stats = {};
const r = simulateCombat(data, () => 0.999999, stats);
assert.equal(r.win, true);
assert.ok(stats.Murder.damage > 1);
});

View File

@@ -47,7 +47,9 @@ end
local cost = c.cost or 0 local cost = c.cost or 0
local skillFree = false local skillFree = false
if self.HandCostZeroThisTurn == true then if self.HandCostZeroThisTurn == true then
\tcost = 0 cost = 0
elseif c.useAllEnergy == true then
cost = self.Energy
end end
if c.kind == "Skill" and self.NextSkillCostZero == true then if c.kind == "Skill" and self.NextSkillCostZero == true then
cost = 0 cost = 0
@@ -61,7 +63,7 @@ if self.Energy < cost then
return return
end end
self.Energy = self.Energy - cost self.Energy = self.Energy - cost
self:ResolveCardEffects(cardId, slot, c, false) self:ResolveCardEffects(cardId, slot, c, false, cost)
if c.kind == "Attack" then if c.kind == "Attack" then
self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1 self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1
end end
@@ -73,6 +75,12 @@ end
if self:HasPowerField("cardPlayedBlock") == true then if self:HasPowerField("cardPlayedBlock") == true then
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock")) self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
end end
if c.cardPlayedDamage ~= nil and c.cardPlayedDamage > 0 then
self:DealDirectDamageToTarget(c.cardPlayedDamage)
end
if c.cardPlayedRandomDamage ~= nil and c.cardPlayedRandomDamage > 0 then
self:DealDirectDamageToRandomMonster(c.cardPlayedRandomDamage)
end
table.remove(self.Hand, slot) table.remove(self.Hand, slot)
if c.exhaust == true then if c.exhaust == true then
if self.ExhaustPile == nil then self.ExhaustPile = {} end if self.ExhaustPile == nil then self.ExhaustPile = {} end
@@ -247,6 +255,12 @@ if m.block > 0 and pierce ~= true then
dmg = dmg - absorbed dmg = dmg - absorbed
end end
m.hp = m.hp - dmg m.hp = m.hp - dmg
if dmg > 0 then
local poison = self:AddPowerFieldTotal("attackPoison")
if poison ~= nil and poison > 0 then
m.poison = (m.poison or 0) + poison
end
end
self:MonsterHitMotion(m.slot) self:MonsterHitMotion(m.slot)
if m.hp <= 0 then if m.hp <= 0 then
m.hp = 0 m.hp = 0
@@ -255,6 +269,48 @@ end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
]), ]),
method('DealDirectDamageToTarget', `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
m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount)
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' },
]),
method('DealDirectDamageToRandomMonster', `local alive = {}
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m ~= nil and m.alive == true then
table.insert(alive, m)
end
end
if #alive <= 0 then
return
end
local m = alive[math.random(1, #alive)]
if m == nil then
return
end
m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount)
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' },
]),
method('PlayAttackFx', `local m = self.Monsters[targetIndex] method('PlayAttackFx', `local m = self.Monsters[targetIndex]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
self:DealDamageToTarget(damage, pierce) self:DealDamageToTarget(damage, pierce)
@@ -320,6 +376,12 @@ _TimerService:SetTimerOnce(function()
dmg = dmg - absorbed dmg = dmg - absorbed
end end
m.hp = m.hp - dmg m.hp = m.hp - dmg
if dmg > 0 then
local poison = self:AddPowerFieldTotal("attackPoison")
if poison ~= nil and poison > 0 then
m.poison = (m.poison or 0) + poison
end
end
self:ShowDmgPop(i, dmg) self:ShowDmgPop(i, dmg)
self:MonsterHitMotion(i) self:MonsterHitMotion(i)
if m.hp <= 0 then if m.hp <= 0 then
@@ -353,6 +415,9 @@ if self.PlayerBlock > 0 then
self.PlayerBlock = self.PlayerBlock - absorbed self.PlayerBlock = self.PlayerBlock - absorbed
dmg = dmg - absorbed dmg = dmg - absorbed
end end
if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then
dmg = 1
end
if dmg > 0 then if dmg > 0 then
self.PlayerHp = self.PlayerHp - dmg self.PlayerHp = self.PlayerHp - dmg
local reflect = self.PlayerThorns or 0 local reflect = self.PlayerThorns or 0

View File

@@ -246,6 +246,7 @@ if self.ClayBlockNext > 0 then
end end
self.TurnAttackMultiplier = self.NextTurnAttackMultiplier or 1 self.TurnAttackMultiplier = self.NextTurnAttackMultiplier or 1
self.NextTurnAttackMultiplier = 1 self.NextTurnAttackMultiplier = 1
self.CardsDrawnThisCombat = self.CardsDrawnThisCombat or 0
self.HandCostZeroThisTurn = false self.HandCostZeroThisTurn = false
self.DrawDisabledThisTurn = false self.DrawDisabledThisTurn = false
local powerTurnDraw = 0 local powerTurnDraw = 0
@@ -425,6 +426,19 @@ for i = 1, #self.Hand do
\tend \tend
end end
self.Hand = kept self.Hand = kept
if self.PlayerPowers ~= nil then
for i = 1, #self.PlayerPowers do
local pc = self.Cards[self.PlayerPowers[i]]
if pc ~= nil and pc.endTurnDexLoss ~= nil and pc.endTurnDexLoss > 0 then
self.PlayerDex = self.PlayerDex - pc.endTurnDexLoss
if self.PlayerDex < 0 then self.PlayerDex = 0 end
end
end
end
if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then
self.PlayerIntangible = self.PlayerIntangible - 1
if self.PlayerIntangible < 0 then self.PlayerIntangible = 0 end
end
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)
@@ -445,6 +459,7 @@ for i = 1, amount do
\tend \tend
\tlocal cardId = table.remove(self.DrawPile) \tlocal cardId = table.remove(self.DrawPile)
\ttable.insert(drawnCards, cardId) \ttable.insert(drawnCards, cardId)
\tself.CardsDrawnThisCombat = (self.CardsDrawnThisCombat or 0) + 1
\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) \t\tself:TriggerSly(cardId)

View File

@@ -308,6 +308,9 @@ end
if c.damagePerSkillInHand ~= nil then if c.damagePerSkillInHand ~= nil then
base2 = base2 + self:CountOtherHandSkills(slot) * c.damagePerSkillInHand base2 = base2 + self:CountOtherHandSkills(slot) * c.damagePerSkillInHand
end end
if c.damagePerCardDrawnThisCombat ~= nil then
base2 = base2 + (self.CardsDrawnThisCombat or 0) * c.damagePerCardDrawnThisCombat
end
if base2 < 0 then if base2 < 0 then
base2 = 0 base2 = 0
end end
@@ -388,10 +391,20 @@ end
if c.drawDisabledThisTurn == true then if c.drawDisabledThisTurn == true then
self.DrawDisabledThisTurn = true self.DrawDisabledThisTurn = true
end end
local xEnergy = energySpent or 0
local weakAmount = c.weak or 0
local vulnAmount = c.vuln or 0
local poisonAmount = c.poison or 0
if c.xWeakPerEnergy ~= nil and c.xWeakPerEnergy > 0 then
weakAmount = weakAmount + xEnergy * c.xWeakPerEnergy
end
if c.kind == "Attack" then if c.kind == "Attack" then
if c.damage ~= nil then if c.damage ~= nil or c.xDamagePerEnergy ~= nil then
self:PlayerAttackMotion() self:PlayerAttackMotion()
local baseDmg = self:AttackBaseForCard(slot, c) local baseDmg = self:AttackBaseForCard(slot, c)
if c.xDamagePerEnergy ~= nil and c.xDamagePerEnergy > 0 then
baseDmg = xEnergy * c.xDamagePerEnergy
end
local total = 0 local total = 0
local hitN = c.hits or 1 local hitN = c.hits or 1
if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then
@@ -446,8 +459,11 @@ end
if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then
self.Energy = self.Energy + c.gainEnergy self.Energy = self.Energy + c.gainEnergy
end end
if c.intangible ~= nil and c.intangible > 0 then
self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible
end
self:QueueNextTurnEffects(c) self:QueueNextTurnEffects(c)
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil or c.xWeakPerEnergy ~= nil then
local tm = self.Monsters[self.TargetIndex] local tm = self.Monsters[self.TargetIndex]
if tm == nil or tm.alive ~= true then if tm == nil or tm.alive ~= true then
for i = 1, #self.Monsters do for i = 1, #self.Monsters do
@@ -455,10 +471,10 @@ if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
end end
end end
if tm ~= nil and tm.alive == true then if tm ~= nil and tm.alive == true then
if c.weak ~= nil then tm.weak = tm.weak + c.weak end if weakAmount ~= nil and weakAmount > 0 then tm.weak = tm.weak + weakAmount end
if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end if poisonAmount ~= nil and poisonAmount > 0 then tm.poison = (tm.poison or 0) + poisonAmount end
if c.vuln ~= nil then if vulnAmount ~= nil and vulnAmount > 0 then
tm.vuln = tm.vuln + c.vuln tm.vuln = tm.vuln + vulnAmount
if self:HasRelic("championBelt") then if self:HasRelic("championBelt") then
tm.weak = tm.weak + 1 tm.weak = tm.weak + 1
end end
@@ -500,13 +516,14 @@ 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: 'c' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'energySpent' },
]), ]),
method('TriggerSly', `local c = self.Cards[cardId] method('TriggerSly', `local c = self.Cards[cardId]
if c == nil or c.sly ~= true then if c == nil or c.sly ~= true then
return return
end end
self:Toast("교활 발동: " .. c.name) self:Toast("교활 발동: " .. c.name)
self:ResolveCardEffects(cardId, 0, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]), self:ResolveCardEffects(cardId, 0, c, true, 0)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
method('DiscardHandCard', `if self.Hand == nil then method('DiscardHandCard', `if self.Hand == nil then
return return
end end

View File

@@ -68,6 +68,10 @@ self:SetHpBar("/ui/RunUIGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, s
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then
if pb ~= "" then pb = pb .. " " end
pb = pb .. "불가침" .. tostring(self.PlayerIntangible)
end
if self.PlayerDex ~= nil and self.PlayerDex > 0 then if self.PlayerDex ~= nil and self.PlayerDex > 0 then
if pb ~= "" then pb = pb .. " " end if pb ~= "" then pb = pb .. " " end
pb = pb .. "민첩+" .. tostring(self.PlayerDex) pb = pb .. "민첩+" .. tostring(self.PlayerDex)

View File

@@ -68,6 +68,7 @@ self.MaxEnergy = 3
self.Turn = 0 self.Turn = 0
self.PlayerBlock = 0 self.PlayerBlock = 0
self.BlockGainMultiplier = 1 self.BlockGainMultiplier = 1
self.CardsDrawnThisCombat = 0
self.HandCostZeroThisTurn = false self.HandCostZeroThisTurn = false
self.DrawDisabledThisTurn = false self.DrawDisabledThisTurn = false
self.NextSkillCostZero = false self.NextSkillCostZero = false
@@ -77,6 +78,7 @@ self.PlayerDex = 0
self.PlayerThorns = 0 self.PlayerThorns = 0
self.PlayerWeak = 0 self.PlayerWeak = 0
self.PlayerVuln = 0 self.PlayerVuln = 0
self.PlayerIntangible = 0
self.PlayerPowers = {} self.PlayerPowers = {}
self.FightAttackCount = 0 self.FightAttackCount = 0
self.TurnAttackCardsPlayed = 0 self.TurnAttackCardsPlayed = 0

View File

@@ -162,8 +162,14 @@ function luaCardsTable(cards) {
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`); if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`);
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`); if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`);
if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${c.damagePerSkillInHand}`); if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${c.damagePerSkillInHand}`);
if (c.damagePerCardDrawnThisCombat != null) fields.push(`damagePerCardDrawnThisCombat = ${c.damagePerCardDrawnThisCombat}`);
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`); if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`);
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`); if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`);
if (c.attackPoison != null) fields.push(`attackPoison = ${c.attackPoison}`);
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`); if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`);
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`); if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`);
if (c.block != null) fields.push(`block = ${c.block}`); if (c.block != null) fields.push(`block = ${c.block}`);
@@ -198,6 +204,9 @@ function luaCardsTable(cards) {
if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = true'); if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = true');
if (c.drawDisabledThisTurn === true) fields.push('drawDisabledThisTurn = true'); if (c.drawDisabledThisTurn === true) fields.push('drawDisabledThisTurn = true');
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true'); if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
if (c.useAllEnergy === true) fields.push('useAllEnergy = true');
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${c.xDamagePerEnergy}`);
if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${c.xWeakPerEnergy}`);
if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${c.nextTurnBlock}`); if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${c.nextTurnBlock}`);
if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`); if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`);
if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true'); if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true');