8 Commits

20 changed files with 578 additions and 63 deletions

File diff suppressed because one or more lines are too long

View File

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

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

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

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`, `Shadowmeld`, `Pounce`, `Pinpoint`
`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`, `Burst`, `StormOfSteel`, `Abrasive`, `Suppress`, `Expertise`, `Shadowmeld`, `Pounce`, `Pinpoint`
공용 메모:
@@ -73,8 +73,6 @@
`BladeOfInk`: 전용 표창 생성
`Burst`: 다음 스킬 1회 추가 사용
`KnifeTrap`: 소멸된 표창 전부 사용
`BulletTime`: 드로우 금지 + 손패 무료 사용

View File

@@ -66,6 +66,7 @@
- `nextTurnCopies`: 다음 턴에 손패에서 가져올 복사본 수
- `nextTurnSelectHandCard`: 현재 손패에서 카드 1장 선택
- `nextTurnSelectPrompt`: 선택 UI 문구
- `nextSkillRepeatCount`: 다음 스킬 카드의 효과를 추가 횟수만큼 다시 적용
- `nextSkillCostZero`: 다음 스킬 카드 비용을 0으로 만듦
- `skillCostReductionThisTurn`: 이번 턴 스킬 카드 비용을 일정량 감소

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` 같은 카드가 이 조합을 사용합니다.

12
docs/next-skill-repeat.md Normal file
View File

@@ -0,0 +1,12 @@
# Next Skill Repeat
`nextSkillRepeatCount`는 다음에 사용하는 스킬 카드의 효과를 추가 횟수만큼 다시 적용하는 공용 필드입니다.
현재 구현은 카드가 발동할 때 이 수치를 전역 상태에 누적해 두고, 다음 스킬 카드가 실제로 사용되면 그 효과를 같은 카드에 대해 다시 한 번 이상 적용합니다. 카드 종류는 고정하지 않았기 때문에, 같은 필드를 다른 카드에도 그대로 붙일 수 있습니다.
예시:
- `Burst`
- `nextSkillRepeatCount = 1`
- 다음 스킬을 한 번 더 적용

5
docs/reward-on-kill.md Normal file
View File

@@ -0,0 +1,5 @@
# 처치 보상
`rewardOnKill`은 해당 카드가 적을 처치했을 때 전투 보상 화면을 한 번 더 이어서 보여주는 공용 필드입니다. 현재 보상 UI는 3장 선택을 유지하고, 보상 화면만 추가로 한 번 더 열립니다.
`TheHunt`는 이 규칙을 사용합니다. 같은 패턴이 필요한 다른 카드에도 그대로 붙일 수 있습니다.

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;
let effectiveCost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) effectiveCost = 0;
else if (card.useAllEnergy === true) effectiveCost = 1;
else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) effectiveCost = 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 attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
@@ -107,6 +108,7 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
const effectiveCost = (card) => {
let cost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) cost = 0;
else if (card.useAllEnergy === true) cost = 1;
else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) cost = 0;
else cost = Math.max(0, cost - (ctx.skillCostReductionThisTurn || 0));
@@ -146,16 +148,20 @@ export function simulateCombat(data, rng, stats) {
const exhaust = [];
let hand = [];
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 handCostZeroThisTurn = false;
let drawDisabledThisTurn = false;
let nextSkillCostZero = false;
let nextSkillRepeatCount = 0;
let skillCostReductionThisTurn = 0;
let nextTurnBlock = 0, nextTurnDraw = 0, nextTurnKeepBlock = false;
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
let nextTurnAddCards = [];
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
let cardsDrawnThisCombat = 0;
let bonusRewardScreens = 0;
let activeKillReward = 0;
let energy = 0;
const powers = [];
const mob = monsters.map((m) => ({
@@ -172,6 +178,7 @@ export function simulateCombat(data, rng, stats) {
if (drawPile.length === 0) break;
const card = drawPile.pop();
drawn.push(card);
cardsDrawnThisCombat++;
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
if (hand.length >= 10) {
discard.push(card);
@@ -228,6 +235,7 @@ export function simulateCombat(data, rng, stats) {
if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn;
if (c.damagePerDiscardedThisTurn) base += turnDiscardedCards * c.damagePerDiscardedThisTurn;
if (c.damagePerSkillInHand) base += countOtherHandSkills(id) * c.damagePerSkillInHand;
if (c.damagePerCardDrawnThisCombat) base += cardsDrawnThisCombat * c.damagePerCardDrawnThisCombat;
if (base < 0) base = 0;
return base;
}
@@ -274,12 +282,14 @@ export function simulateCombat(data, rng, stats) {
let blockGained = 0;
if (c.blockGainMultiplier && c.blockGainMultiplier > 0) blockGainMultiplier *= c.blockGainMultiplier;
if (c.nextSkillCostZero === true) nextSkillCostZero = true;
if (c.nextSkillRepeatCount && c.nextSkillRepeatCount > 0) nextSkillRepeatCount += c.nextSkillRepeatCount;
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
const xEnergy = costSpent || 0;
if (c.kind === 'Attack') {
if (alive.length && c.damage) {
const baseDamage = attackBaseForCard(id, c);
if (alive.length && (c.damage || c.xDamagePerEnergy)) {
const baseDamage = c.xDamagePerEnergy ? xEnergy * c.xDamagePerEnergy : attackBaseForCard(id, c);
const bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast)
? c.bonusHitsWhenOtherHandAtLeast : 0;
const hitN = (c.hits || 1) + bonusHits;
@@ -295,7 +305,12 @@ export function simulateCombat(data, rng, stats) {
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;
const attackPoison = powerFieldTotal('attackPoison');
if (d2 > 0 && attackPoison > 0) m2.poison += attackPoison;
if (m2.hp <= 0) {
m2.alive = false;
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
}
}
} else {
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
@@ -306,7 +321,12 @@ export function simulateCombat(data, rng, stats) {
const r = applyDamage(target.hp, target.block, dmg);
target.hp = r.hp; target.block = r.block;
}
if (target.hp <= 0) target.alive = false;
const attackPoison = powerFieldTotal('attackPoison');
if (dmg > 0 && attackPoison > 0) target.poison += attackPoison;
if (target.hp <= 0) {
target.alive = false;
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
}
}
}
if (c.block) blockGained = addBlock(c.block);
@@ -314,9 +334,10 @@ export function simulateCombat(data, rng, stats) {
if (recordStats) powers.push(id);
} else {
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);
if (c.weak) target.weak += c.weak;
if (weakAmount) target.weak += weakAmount;
if (c.vuln) target.vuln += c.vuln;
if (c.poison) target.poison += c.poison;
}
@@ -327,6 +348,8 @@ export function simulateCombat(data, rng, stats) {
if (c.selfVuln) pVuln += c.selfVuln;
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
if (c.gainEnergy) energy += c.gainEnergy;
activeKillReward = c.rewardOnKill || 0;
if (c.intangible) pIntangible += c.intangible;
queueNextTurnEffects(c);
let drawnCards = [];
if (c.draw) drawnCards = drawnCards.concat(draw(c.draw));
@@ -340,6 +363,25 @@ export function simulateCombat(data, rng, stats) {
}
}
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);
}
function triggerSly(id) {
@@ -420,20 +462,66 @@ export function simulateCombat(data, rng, stats) {
if (idx < 0) break;
const id = hand[idx], c = cards[id];
const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
const skillRepeat = c.kind === 'Skill' ? nextSkillRepeatCount : 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;
resolveCardEffects(id, c, cost);
if (c.kind === 'Attack') turnAttackCardsPlayed++;
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
const playedBlock = powerFieldTotal('cardPlayedBlock');
if (playedBlock > 0) addBlock(playedBlock);
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 (skillRepeat > 0) {
nextSkillRepeatCount = Math.max(0, nextSkillRepeatCount - skillRepeat);
for (let r = 0; r < skillRepeat; r++) {
resolveCardEffects(id, c, cost);
if (playedBlock > 0) addBlock(playedBlock);
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 (c.kind === 'Attack') turnAttackCardsPlayed++;
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
hand.splice(idx, 1);
queueSelectedReserve(c);
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
else if (c.kind !== 'Power') discard.push(id);
applyDiscardEffects(c);
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
}
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
let burn = 0;
@@ -446,6 +534,14 @@ export function simulateCombat(data, rng, stats) {
else discard.push(hid);
}
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 };
// 플레이어 디버프 감소 — Lua EndPlayerTurn 동기화 (적 행동 전)
if (pWeak > 0) pWeak--;
@@ -465,7 +561,9 @@ export function simulateCombat(data, rng, stats) {
if (it.kind === 'Attack') {
const atk = calcAttack(it.value, m.str, m.weak, pVuln);
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) {
m.hp -= pThorns;
if (m.hp <= 0) m.alive = false;
@@ -486,9 +584,9 @@ export function simulateCombat(data, rng, stats) {
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
}
// 독 사망 등 적 페이즈 중 전멸 처리 (Lua FinishEnemyTurn→CheckCombatEnd 동기화)
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp };
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
}
return { win: false, turns, playerHpRemaining: pHp, draw: true };
return { win: false, turns, playerHpRemaining: pHp, draw: true, bonusRewardScreens };
}
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }

View File

@@ -713,6 +713,28 @@ test("simulateCombat: nextSkillCostZero makes the next skill free", () => {
assert.equal(r.playerHpRemaining, 80);
});
test("simulateCombat: nextSkillRepeatCount repeats the next skill effect", () => {
const shared = {
cards: {
Burst: { name: "Burst", cost: 1, kind: "Skill", draw: 1, block: 5, nextSkillRepeatCount: 1 },
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
},
starterDeck: ["Burst", "Guard"],
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 15 }] }],
};
const withBurst = simulateCombat(shared, () => 0.999999);
const withoutBurst = simulateCombat({
...shared,
cards: {
Burst: { name: "Burst", cost: 1, kind: "Skill", draw: 1, block: 5 },
Guard: shared.cards.Guard,
},
}, () => 0.999999);
assert.equal(withBurst.draw, true);
assert.equal(withBurst.playerHpRemaining, 80);
assert.ok(withBurst.playerHpRemaining > withoutBurst.playerHpRemaining);
});
test("chooseAction: skillCostReductionThisTurn allows discounted skills", () => {
const cards = {
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
@@ -729,6 +751,13 @@ test("chooseAction: handCostZeroThisTurn lets expensive cards be played", () =>
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", () => {
const data = {
cards: {
@@ -777,3 +806,111 @@ test("simulateCombat: damagePerTurn powers damage all enemies at turn start", ()
const r = simulateCombat(data, () => 0.999999);
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: rewardOnKill grants an extra reward screen when an attack kills", () => {
const data = {
cards: {
TheHunt: { name: "TheHunt", cost: 1, kind: "Attack", damage: 10, rewardOnKill: 1 },
},
starterDeck: ["TheHunt"],
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.bonusRewardScreens, 1);
});
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

@@ -46,8 +46,11 @@ if self:CanPlayCardNow(c) ~= true then
end
local cost = c.cost or 0
local skillFree = false
local skillRepeat = 0
if self.HandCostZeroThisTurn == true then
\tcost = 0
cost = 0
elseif c.useAllEnergy == true then
cost = self.Energy
end
if c.kind == "Skill" and self.NextSkillCostZero == true then
cost = 0
@@ -56,12 +59,39 @@ end
if c.kind == "Skill" and self.SkillCostReductionThisTurn ~= nil and self.SkillCostReductionThisTurn > 0 then
cost = math.max(0, cost - self.SkillCostReductionThisTurn)
end
if c.kind == "Skill" and self.NextSkillRepeatCount ~= nil and self.NextSkillRepeatCount > 0 then
skillRepeat = self.NextSkillRepeatCount
end
if self.Energy < cost then
self:Toast("에너지가 부족합니다")
return
end
self.Energy = self.Energy - cost
self:ResolveCardEffects(cardId, slot, c, false)
self.ActiveKillReward = c.rewardOnKill or 0
self:ResolveCardEffects(cardId, slot, c, false, cost)
local function applyCardPlayHooks()
if self:HasPowerField("cardPlayedBlock") == true then
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
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
end
applyCardPlayHooks()
if skillRepeat > 0 then
local remaining = (self.NextSkillRepeatCount or 0) - skillRepeat
if remaining < 0 then
remaining = 0
end
self.NextSkillRepeatCount = remaining
for i = 1, skillRepeat do
self:ResolveCardEffects(cardId, slot, c, false, cost)
applyCardPlayHooks()
end
end
if c.kind == "Attack" then
self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1
end
@@ -70,8 +100,8 @@ if skillFree == true then
self.NextSkillCostZero = false
end
end
if self:HasPowerField("cardPlayedBlock") == true then
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then
self.ActiveKillReward = 0
end
table.remove(self.Hand, slot)
if c.exhaust == true then
@@ -227,7 +257,7 @@ end`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
if m == nil or m.alive ~= true then
m = nil
for i = 1, #self.Monsters do
@@ -235,7 +265,7 @@ if m == nil or m.alive ~= true then
end
end
if m == nil then
return
return false
end
local dmg = amount
if m.vuln > 0 then
@@ -247,14 +277,71 @@ if m.block > 0 and pierce ~= true then
dmg = dmg - absorbed
end
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)
local killed = false
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
end`, [
killed = true
end
return killed`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
]),
], 0, 'boolean'),
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 false
end
m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot)
local killed = false
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
killed = true
end
return killed`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
], 0, 'boolean'),
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 false
end
local m = alive[math.random(1, #alive)]
if m == nil then
return false
end
m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot)
local killed = false
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
killed = true
end
return killed`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
], 0, 'boolean'),
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
self:DealDamageToTarget(damage, pierce)
@@ -283,7 +370,11 @@ _TimerService:SetTimerOnce(function()
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
shown = math.floor(damage * 1.5)
end
self:DealDamageToTarget(damage, pierce)
local killed = self:DealDamageToTarget(damage, pierce)
if killed == true and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + self.ActiveKillReward
end
self.ActiveKillReward = 0
self:ShowDmgPop(targetIndex, shown)
self:RenderCombat()
self:CheckCombatEnd()
@@ -307,6 +398,7 @@ end
_TimerService:SetTimerOnce(function()
if fx ~= nil then fx.Enable = false end
self.FxBusy = false
local killCount = 0
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m ~= nil and m.alive == true then
@@ -320,14 +412,25 @@ _TimerService:SetTimerOnce(function()
dmg = dmg - absorbed
end
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:MonsterHitMotion(i)
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
killCount = killCount + 1
end
end
end
if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
end
self.ActiveKillReward = 0
self:RenderCombat()
self:CheckCombatEnd()
end, 0.35)`, [
@@ -353,6 +456,9 @@ if self.PlayerBlock > 0 then
self.PlayerBlock = self.PlayerBlock - absorbed
dmg = dmg - absorbed
end
if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then
dmg = 1
end
if dmg > 0 then
self.PlayerHp = self.PlayerHp - dmg
local reflect = self.PlayerThorns or 0

View File

@@ -246,6 +246,7 @@ if self.ClayBlockNext > 0 then
end
self.TurnAttackMultiplier = self.NextTurnAttackMultiplier or 1
self.NextTurnAttackMultiplier = 1
self.CardsDrawnThisCombat = self.CardsDrawnThisCombat or 0
self.HandCostZeroThisTurn = false
self.DrawDisabledThisTurn = false
local powerTurnDraw = 0
@@ -425,6 +426,19 @@ for i = 1, #self.Hand do
\tend
end
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.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
self:RenderHand(false)
@@ -445,6 +459,7 @@ for i = 1, amount do
\tend
\tlocal cardId = table.remove(self.DrawPile)
\ttable.insert(drawnCards, cardId)
\tself.CardsDrawnThisCombat = (self.CardsDrawnThisCombat or 0) + 1
\tif #self.Hand >= 10 then
\t\ttable.insert(self.DiscardPile, cardId)
\t\tself:TriggerSly(cardId)

View File

@@ -308,6 +308,9 @@ end
if c.damagePerSkillInHand ~= nil then
base2 = base2 + self:CountOtherHandSkills(slot) * c.damagePerSkillInHand
end
if c.damagePerCardDrawnThisCombat ~= nil then
base2 = base2 + (self.CardsDrawnThisCombat or 0) * c.damagePerCardDrawnThisCombat
end
if base2 < 0 then
base2 = 0
end
@@ -379,6 +382,9 @@ end
if c.nextSkillCostZero == true then
self.NextSkillCostZero = true
end
if c.nextSkillRepeatCount ~= nil and c.nextSkillRepeatCount > 0 then
self.NextSkillRepeatCount = (self.NextSkillRepeatCount or 0) + c.nextSkillRepeatCount
end
if c.skillCostReductionThisTurn ~= nil and c.skillCostReductionThisTurn > 0 then
self.SkillCostReductionThisTurn = (self.SkillCostReductionThisTurn or 0) + c.skillCostReductionThisTurn
end
@@ -388,10 +394,20 @@ end
if c.drawDisabledThisTurn == true then
self.DrawDisabledThisTurn = true
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.damage ~= nil then
if c.damage ~= nil or c.xDamagePerEnergy ~= nil then
self:PlayerAttackMotion()
local baseDmg = self:AttackBaseForCard(slot, c)
if c.xDamagePerEnergy ~= nil and c.xDamagePerEnergy > 0 then
baseDmg = xEnergy * c.xDamagePerEnergy
end
local total = 0
local hitN = c.hits or 1
if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then
@@ -446,8 +462,11 @@ end
if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then
self.Energy = self.Energy + c.gainEnergy
end
if c.intangible ~= nil and c.intangible > 0 then
self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible
end
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]
if tm == nil or tm.alive ~= true then
for i = 1, #self.Monsters do
@@ -455,10 +474,10 @@ if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
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 weakAmount ~= nil and weakAmount > 0 then tm.weak = tm.weak + weakAmount end
if poisonAmount ~= nil and poisonAmount > 0 then tm.poison = (tm.poison or 0) + poisonAmount end
if vulnAmount ~= nil and vulnAmount > 0 then
tm.vuln = tm.vuln + vulnAmount
if self:HasRelic("championBelt") then
tm.weak = tm.weak + 1
end
@@ -500,13 +519,14 @@ end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
{ 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]
if c == nil or c.sly ~= true then
return
end
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
return
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:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
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 pb ~= "" then pb = pb .. " " end
pb = pb .. "민첩+" .. tostring(self.PlayerDex)

View File

@@ -38,7 +38,7 @@ end`),
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
return
end
if slot ~= 0 and self.RewardChoices ~= nil then
@@ -47,6 +47,11 @@ if slot ~= 0 and self.RewardChoices ~= nil then
table.insert(self.RunDeck, id)
end
end
if self.BonusRewardScreens ~= nil and self.BonusRewardScreens > 0 and slot ~= 0 then
self.BonusRewardScreens = self.BonusRewardScreens - 1
self:OfferReward()
return
end
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
if hud ~= nil then
hud.Enable = false

View File

@@ -68,15 +68,20 @@ self.MaxEnergy = 3
self.Turn = 0
self.PlayerBlock = 0
self.BlockGainMultiplier = 1
self.CardsDrawnThisCombat = 0
self.HandCostZeroThisTurn = false
self.DrawDisabledThisTurn = false
self.NextSkillCostZero = false
self.NextSkillRepeatCount = 0
self.SkillCostReductionThisTurn = 0
self.PlayerStr = 0
self.PlayerDex = 0
self.PlayerThorns = 0
self.PlayerWeak = 0
self.PlayerVuln = 0
self.PlayerIntangible = 0
self.BonusRewardScreens = 0
self.ActiveKillReward = 0
self.PlayerPowers = {}
self.FightAttackCount = 0
self.TurnAttackCardsPlayed = 0

View File

@@ -162,8 +162,15 @@ function luaCardsTable(cards) {
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`);
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`);
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.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`);
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.attackPoison != null) fields.push(`attackPoison = ${c.attackPoison}`);
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`);
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`);
if (c.block != null) fields.push(`block = ${c.block}`);
@@ -198,6 +205,9 @@ function luaCardsTable(cards) {
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.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.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`);
if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true');
@@ -205,6 +215,7 @@ function luaCardsTable(cards) {
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${c.nextSkillRepeatCount}`);
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');