3 Commits

Author SHA1 Message Date
acf295d56c 도적 카드 공용 효과 추가 2026-06-19 02:57:11 +09:00
b2bf1bf4dd 카드 설명 키워드 하이라이트 추가 2026-06-19 01:51:36 +09:00
71435a2c91 밴딧 카드 공용 효과 확장 2026-06-19 01:26:15 +09:00
14 changed files with 385 additions and 41 deletions

File diff suppressed because one or more lines are too long

View File

@@ -728,6 +728,7 @@
"rarity": "unique", "rarity": "unique",
"desc": "피해를 12 줍니다. 다음에 사용하는 스킬 카드의 비용이 0 이 됩니다.", "desc": "피해를 12 줍니다. 다음에 사용하는 스킬 카드의 비용이 0 이 됩니다.",
"damage": 12, "damage": 12,
"nextSkillCostZero": true,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f" "image": "91a2d1c16cb041549adbf1a0d7b1f37f"
}, },
"Dash": { "Dash": {
@@ -760,6 +761,7 @@
"rarity": "unique", "rarity": "unique",
"desc": "피해를 15 줍니다. 이번 턴에 스킬을 사용할 때마다 비용이 1 감소합니다.", "desc": "피해를 15 줍니다. 이번 턴에 스킬을 사용할 때마다 비용이 1 감소합니다.",
"damage": 15, "damage": 15,
"skillCostReductionThisTurn": 1,
"image": "1b0f2dc8abd0434990eee1befefcbe0d" "image": "1b0f2dc8abd0434990eee1befefcbe0d"
}, },
"CalculatedGamble": { "CalculatedGamble": {
@@ -801,8 +803,8 @@
"class": "bandit", "class": "bandit",
"rarity": "unique", "rarity": "unique",
"desc": "카드를 1장 뽑습니다. 뽑은 카드가 스킬 카드라면, 방어도를 3 얻습니다.", "desc": "카드를 1장 뽑습니다. 뽑은 카드가 스킬 카드라면, 방어도를 3 얻습니다.",
"block": 3,
"draw": 1, "draw": 1,
"drawSkillBlock": 3,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f" "image": "91a2d1c16cb041549adbf1a0d7b1f37f"
}, },
"Acrobatics": { "Acrobatics": {
@@ -983,8 +985,8 @@
"rarity": "unique", "rarity": "unique",
"desc": "내 턴 시작 시, 모든 적에게 중독을 2 부여합니다.", "desc": "내 턴 시작 시, 모든 적에게 중독을 2 부여합니다.",
"poison": 2, "poison": 2,
"powerEffect": "strengthPerTurn", "powerEffect": "poisonPerTurn",
"value": 1, "value": 2,
"image": "19361e72087946b1888684185b40d935" "image": "19361e72087946b1888684185b40d935"
}, },
"Accuracy": { "Accuracy": {
@@ -1017,9 +1019,8 @@
"rarity": "unique", "rarity": "unique",
"desc": "내 턴 동안 카드를 뽑을 때마다, 모든 적에게 피해를 2 줍니다.", "desc": "내 턴 동안 카드를 뽑을 때마다, 모든 적에게 피해를 2 줍니다.",
"aoe": true, "aoe": true,
"powerEffect": "strengthPerTurn", "powerEffect": "damagePerTurn",
"value": 1, "value": 2,
"damage": 2,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f" "image": "91a2d1c16cb041549adbf1a0d7b1f37f"
}, },
"GrandFinale": { "GrandFinale": {
@@ -1127,7 +1128,7 @@
"class": "bandit", "class": "bandit",
"rarity": "legend", "rarity": "legend",
"desc": "이번 턴 동안 얻는 방어도가 2배가 됩니다.", "desc": "이번 턴 동안 얻는 방어도가 2배가 됩니다.",
"draw": 1, "blockGainMultiplier": 2,
"image": "0946f69d84464df29b24b94c744c868d" "image": "0946f69d84464df29b24b94c744c868d"
}, },
"CorrosiveWave": { "CorrosiveWave": {
@@ -1177,8 +1178,8 @@
"class": "bandit", "class": "bandit",
"rarity": "legend", "rarity": "legend",
"desc": "이번 턴 동안 더 이상 카드를 뽑을 수 없습니다. 이번 턴 동안 손에 있는 모든 카드를 비용 없이 사용할 수 있습니다.", "desc": "이번 턴 동안 더 이상 카드를 뽑을 수 없습니다. 이번 턴 동안 손에 있는 모든 카드를 비용 없이 사용할 수 있습니다.",
"powerEffect": "energyPerTurn", "handCostZeroThisTurn": true,
"value": 1, "drawDisabledThisTurn": true,
"image": "91a2d1c16cb041549adbf1a0d7b1f37f" "image": "91a2d1c16cb041549adbf1a0d7b1f37f"
}, },
"Nightmare": { "Nightmare": {

View File

@@ -10,7 +10,7 @@
## 구현됨 ## 구현됨
`Neutralize`, `SilentStrike`, `Survivor`, `SilentDefend`, `Slice`, `DaggerSpray`, `DaggerThrow`, `PoisonedStab`, `SuckerPunch`, `LeadingStrike`, `FollowThrough`, `FlickFlack`, `Prepared`, `Deflect`, `BladeDance`, `Backflip`, `DodgeAndRoll`, `CloakAndDagger`, `DeadlyPoison`, `Snakebite`, `Untouchable`, `Backstab`, `PreciseCut`, `Finisher`, `MementoMori`, `Flechettes`, `Dash`, `Predator`, `CalculatedGamble`, `HiddenDaggers`, `Acrobatics`, `Blur`, `LegSweep`, `Reflex`, `Haze`, `Tactician`, `WellLaidPlans`, `InfiniteBlades`, `Footwork`, `GrandFinale`, `Adrenaline`, `ShadowStep`, `Assassinate`, `Nightmare`, `ToolsOfTheTrade`, `Afterimage`, `StormOfSteel`, `Abrasive`, `Suppress`, `Expertise` `Neutralize`, `SilentStrike`, `Survivor`, `SilentDefend`, `Slice`, `DaggerSpray`, `DaggerThrow`, `PoisonedStab`, `SuckerPunch`, `LeadingStrike`, `FollowThrough`, `FlickFlack`, `Prepared`, `Deflect`, `BladeDance`, `Backflip`, `DodgeAndRoll`, `CloakAndDagger`, `DeadlyPoison`, `Snakebite`, `Untouchable`, `Backstab`, `PreciseCut`, `Finisher`, `MementoMori`, `Flechettes`, `Dash`, `Predator`, `CalculatedGamble`, `HiddenDaggers`, `Acrobatics`, `Blur`, `LegSweep`, `Reflex`, `Haze`, `Tactician`, `WellLaidPlans`, `InfiniteBlades`, `Footwork`, `GrandFinale`, `Adrenaline`, `ShadowStep`, `Assassinate`, `Nightmare`, `ToolsOfTheTrade`, `Afterimage`, `StormOfSteel`, `Abrasive`, `Suppress`, `Expertise`, `Shadowmeld`, `Pounce`, `Pinpoint`
공용 메모: 공용 메모:
@@ -19,7 +19,7 @@
- `turnStartDraw`, `turnStartDiscard` 구현됨 - `turnStartDraw`, `turnStartDiscard` 구현됨
- `nextTurnBlock`, `nextTurnDraw`, `nextTurnKeepBlock`, `nextTurnAttackMultiplier`, `nextTurnCopies`, `nextTurnSelectHandCard` 구현됨 - `nextTurnBlock`, `nextTurnDraw`, `nextTurnKeepBlock`, `nextTurnAttackMultiplier`, `nextTurnCopies`, `nextTurnSelectHandCard` 구현됨
- `damagePerOtherHandCard`, `damagePerAttackPlayedThisTurn`, `damagePerDiscardedThisTurn`, `damagePerSkillInHand`, `otherHandAtLeast`, `bonusHitsWhenOtherHandAtLeast` 구현됨 - `damagePerOtherHandCard`, `damagePerAttackPlayedThisTurn`, `damagePerDiscardedThisTurn`, `damagePerSkillInHand`, `otherHandAtLeast`, `bonusHitsWhenOtherHandAtLeast` 구현됨
- `gainEnergy`, `drawUntilHandSize`, `drawPerDiscarded`, `cardPlayedBlock` 구현됨 - `gainEnergy`, `drawUntilHandSize`, `drawPerDiscarded`, `cardPlayedBlock`, `blockGainMultiplier`, `nextSkillCostZero`, `skillCostReductionThisTurn` 구현됨
## 부분구현 ## 부분구현
@@ -43,10 +43,6 @@
`Strangle`: 이번 턴 카드 사용마다 추가 피해 `Strangle`: 이번 턴 카드 사용마다 추가 피해
`Pounce`: 다음 스킬 카드 비용 0
`Pinpoint`: 이번 턴 스킬 사용 시 비용 감소
`EscapePlan`: 드로우한 카드가 스킬이면 방어도 3 `EscapePlan`: 드로우한 카드가 스킬이면 방어도 3
`HandTrick`: 손패의 스킬 카드 하나에 교활 부여 `HandTrick`: 손패의 스킬 카드 하나에 교활 부여
@@ -71,7 +67,7 @@
`Malaise`: X코스트 약화/피해 감소 `Malaise`: X코스트 약화/피해 감소
`Shadowmeld`: 이번 턴 얻는 방어도 2배 `Pinpoint`: 이번 턴 스킬 비용 감소
`CorrosiveWave`: 드로우할 때마다 독 `CorrosiveWave`: 드로우할 때마다 독

View File

@@ -16,6 +16,7 @@
- `block`: 방어도 획득 - `block`: 방어도 획득
- `cardPlayedBlock`: 카드를 사용할 때마다 방어도 획득 - `cardPlayedBlock`: 카드를 사용할 때마다 방어도 획득
- `blockGainMultiplier`: 이번 턴 동안 얻는 방어도 배수
- `hits`: 다단히트 횟수 - `hits`: 다단히트 횟수
- `aoe`: 모든 적 대상 - `aoe`: 모든 적 대상
- `pierce`: 방어도 무시 - `pierce`: 방어도 무시
@@ -65,6 +66,8 @@
- `nextTurnCopies`: 다음 턴에 손패에서 가져올 복사본 수 - `nextTurnCopies`: 다음 턴에 손패에서 가져올 복사본 수
- `nextTurnSelectHandCard`: 현재 손패에서 카드 1장 선택 - `nextTurnSelectHandCard`: 현재 손패에서 카드 1장 선택
- `nextTurnSelectPrompt`: 선택 UI 문구 - `nextTurnSelectPrompt`: 선택 UI 문구
- `nextSkillCostZero`: 다음 스킬 카드 비용을 0으로 만듦
- `skillCostReductionThisTurn`: 이번 턴 스킬 카드 비용을 일정량 감소
## 기타 ## 기타

22
docs/draw-skill-block.md Normal file
View File

@@ -0,0 +1,22 @@
# 드로우 연동 효과
드로우 결과를 받아 후속 효과를 처리하는 공용 패턴을 정리합니다.
## 현재 구현
- `draw`: 카드를 뽑음
- `drawUntilHandSize`: 손패가 지정 수치가 될 때까지 뽑음
- `drawSkillBlock`: 이번 카드로 뽑힌 카드 중 스킬 카드마다 방어도를 얻음
## 동작 방식
- 드로우 함수는 이번에 뽑힌 카드 ID 목록을 반환합니다.
- 카드 효과는 그 목록을 보고 조건을 판정합니다.
- 그래서 `EscapePlan` 같은 카드뿐 아니라, 나중에 같은 규칙이 필요한 카드에도 같은 필드를 붙이면 됩니다.
## 예시
- `EscapePlan`
- `draw = 1`
- `drawSkillBlock = 3`

View File

@@ -90,12 +90,31 @@ function canPlayCardNow(card, ctx = {}) {
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님). // 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬. // 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
export function chooseAction(hand, cards, energy, ctx = {}) { export function chooseAction(hand, cards, energy, ctx = {}) {
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id] && cards[x.id].cost <= energy && !cards[x.id].unplayable && canPlayCardNow(cards[x.id], ctx)); const entries = hand.map((id, i) => ({ id, i })).filter((x) => {
const card = cards[x.id];
if (!card || card.unplayable || !canPlayCardNow(card, ctx)) return false;
let effectiveCost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) effectiveCost = 0;
else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) effectiveCost = 0;
else effectiveCost = Math.max(0, effectiveCost - (ctx.skillCostReductionThisTurn || 0));
}
return 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');
const skills = entries.filter((x) => cards[x.id].kind === 'Skill'); const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
const dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(cards[x.id].cost, 1); const effectiveCost = (card) => {
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(cards[x.id].cost, 1); let cost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) cost = 0;
else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) cost = 0;
else cost = Math.max(0, cost - (ctx.skillCostReductionThisTurn || 0));
}
return cost;
};
const dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(effectiveCost(cards[x.id]), 1);
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(effectiveCost(cards[x.id]), 1);
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0]; const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
if (powers.length) return powers[0].i; if (powers.length) return powers[0].i;
if (attacks.length) return bestBy(attacks, dmgEff).i; if (attacks.length) return bestBy(attacks, dmgEff).i;
@@ -128,6 +147,11 @@ export function simulateCombat(data, rng, stats) {
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;
let blockGainMultiplier = 1;
let handCostZeroThisTurn = false;
let drawDisabledThisTurn = false;
let nextSkillCostZero = false;
let skillCostReductionThisTurn = 0;
let nextTurnBlock = 0, nextTurnDraw = 0, nextTurnKeepBlock = false; let nextTurnBlock = 0, nextTurnDraw = 0, nextTurnKeepBlock = false;
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1; let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
let nextTurnAddCards = []; let nextTurnAddCards = [];
@@ -141,16 +165,20 @@ export function simulateCombat(data, rng, stats) {
let turns = 0; let turns = 0;
function draw(n) { function draw(n) {
const drawn = [];
if (drawDisabledThisTurn === true) return drawn;
for (let k = 0; k < n; k++) { for (let k = 0; k < n; k++) {
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; } if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
if (drawPile.length === 0) break; if (drawPile.length === 0) break;
const card = drawPile.pop(); const card = drawPile.pop();
drawn.push(card);
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화) // 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
if (hand.length >= 10) { if (hand.length >= 10) {
discard.push(card); discard.push(card);
triggerSly(card); triggerSly(card);
} else hand.push(card); } else hand.push(card);
} }
return drawn;
} }
function addCardsToHand(id, n) { function addCardsToHand(id, n) {
for (let k = 0; k < n; k++) { for (let k = 0; k < n; k++) {
@@ -161,6 +189,7 @@ export function simulateCombat(data, rng, stats) {
function addBlock(base) { function addBlock(base) {
let amount = base || 0; let amount = base || 0;
if (amount > 0) amount += pDex; if (amount > 0) amount += pDex;
if (blockGainMultiplier > 1) amount *= blockGainMultiplier;
if (amount < 0) amount = 0; if (amount < 0) amount = 0;
pBlock += amount; pBlock += amount;
return amount; return amount;
@@ -243,6 +272,11 @@ export function simulateCombat(data, rng, stats) {
const alive = aliveList(); const alive = aliveList();
let dmg = 0; let dmg = 0;
let blockGained = 0; let blockGained = 0;
if (c.blockGainMultiplier && c.blockGainMultiplier > 0) blockGainMultiplier *= c.blockGainMultiplier;
if (c.nextSkillCostZero === true) nextSkillCostZero = true;
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
if (c.kind === 'Attack') { if (c.kind === 'Attack') {
if (alive.length && c.damage) { if (alive.length && c.damage) {
const baseDamage = attackBaseForCard(id, c); const baseDamage = attackBaseForCard(id, c);
@@ -294,10 +328,16 @@ export function simulateCombat(data, rng, stats) {
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;
queueNextTurnEffects(c); queueNextTurnEffects(c);
if (c.draw) draw(c.draw); let drawnCards = [];
if (c.draw) drawnCards = drawnCards.concat(draw(c.draw));
if (c.drawUntilHandSize) { if (c.drawUntilHandSize) {
const need = c.drawUntilHandSize - Math.max(0, hand.length - 1); const need = c.drawUntilHandSize - Math.max(0, hand.length - 1);
if (need > 0) draw(need); if (need > 0) drawnCards = drawnCards.concat(draw(need));
}
if (c.drawSkillBlock && c.drawSkillBlock > 0) {
for (const drawnId of drawnCards) {
if (cards[drawnId]?.kind === 'Skill') blockGained += addBlock(c.drawSkillBlock);
}
} }
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 (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained); if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
@@ -331,6 +371,10 @@ export function simulateCombat(data, rng, stats) {
turns++; turns++;
turnAttackCardsPlayed = 0; turnAttackCardsPlayed = 0;
turnDiscardedCards = 0; turnDiscardedCards = 0;
blockGainMultiplier = 1;
handCostZeroThisTurn = false;
drawDisabledThisTurn = false;
skillCostReductionThisTurn = 0;
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워) // 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
if (nextTurnKeepBlock === true) nextTurnKeepBlock = false; if (nextTurnKeepBlock === true) nextTurnKeepBlock = false;
else pBlock = 0; else pBlock = 0;
@@ -345,6 +389,16 @@ export function simulateCombat(data, rng, stats) {
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value; if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
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;
else if (pc.powerEffect === 'poisonPerTurn') {
for (const m of mob) if (m.alive) m.poison += pc.value;
} else if (pc.powerEffect === 'damagePerTurn') {
for (const m of mob) {
if (!m.alive) continue;
const r = applyDamage(m.hp, m.block, pc.value || 0);
m.hp = r.hp; m.block = r.block;
if (m.hp <= 0) m.alive = false;
}
}
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv); if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
if (pc.turnStartDraw) powerTurnDraw += pc.turnStartDraw; if (pc.turnStartDraw) powerTurnDraw += pc.turnStartDraw;
if (pc.turnStartDiscard) powerTurnDiscard += pc.turnStartDiscard; if (pc.turnStartDiscard) powerTurnDiscard += pc.turnStartDiscard;
@@ -362,12 +416,16 @@ export function simulateCombat(data, rng, stats) {
while (true) { while (true) {
const alive = aliveList(); const alive = aliveList();
if (alive.length === 0) break; if (alive.length === 0) break;
const idx = chooseAction(hand, cards, energy, { drawPileCount: drawPile.length }); const idx = chooseAction(hand, cards, energy, { drawPileCount: drawPile.length, nextSkillCostZero, skillCostReductionThisTurn, handCostZeroThisTurn });
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; const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
resolveCardEffects(id, c, c.cost); const baseCost = c.cost || 0;
const cost = handCostZeroThisTurn === true ? 0 : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost));
energy -= cost;
resolveCardEffects(id, c, cost);
if (c.kind === 'Attack') turnAttackCardsPlayed++; if (c.kind === 'Attack') turnAttackCardsPlayed++;
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
const playedBlock = powerFieldTotal('cardPlayedBlock'); const playedBlock = powerFieldTotal('cardPlayedBlock');
if (playedBlock > 0) addBlock(playedBlock); if (playedBlock > 0) addBlock(playedBlock);
hand.splice(idx, 1); hand.splice(idx, 1);

View File

@@ -684,3 +684,96 @@ test("simulateCombat: cardPlayedBlock grants block whenever a card is played", (
assert.equal(r.draw, true); assert.equal(r.draw, true);
assert.equal(r.playerHpRemaining, 80); assert.equal(r.playerHpRemaining, 80);
}); });
test("simulateCombat: blockGainMultiplier doubles block gain for the turn", () => {
const data = {
cards: {
Shadow: { name: "Shadowmeld", cost: 1, kind: "Skill", block: 5, blockGainMultiplier: 2 },
Shield: { name: "Shield", cost: 1, kind: "Skill", block: 2 },
},
starterDeck: ["Shadow", "Shield"],
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 8 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.draw, true);
assert.equal(r.playerHpRemaining, 80);
});
test("simulateCombat: nextSkillCostZero makes the next skill free", () => {
const data = {
cards: {
Pounce: { name: "Pounce", cost: 2, kind: "Attack", damage: 12, nextSkillCostZero: true },
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
},
starterDeck: ["Pounce", "Guard"],
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 8 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.draw, true);
assert.equal(r.playerHpRemaining, 80);
});
test("chooseAction: skillCostReductionThisTurn allows discounted skills", () => {
const cards = {
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
};
assert.equal(chooseAction(["Guard"], cards, 1, { skillCostReductionThisTurn: 1 }), 0);
assert.equal(chooseAction(["Guard"], cards, 1, {}), -1);
});
test("chooseAction: handCostZeroThisTurn lets expensive cards be played", () => {
const cards = {
Burst: { name: "Burst", cost: 3, kind: "Skill", block: 8 },
};
assert.equal(chooseAction(["Burst"], cards, 0, { handCostZeroThisTurn: true }), 0);
assert.equal(chooseAction(["Burst"], cards, 0, {}), -1);
});
test("simulateCombat: drawSkillBlock grants block for each drawn skill", () => {
const data = {
cards: {
Escape: { name: "EscapePlan", cost: 0, kind: "Skill", draw: 1, drawSkillBlock: 3, innate: true, exhaust: true },
Filler1: { name: "Filler1", cost: 99, kind: "Skill", block: 0 },
Filler2: { name: "Filler2", cost: 99, kind: "Skill", block: 0 },
Filler3: { name: "Filler3", cost: 99, kind: "Skill", block: 0 },
Filler4: { name: "Filler4", cost: 99, kind: "Skill", block: 0 },
Filler5: { name: "Filler5", cost: 99, kind: "Skill", block: 0 },
},
starterDeck: ["Escape", "Filler1", "Filler2", "Filler3", "Filler4", "Filler5"],
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 0 }] }],
};
const stats = {};
const r = simulateCombat(data, () => 0.999999, stats);
assert.equal(r.draw, true);
assert.equal(stats.Escape.block, 3);
});
test("simulateCombat: poisonPerTurn powers poison all enemies at turn start", () => {
const data = {
cards: {
Fumes: { name: "NoxiousFumes", cost: 1, kind: "Power", powerEffect: "poisonPerTurn", value: 2 },
},
starterDeck: ["Fumes"],
monsters: [
{ name: "DummyA", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
{ name: "DummyB", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: damagePerTurn powers damage all enemies at turn start", () => {
const data = {
cards: {
Speed: { name: "Speedster", cost: 2, kind: "Power", powerEffect: "damagePerTurn", value: 2 },
},
starterDeck: ["Speed"],
monsters: [
{ name: "DummyA", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
{ name: "DummyB", maxHp: 2, intents: [{ kind: "Attack", value: 0 }] },
],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});

View File

@@ -44,15 +44,32 @@ end
if self:CanPlayCardNow(c) ~= true then if self:CanPlayCardNow(c) ~= true then
return return
end end
if self.Energy < c.cost then local cost = c.cost or 0
local skillFree = false
if self.HandCostZeroThisTurn == true then
\tcost = 0
end
if c.kind == "Skill" and self.NextSkillCostZero == true then
cost = 0
skillFree = true
end
if c.kind == "Skill" and self.SkillCostReductionThisTurn ~= nil and self.SkillCostReductionThisTurn > 0 then
cost = math.max(0, cost - self.SkillCostReductionThisTurn)
end
if self.Energy < cost then
self:Toast("에너지가 부족합니다") self:Toast("에너지가 부족합니다")
return return
end end
self.Energy = self.Energy - c.cost self.Energy = self.Energy - cost
self:ResolveCardEffects(cardId, slot, c, false) self:ResolveCardEffects(cardId, slot, c, false)
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
if skillFree == true then
if c.nextSkillCostZero ~= true then
self.NextSkillCostZero = false
end
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

View File

@@ -230,8 +230,10 @@ self.TurnAttackCardsPlayed = 0
self.TurnDiscardedCards = 0 self.TurnDiscardedCards = 0
self.NextTurnSelectCopies = 0 self.NextTurnSelectCopies = 0
self.NextTurnSelectPrompt = "" self.NextTurnSelectPrompt = ""
self.SkillCostReductionThisTurn = 0
self:UpdateDiscardPrompt() self:UpdateDiscardPrompt()
self.Energy = self.MaxEnergy self.Energy = self.MaxEnergy
self.BlockGainMultiplier = 1
self:ApplyRelics("turnStart") self:ApplyRelics("turnStart")
if self.NextTurnKeepBlock == true then if self.NextTurnKeepBlock == true then
self.NextTurnKeepBlock = false self.NextTurnKeepBlock = false
@@ -244,6 +246,8 @@ 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.HandCostZeroThisTurn = false
self.DrawDisabledThisTurn = false
local powerTurnDraw = 0 local powerTurnDraw = 0
local powerTurnDiscard = 0 local powerTurnDiscard = 0
if self.PlayerPowers ~= nil then if self.PlayerPowers ~= nil then
@@ -256,6 +260,19 @@ if self.PlayerPowers ~= nil then
self.Energy = self.Energy + pc.value self.Energy = self.Energy + pc.value
elseif pc.powerEffect == "blockPerTurn" then elseif pc.powerEffect == "blockPerTurn" then
self.PlayerBlock = self.PlayerBlock + pc.value self.PlayerBlock = self.PlayerBlock + pc.value
elseif pc.powerEffect == "poisonPerTurn" then
if self.Monsters ~= nil then
for j = 1, #self.Monsters do
local tm = self.Monsters[j]
if tm ~= nil and tm.alive == true then
tm.poison = (tm.poison or 0) + pc.value
end
end
end
elseif pc.powerEffect == "damagePerTurn" then
if self.Monsters ~= nil then
self:PlayAoeFx(pc.fx or pc.image, pc.value or 0)
end
end end
if pc.turnStartShiv ~= nil then if pc.turnStartShiv ~= nil then
self:AddCardsToHand("Shiv", pc.turnStartShiv) self:AddCardsToHand("Shiv", pc.turnStartShiv)
@@ -413,8 +430,12 @@ if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
self:RenderHand(false) self:RenderHand(false)
self:RenderPiles() self:RenderPiles()
self:EnemyTurn()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'retainSlot' }]), self:EnemyTurn()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'retainSlot' }]),
method('DrawCards', `local drawnSlots = {} method('DrawCards', `local drawnSlots = {}
local drawnCards = {}
local drewAny = false local drewAny = false
if self.DrawDisabledThisTurn == true then
\treturn drawnCards
end
for i = 1, amount do for i = 1, amount do
\tif #self.DrawPile <= 0 then \tif #self.DrawPile <= 0 then
\t\tself:RecycleDiscardIntoDraw() \t\tself:RecycleDiscardIntoDraw()
@@ -423,6 +444,7 @@ for i = 1, amount do
\t\tbreak \t\tbreak
\tend \tend
\tlocal cardId = table.remove(self.DrawPile) \tlocal cardId = table.remove(self.DrawPile)
\ttable.insert(drawnCards, cardId)
\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)
@@ -442,10 +464,11 @@ if animate == true and #drawnSlots > 0 then
\t\tlocal slot = drawnSlots[i] \t\tlocal slot = drawnSlots[i]
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045) \t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
\tend \tend
return drawnCards
end`, [ 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: 'animate' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
]), ], 0, 'any'),
method('AddCardsToHand', `if self.Hand == nil then method('AddCardsToHand', `if self.Hand == nil then
self.Hand = {} self.Hand = {}
end end

View File

@@ -60,7 +60,7 @@ if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
end end
self:SetText(base .. "/Cost", string.format("%d", c.cost)) self:SetText(base .. "/Cost", string.format("%d", c.cost))
self:SetText(base .. "/Name", c.name) self:SetText(base .. "/Name", c.name)
self:SetText(base .. "/Desc", c.desc) self:SetText(base .. "/Desc", self:FormatCardDescription(c.desc))
local art = _EntityService:GetEntityByPath(base .. "/Art") local art = _EntityService:GetEntityByPath(base .. "/Art")
if art ~= nil then if art ~= nil then
if c.image ~= nil and c.image ~= "" then if c.image ~= nil and c.image ~= "" then
@@ -269,6 +269,9 @@ end, 1 / 60)`, [
if amount > 0 and self.PlayerDex ~= nil then if amount > 0 and self.PlayerDex ~= nil then
amount = amount + self.PlayerDex amount = amount + self.PlayerDex
end end
if self.BlockGainMultiplier ~= nil and self.BlockGainMultiplier > 1 then
amount = amount * self.BlockGainMultiplier
end
if amount < 0 then if amount < 0 then
amount = 0 amount = 0
end end
@@ -367,9 +370,24 @@ if c.nextTurnAttackMultiplier ~= nil and c.nextTurnAttackMultiplier > 0 then
local cur = self.NextTurnAttackMultiplier or 1 local cur = self.NextTurnAttackMultiplier or 1
self.NextTurnAttackMultiplier = cur * c.nextTurnAttackMultiplier self.NextTurnAttackMultiplier = cur * c.nextTurnAttackMultiplier
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }]), end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }]),
method('ResolveCardEffects', `if c == nil then method('ResolveCardEffects', `if c == nil then
return return
end end
if c.blockGainMultiplier ~= nil and c.blockGainMultiplier > 0 then
self.BlockGainMultiplier = (self.BlockGainMultiplier or 1) * c.blockGainMultiplier
end
if c.nextSkillCostZero == true then
self.NextSkillCostZero = true
end
if c.skillCostReductionThisTurn ~= nil and c.skillCostReductionThisTurn > 0 then
self.SkillCostReductionThisTurn = (self.SkillCostReductionThisTurn or 0) + c.skillCostReductionThisTurn
end
if c.handCostZeroThisTurn == true then
self.HandCostZeroThisTurn = true
end
if c.drawDisabledThisTurn == true then
self.DrawDisabledThisTurn = true
end
if c.kind == "Attack" then if c.kind == "Attack" then
if c.damage ~= nil then if c.damage ~= nil then
self:PlayerAttackMotion() self:PlayerAttackMotion()
@@ -447,8 +465,9 @@ if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
end end
end end
end end
local drawnCards = {}
if c.draw ~= nil then if c.draw ~= nil then
self:DrawCards(c.draw, true) drawnCards = self:DrawCards(c.draw, true) or {}
end end
if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then
local currentHand = 0 local currentHand = 0
@@ -460,7 +479,18 @@ if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then
end end
local need = c.drawUntilHandSize - currentHand local need = c.drawUntilHandSize - currentHand
if need > 0 then if need > 0 then
self:DrawCards(need, true) local moreDrawnCards = self:DrawCards(need, true) or {}
for i = 1, #moreDrawnCards do
table.insert(drawnCards, moreDrawnCards[i])
end
end
end
if c.drawSkillBlock ~= nil and c.drawSkillBlock > 0 then
for i = 1, #drawnCards do
local drawnCard = self.Cards[drawnCards[i]]
if drawnCard ~= nil and drawnCard.kind == "Skill" then
self:AddCardBlock(c.drawSkillBlock)
end
end end
end end
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then

View File

@@ -67,6 +67,11 @@ self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
self.MaxEnergy = 3 self.MaxEnergy = 3
self.Turn = 0 self.Turn = 0
self.PlayerBlock = 0 self.PlayerBlock = 0
self.BlockGainMultiplier = 1
self.HandCostZeroThisTurn = false
self.DrawDisabledThisTurn = false
self.NextSkillCostZero = false
self.SkillCostReductionThisTurn = 0
self.PlayerStr = 0 self.PlayerStr = 0
self.PlayerDex = 0 self.PlayerDex = 0
self.PlayerThorns = 0 self.PlayerThorns = 0

View File

@@ -3,6 +3,47 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const tooltipMethods = [ export const tooltipMethods = [
method('FormatCardDescription', `if desc == nil or desc == "" then
return ""
end
local function replacePlain(text, needle, replacement)
local out = ""
local pos = 1
while true do
local s, e = string.find(text, needle, pos, true)
if s == nil then
out = out .. string.sub(text, pos)
break
end
out = out .. string.sub(text, pos, s - 1) .. replacement
pos = e + 1
end
return out
end
local terms = {
"교활",
"보존",
"민첩",
"가시",
"소멸",
"선천성",
"취약",
"약화",
"독",
"광역",
"관통",
"방어도",
"힘",
"스킬",
"공격",
"파워",
}
local out = desc
for i = 1, #terms do
local term = terms[i]
out = replacePlain(out, term, "<color=#70D6FF>" .. term .. "</color>")
end
return out`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }], 0, 'string'),
method('BuildCardKeywordTooltip', `if c == nil then method('BuildCardKeywordTooltip', `if c == nil then
return "" return ""
end end

View File

@@ -81,6 +81,7 @@ function writeCodeblocks() {
prop('number', 'PlayerHp', '0'), prop('number', 'PlayerHp', '0'),
prop('number', 'PlayerMaxHp', '80'), prop('number', 'PlayerMaxHp', '80'),
prop('number', 'PlayerBlock', '0'), prop('number', 'PlayerBlock', '0'),
prop('number', 'BlockGainMultiplier', '1'),
prop('number', 'PlayerDex', '0'), prop('number', 'PlayerDex', '0'),
prop('number', 'PlayerThorns', '0'), prop('number', 'PlayerThorns', '0'),
prop('boolean', 'CombatOver', 'false'), prop('boolean', 'CombatOver', 'false'),
@@ -141,6 +142,8 @@ function writeCodeblocks() {
prop('number', 'TurnAttackMultiplier', '1'), prop('number', 'TurnAttackMultiplier', '1'),
prop('string', 'NextTurnSelectPrompt', '""'), prop('string', 'NextTurnSelectPrompt', '""'),
prop('number', 'NextTurnSelectCopies', '0'), prop('number', 'NextTurnSelectCopies', '0'),
prop('boolean', 'NextSkillCostZero', 'false'),
prop('number', 'SkillCostReductionThisTurn', '0'),
prop('any', 'NextTurnAddCards'), prop('any', 'NextTurnAddCards'),
], [ ], [
...bootMethods, ...bootMethods,

View File

@@ -162,9 +162,12 @@ 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.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`);
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`);
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}`);
if (c.blockGainMultiplier != null) fields.push(`blockGainMultiplier = ${c.blockGainMultiplier}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`); if (c.strength != null) fields.push(`strength = ${c.strength}`);
if (c.dex != null) fields.push(`dex = ${c.dex}`); if (c.dex != null) fields.push(`dex = ${c.dex}`);
if (c.thorns != null) fields.push(`thorns = ${c.thorns}`); if (c.thorns != null) fields.push(`thorns = ${c.thorns}`);
@@ -181,6 +184,7 @@ function luaCardsTable(cards) {
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`); if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
if (c.draw != null) fields.push(`draw = ${c.draw}`); if (c.draw != null) fields.push(`draw = ${c.draw}`);
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`); if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${c.drawSkillBlock}`);
if (c.heal != null) fields.push(`heal = ${c.heal}`); if (c.heal != null) fields.push(`heal = ${c.heal}`);
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`); if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
if (c.poison != null) fields.push(`poison = ${c.poison}`); if (c.poison != null) fields.push(`poison = ${c.poison}`);
@@ -191,6 +195,8 @@ function luaCardsTable(cards) {
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`); if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`);
if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${c.turnStartDraw}`); if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${c.turnStartDraw}`);
if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${c.turnStartDiscard}`); if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${c.turnStartDiscard}`);
if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = 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.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}`);
@@ -199,6 +205,8 @@ function luaCardsTable(cards) {
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`); if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true'); if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`); if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`);
if (c.innate === true) fields.push('innate = true'); if (c.innate === true) fields.push('innate = true');
if (c.playableWhenDrawPileEmpty === true) fields.push('playableWhenDrawPileEmpty = true'); if (c.playableWhenDrawPileEmpty === true) fields.push('playableWhenDrawPileEmpty = true');
if (c.sly === true) fields.push('sly = true'); if (c.sly === true) fields.push('sly = true');