feat: 도적 공용 효과 정리
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, rarityForRoll,
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, calcEnemyAttack, rarityForRoll,
|
||||
} from './sim-balance.mjs';
|
||||
|
||||
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
||||
@@ -758,6 +758,14 @@ test("chooseAction: useAllEnergy cards remain playable at zero energy", () => {
|
||||
assert.equal(chooseAction(["Skewer"], cards, 0, {}), 0);
|
||||
});
|
||||
|
||||
test("chooseAction: combatCardCostReduction discounts the same card across combat", () => {
|
||||
const cards = {
|
||||
Sleeve: { name: "UpMySleeve", cost: 2, kind: "Skill" },
|
||||
};
|
||||
assert.equal(chooseAction(["Sleeve"], cards, 1, { combatCardCostReduction: { Sleeve: 1 } }), 0);
|
||||
assert.equal(chooseAction(["Sleeve"], cards, 1, {}), -1);
|
||||
});
|
||||
|
||||
test("simulateCombat: drawSkillBlock grants block for each drawn skill", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
@@ -821,10 +829,181 @@ test("simulateCombat: attackPoison power applies poison on attack damage", () =>
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
test("simulateCombat: cardPlayedDamage hits the target whenever a card is played", () => {
|
||||
test("simulateCombat: skillSlyOnPlay makes later discards of the same skill trigger sly effects", () => {
|
||||
const shared = {
|
||||
cards: {
|
||||
MasterPlanner: { name: "MasterPlanner", cost: 1, kind: "Skill", poison: 1, discardAll: true },
|
||||
},
|
||||
starterDeck: ["MasterPlanner", "MasterPlanner"],
|
||||
monsters: [{ name: "Dummy", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] }],
|
||||
};
|
||||
const withSly = simulateCombat({
|
||||
...shared,
|
||||
cards: {
|
||||
MasterPlanner: { name: "MasterPlanner", cost: 1, kind: "Skill", poison: 1, discardAll: true, skillSlyOnPlay: true },
|
||||
},
|
||||
}, () => 0.999999);
|
||||
const withoutSly = simulateCombat(shared, () => 0.999999);
|
||||
assert.equal(withSly.win, true);
|
||||
assert.equal(withSly.turns, 1);
|
||||
assert.ok(withoutSly.turns > withSly.turns);
|
||||
});
|
||||
|
||||
test("simulateCombat: randomTargetEachHit can spread hits across alive enemies", () => {
|
||||
const shared = {
|
||||
cards: {
|
||||
Ricochet: { name: "Ricochet", cost: 2, kind: "Attack", damage: 3, hits: 4, randomTargetEachHit: true },
|
||||
},
|
||||
starterDeck: ["Ricochet"],
|
||||
monsters: [
|
||||
{ name: "DummyA", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] },
|
||||
{ name: "DummyB", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] },
|
||||
],
|
||||
};
|
||||
const makeRng = () => {
|
||||
const seq = [0, 0.999999, 0, 0.999999];
|
||||
let i = 0;
|
||||
return () => seq[i++ % seq.length];
|
||||
};
|
||||
const withRicochet = simulateCombat(shared, makeRng());
|
||||
const withoutRicochet = simulateCombat({
|
||||
...shared,
|
||||
cards: {
|
||||
Ricochet: { name: "Ricochet", cost: 2, kind: "Attack", damage: 3, hits: 4 },
|
||||
},
|
||||
}, makeRng());
|
||||
assert.equal(withRicochet.win, true);
|
||||
assert.equal(withRicochet.turns, 1);
|
||||
assert.equal(withoutRicochet.turns, 2);
|
||||
});
|
||||
|
||||
test("calcEnemyAttack: enemyStrengthLossThisTurn reduces enemy attack damage", () => {
|
||||
assert.equal(calcEnemyAttack(10, 6, 0, 0, 6), 10);
|
||||
assert.equal(calcEnemyAttack(10, 6, 0, 0, 0), 16);
|
||||
});
|
||||
|
||||
test("simulateCombat: repeatOnKill repeats an attack until no kill occurs", () => {
|
||||
const shared = {
|
||||
cards: {
|
||||
EchoingSlash: { name: "EchoingSlash", cost: 1, kind: "Attack", aoe: true, damage: 10, repeatOnKill: true },
|
||||
},
|
||||
starterDeck: ["EchoingSlash"],
|
||||
monsters: [
|
||||
{ name: "DummyA", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] },
|
||||
{ name: "DummyB", maxHp: 20, intents: [{ kind: "Attack", value: 0 }] },
|
||||
],
|
||||
};
|
||||
const withRepeat = simulateCombat(shared, () => 0.999999);
|
||||
const withoutRepeat = simulateCombat({
|
||||
...shared,
|
||||
cards: {
|
||||
EchoingSlash: { name: "EchoingSlash", cost: 1, kind: "Attack", aoe: true, damage: 10 },
|
||||
},
|
||||
}, () => 0.999999);
|
||||
assert.equal(withRepeat.win, true);
|
||||
assert.equal(withRepeat.turns, 1);
|
||||
assert.equal(withoutRepeat.turns, 2);
|
||||
});
|
||||
|
||||
test("simulateCombat: poisonIfTargetPoisoned only applies poison to already poisoned enemies", () => {
|
||||
const shared = {
|
||||
cards: {
|
||||
Bubble: { name: "BubbleBubble", cost: 1, kind: "Skill", poison: 9, poisonIfTargetPoisoned: true },
|
||||
},
|
||||
starterDeck: ["Bubble"],
|
||||
monsters: [{ name: "Dummy", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] }],
|
||||
};
|
||||
const withBubble = simulateCombat(shared, () => 0.999999);
|
||||
const withoutBubble = simulateCombat({
|
||||
...shared,
|
||||
cards: {
|
||||
Bubble: { name: "BubbleBubble", cost: 1, kind: "Skill", poison: 9 },
|
||||
},
|
||||
}, () => 0.999999);
|
||||
assert.equal(withBubble.draw, true);
|
||||
assert.equal(withBubble.turns, 100);
|
||||
assert.equal(withoutBubble.win, true);
|
||||
assert.equal(withoutBubble.turns, 1);
|
||||
});
|
||||
|
||||
test("simulateCombat: turnHandSlyCount marks a skill in hand as sly for the turn", () => {
|
||||
const shared = {
|
||||
cards: {
|
||||
HandTrick: { name: "HandTrick", cost: 0, kind: "Skill", block: 7, turnHandSlyCount: 1 },
|
||||
Shield: { name: "Shield", cost: 0, kind: "Skill", unplayable: true, block: 7 },
|
||||
Gamble: { name: "Gamble", cost: 0, kind: "Skill", discardAll: true },
|
||||
},
|
||||
starterDeck: ["Gamble", "Shield", "HandTrick"],
|
||||
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 10 }] }],
|
||||
};
|
||||
const withHandTrick = simulateCombat(shared, () => 0.999999);
|
||||
const withoutHandTrick = simulateCombat({
|
||||
...shared,
|
||||
cards: {
|
||||
HandTrick: { name: "HandTrick", cost: 0, kind: "Skill", block: 7 },
|
||||
Shield: shared.cards.Shield,
|
||||
Gamble: shared.cards.Gamble,
|
||||
},
|
||||
}, () => 0.999999);
|
||||
assert.equal(withHandTrick.playerHpRemaining, 80);
|
||||
assert.equal(withoutHandTrick.playerHpRemaining, 0);
|
||||
});
|
||||
|
||||
test("simulateCombat: extraPoisonTicks adds an extra poison tick at enemy turn start", () => {
|
||||
const shared = {
|
||||
cards: {
|
||||
Accelerant: { name: "Accelerant", cost: 1, kind: "Power", extraPoisonTicks: 1 },
|
||||
Poison: { name: "Poison", cost: 1, kind: "Skill", poison: 2 },
|
||||
},
|
||||
starterDeck: ["Accelerant", "Poison"],
|
||||
monsters: [{ name: "Dummy", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] }],
|
||||
};
|
||||
const withTick = simulateCombat(shared, () => 0.999999);
|
||||
const withoutTick = simulateCombat({
|
||||
...shared,
|
||||
cards: {
|
||||
Accelerant: { name: "Accelerant", cost: 1, kind: "Power" },
|
||||
Poison: shared.cards.Poison,
|
||||
},
|
||||
}, () => 0.999999);
|
||||
assert.equal(withTick.win, true);
|
||||
assert.equal(withTick.turns, 1);
|
||||
assert.equal(withoutTick.turns, 2);
|
||||
});
|
||||
|
||||
test("simulateCombat: poisonApplicationBurstEvery bursts after every third poison application", () => {
|
||||
const shared = {
|
||||
cards: {
|
||||
Outbreak: { name: "Outbreak", cost: 1, kind: "Power", poisonApplicationBurstEvery: 3, poisonApplicationBurstDamage: 11 },
|
||||
Poison1: { name: "Poison1", cost: 0, kind: "Skill", poison: 1 },
|
||||
Poison2: { name: "Poison2", cost: 0, kind: "Skill", poison: 1 },
|
||||
Poison3: { name: "Poison3", cost: 0, kind: "Skill", poison: 1 },
|
||||
},
|
||||
starterDeck: ["Outbreak", "Poison1", "Poison2", "Poison3"],
|
||||
monsters: [
|
||||
{ name: "DummyA", maxHp: 11, intents: [{ kind: "Attack", value: 0 }] },
|
||||
{ name: "DummyB", maxHp: 11, intents: [{ kind: "Attack", value: 0 }] },
|
||||
],
|
||||
};
|
||||
const withBurst = simulateCombat(shared, () => 0.999999);
|
||||
const withoutBurst = simulateCombat({
|
||||
...shared,
|
||||
cards: {
|
||||
Outbreak: { name: "Outbreak", cost: 1, kind: "Power" },
|
||||
Poison1: shared.cards.Poison1,
|
||||
Poison2: shared.cards.Poison2,
|
||||
Poison3: shared.cards.Poison3,
|
||||
},
|
||||
}, () => 0.999999);
|
||||
assert.equal(withBurst.win, true);
|
||||
assert.equal(withBurst.turns, 1);
|
||||
assert.ok(withoutBurst.turns > withBurst.turns);
|
||||
});
|
||||
|
||||
test("simulateCombat: firstCardDamageBonus applies on the first card played this turn", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Strangle: { name: "Strangle", cost: 1, kind: "Attack", damage: 8, cardPlayedDamage: 2 },
|
||||
Strangle: { name: "Strangle", cost: 1, kind: "Attack", damage: 8, firstCardDamageBonus: 2 },
|
||||
},
|
||||
starterDeck: ["Strangle"],
|
||||
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
|
||||
@@ -833,6 +1012,19 @@ test("simulateCombat: cardPlayedDamage hits the target whenever a card is played
|
||||
assert.equal(r.win, true);
|
||||
});
|
||||
|
||||
test("simulateCombat: blockPerDamageDealtThisTurn grants block from damage dealt this turn", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Mirage: { name: "Mirage", cost: 1, kind: "Skill", blockPerDamageDealtThisTurn: 1, block: 0 },
|
||||
Strike: { name: "Strike", cost: 1, kind: "Attack", damage: 4 },
|
||||
},
|
||||
starterDeck: ["Strike", "Mirage"],
|
||||
monsters: [{ name: "Dummy", maxHp: 4, 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: {
|
||||
|
||||
Reference in New Issue
Block a user