feat: 도적 공용 카드 효과 구현 #89
File diff suppressed because one or more lines are too long
@@ -59,6 +59,7 @@
|
|||||||
"cost": 2,
|
"cost": 2,
|
||||||
"kind": "Attack",
|
"kind": "Attack",
|
||||||
"damage": 8,
|
"damage": 8,
|
||||||
|
"firstCardDamageBonus": 2,
|
||||||
"vuln": 2,
|
"vuln": 2,
|
||||||
"desc": "피해 8, 취약 2",
|
"desc": "피해 8, 취약 2",
|
||||||
"image": "fe83c7635b0e49ed83d75a2833adb53e",
|
"image": "fe83c7635b0e49ed83d75a2833adb53e",
|
||||||
@@ -527,7 +528,8 @@
|
|||||||
"damage": 3,
|
"damage": 3,
|
||||||
"hits": 4,
|
"hits": 4,
|
||||||
"sly": true,
|
"sly": true,
|
||||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
"image": "1b0f2dc8abd0434990eee1befefcbe0d",
|
||||||
|
"randomTargetEachHit": true
|
||||||
},
|
},
|
||||||
"Prepared": {
|
"Prepared": {
|
||||||
"name": "예비",
|
"name": "예비",
|
||||||
@@ -536,7 +538,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "normal",
|
"rarity": "normal",
|
||||||
"desc": "카드를 1장 뽑습니다. 카드를 1장 버립니다.",
|
"desc": "카드를 1장 뽑습니다. 카드를 1장 버립니다.",
|
||||||
"draw": 1,
|
"blockPerDamageDealtThisTurn": 1,
|
||||||
"discard": 1,
|
"discard": 1,
|
||||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
||||||
},
|
},
|
||||||
@@ -601,7 +603,9 @@
|
|||||||
"rarity": "normal",
|
"rarity": "normal",
|
||||||
"desc": "이번 턴 동안 모든 적이 힘을 6 잃습니다. 소멸.",
|
"desc": "이번 턴 동안 모든 적이 힘을 6 잃습니다. 소멸.",
|
||||||
"draw": 1,
|
"draw": 1,
|
||||||
"image": "0946f69d84464df29b24b94c744c868d"
|
"image": "0946f69d84464df29b24b94c744c868d",
|
||||||
|
"affectsAllEnemies": true,
|
||||||
|
"enemyStrengthLossThisTurn": 6
|
||||||
},
|
},
|
||||||
"CloakAndDagger": {
|
"CloakAndDagger": {
|
||||||
"name": "망토와 단검",
|
"name": "망토와 단검",
|
||||||
@@ -786,7 +790,10 @@
|
|||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "대상 적의 모든 인공물과 방어도를 제거합니다. 취약을 2 부여합니다. 소멸.",
|
"desc": "대상 적의 모든 인공물과 방어도를 제거합니다. 취약을 2 부여합니다. 소멸.",
|
||||||
"vuln": 2,
|
"vuln": 2,
|
||||||
"image": "0946f69d84464df29b24b94c744c868d"
|
"image": "0946f69d84464df29b24b94c744c868d",
|
||||||
|
"affectsAllEnemies": true,
|
||||||
|
"removeEnemyBlock": true,
|
||||||
|
"removeEnemyArtifact": true
|
||||||
},
|
},
|
||||||
"HiddenDaggers": {
|
"HiddenDaggers": {
|
||||||
"name": "숨겨진 단검",
|
"name": "숨겨진 단검",
|
||||||
@@ -829,7 +836,8 @@
|
|||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "방어도를 7 얻습니다. 이번 턴 동안 손에 있는 스킬 카드 1장에 교활을 추가합니다.",
|
"desc": "방어도를 7 얻습니다. 이번 턴 동안 손에 있는 스킬 카드 1장에 교활을 추가합니다.",
|
||||||
"block": 7,
|
"block": 7,
|
||||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
"image": "c1e19219745e44c39ae6ac2f77e347d9",
|
||||||
|
"turnHandSlyCount": 1
|
||||||
},
|
},
|
||||||
"Mirage": {
|
"Mirage": {
|
||||||
"name": "신기루",
|
"name": "신기루",
|
||||||
@@ -859,7 +867,8 @@
|
|||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "적이 중독을 보유하고 있다면, 중독을 9 부여합니다.",
|
"desc": "적이 중독을 보유하고 있다면, 중독을 9 부여합니다.",
|
||||||
"poison": 9,
|
"poison": 9,
|
||||||
"image": "19361e72087946b1888684185b40d935"
|
"image": "19361e72087946b1888684185b40d935",
|
||||||
|
"poisonIfTargetPoisoned": true
|
||||||
},
|
},
|
||||||
"Blur": {
|
"Blur": {
|
||||||
"name": "흐릿함",
|
"name": "흐릿함",
|
||||||
@@ -891,7 +900,8 @@
|
|||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "표창을 3장 손으로 가져옵니다. 이 카드의 비용이 1 감소합니다.",
|
"desc": "표창을 3장 손으로 가져옵니다. 이 카드의 비용이 1 감소합니다.",
|
||||||
"addShiv": 3,
|
"addShiv": 3,
|
||||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
"image": "1b0f2dc8abd0434990eee1befefcbe0d",
|
||||||
|
"combatCostReductionOnPlay": 1
|
||||||
},
|
},
|
||||||
"BouncingFlask": {
|
"BouncingFlask": {
|
||||||
"name": "탄성 플라스크",
|
"name": "탄성 플라스크",
|
||||||
@@ -975,12 +985,10 @@
|
|||||||
"kind": "Power",
|
"kind": "Power",
|
||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "중독을 3번 부여할 때마다, 모든 적에게 피해를 11 줍니다.",
|
"desc": "독이 3번 부여될 때마다 모든 적에게 11 피해를 줍니다.",
|
||||||
"aoe": true,
|
"image": "19361e72087946b1888684185b40d935",
|
||||||
"powerEffect": "strengthPerTurn",
|
"poisonApplicationBurstEvery": 3,
|
||||||
"value": 1,
|
"poisonApplicationBurstDamage": 11
|
||||||
"damage": 11,
|
|
||||||
"image": "19361e72087946b1888684185b40d935"
|
|
||||||
},
|
},
|
||||||
"NoxiousFumes": {
|
"NoxiousFumes": {
|
||||||
"name": "유독 가스",
|
"name": "유독 가스",
|
||||||
@@ -1059,7 +1067,8 @@
|
|||||||
"desc": "모든 적에게 피해를 10 줍니다. 적을 처치할 때마다 이 효과를 반복합니다.",
|
"desc": "모든 적에게 피해를 10 줍니다. 적을 처치할 때마다 이 효과를 반복합니다.",
|
||||||
"aoe": true,
|
"aoe": true,
|
||||||
"damage": 10,
|
"damage": 10,
|
||||||
"image": "dbdbb1b56ae54672ae68ac6882fff6a2"
|
"image": "dbdbb1b56ae54672ae68ac6882fff6a2",
|
||||||
|
"repeatOnKill": true
|
||||||
},
|
},
|
||||||
"TheHunt": {
|
"TheHunt": {
|
||||||
"name": "사냥",
|
"name": "사냥",
|
||||||
@@ -1228,10 +1237,9 @@
|
|||||||
"kind": "Power",
|
"kind": "Power",
|
||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "중독이 1번 추가로 발동합니다.",
|
"desc": "적 턴 시작 시 독이 한 번 더 틱합니다.",
|
||||||
"powerEffect": "strengthPerTurn",
|
"image": "19361e72087946b1888684185b40d935",
|
||||||
"value": 1,
|
"extraPoisonTicks": 1
|
||||||
"image": "19361e72087946b1888684185b40d935"
|
|
||||||
},
|
},
|
||||||
"Envenom": {
|
"Envenom": {
|
||||||
"name": "독 바르기",
|
"name": "독 바르기",
|
||||||
@@ -1249,10 +1257,9 @@
|
|||||||
"kind": "Power",
|
"kind": "Power",
|
||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "스킬 카드를 사용 시, 그 카드가 교활을 얻습니다.",
|
"desc": "사용한 스킬 카드는 교활해집니다.",
|
||||||
"powerEffect": "strengthPerTurn",
|
"image": "c1e19219745e44c39ae6ac2f77e347d9",
|
||||||
"value": 1,
|
"skillSlyOnPlay": true
|
||||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
|
||||||
},
|
},
|
||||||
"Tracking": {
|
"Tracking": {
|
||||||
"name": "추적",
|
"name": "추적",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Current status of bandit cards and shared effect hooks.
|
|||||||
|
|
||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
`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`, `BouncingFlask`, `Accuracy`, `PhantomBlades`, `Speedster`, `CorrosiveWave`, `Tracking`, `FanOfKnives`
|
`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`, `BouncingFlask`, `Accuracy`, `PhantomBlades`, `Speedster`, `CorrosiveWave`, `Tracking`, `FanOfKnives`, `Strangle`, `Mirage`, `Accelerant`, `MasterPlanner`, `Outbreak`, `EscapePlan`, `HandTrick`, `NoxiousFumes`, `Pinpoint`, `TheHunt`, `Murder`, `Malaise`, `BladeOfInk`, `KnifeTrap`, `BulletTime`, `Envenom`, `SerpentForm`, `WraithForm`, `Skewer`, `Ricochet`, `Anticipate`, `PiercingWail`, `Expose`, `UpMySleeve`, `EchoingSlash`, `BubbleBubble`
|
||||||
|
|
||||||
Shared hooks already in use:
|
Shared hooks already in use:
|
||||||
|
|
||||||
@@ -13,67 +13,10 @@ Shared hooks already in use:
|
|||||||
- `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`, `blockGainMultiplier`, `nextSkillCostZero`, `skillCostReductionThisTurn`
|
- `gainEnergy`, `drawUntilHandSize`, `drawPerDiscarded`, `cardPlayedBlock`, `blockGainMultiplier`, `blockPerDamageDealtThisTurn`, `nextSkillCostZero`, `skillCostReductionThisTurn`
|
||||||
- `drawDamage`, `drawPoison`, `shivDamageBonus`, `firstShivDamageBonus`, `shivRetain`, `shivAoe`, `attackDamageVsWeakMultiplier`, `poisonHits`, `poisonRandomTargets`
|
- `firstCardDamageBonus`
|
||||||
|
- `drawDamage`, `drawPoison`, `shivDamageBonus`, `firstShivDamageBonus`, `shivRetain`, `shivAoe`, `attackDamageVsWeakMultiplier`, `poisonHits`, `poisonRandomTargets`, `skillSlyOnPlay`, `extraPoisonTicks`, `poisonApplicationBurstEvery`, `poisonApplicationBurstDamage`
|
||||||
## Partial
|
|
||||||
|
|
||||||
`Ricochet`: random split attacks need fuller targeting rules.
|
|
||||||
|
|
||||||
`Anticipate`: next-turn enemy intent preview plus block still needs exact behavior review.
|
|
||||||
|
|
||||||
`PiercingWail`: enemy attack reduction is implemented as a shared Weak/Vulnerable style effect, but needs final balance review.
|
|
||||||
|
|
||||||
`Expose`: weak/vulnerable interaction is only partially modeled.
|
|
||||||
|
|
||||||
`BubbleBubble`: still missing the condition for poison dragon form ownership.
|
|
||||||
|
|
||||||
`Skewer`: X-cost attack.
|
|
||||||
|
|
||||||
`Outbreak`: every 3 poison applications deal 11 to all enemies.
|
|
||||||
|
|
||||||
`Strangle`: extra damage the first time a card is used.
|
|
||||||
|
|
||||||
`EscapePlan`: gain block when discarded.
|
|
||||||
|
|
||||||
`HandTrick`: one card becomes sly and grants block when discarded.
|
|
||||||
|
|
||||||
`Mirage`: gain block equal to total damage dealt this turn.
|
|
||||||
|
|
||||||
`UpMySleeve`: create Shiv plus reduce discard cost.
|
|
||||||
|
|
||||||
`NoxiousFumes`: poison all enemies at turn start.
|
|
||||||
|
|
||||||
`EchoingSlash`: repeat on kill.
|
|
||||||
|
|
||||||
`TheHunt`: kill reward is implemented.
|
|
||||||
|
|
||||||
`Murder`: damage scales with cards drawn this combat.
|
|
||||||
|
|
||||||
`Malaise`: X-cost weak/vulnerable reduction.
|
|
||||||
|
|
||||||
`Pinpoint`: per-turn discard-based attack damage.
|
|
||||||
|
|
||||||
`BladeOfInk`: turn-based Shiv creation.
|
|
||||||
|
|
||||||
`KnifeTrap`: playable only when draw pile is empty.
|
|
||||||
|
|
||||||
`BulletTime`: hand cost zero plus draw disabled.
|
|
||||||
|
|
||||||
`Accelerant`: poison trigger timing is not finalized.
|
|
||||||
|
|
||||||
`Envenom`: attack poison on hit.
|
|
||||||
|
|
||||||
`MasterPlanner`: skill discard interaction still needs a clear rules decision.
|
|
||||||
|
|
||||||
`SerpentForm`: damage on card play.
|
|
||||||
|
|
||||||
`WraithForm`: intangible plus turn-end block loss.
|
|
||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
- `MasterPlanner`
|
None at the moment.
|
||||||
- `Accelerant`
|
|
||||||
- `Outbreak`
|
|
||||||
|
|
||||||
These three still need a confirmed rule interpretation before we lock them in.
|
|
||||||
|
|||||||
@@ -14,10 +14,18 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
|
|||||||
- `damagePerTurn`: damage applied at turn start
|
- `damagePerTurn`: damage applied at turn start
|
||||||
- `cardPlayedDamage`: damage when the card is played
|
- `cardPlayedDamage`: damage when the card is played
|
||||||
- `cardPlayedRandomDamage`: random damage when the card is played
|
- `cardPlayedRandomDamage`: random damage when the card is played
|
||||||
|
- `rewardOnKill`: gain bonus reward screens when the card kills
|
||||||
|
- `randomTargetEachHit`: choose a random alive enemy for each hit
|
||||||
|
- `repeatOnKill`: repeat the attack when it kills at least one enemy
|
||||||
|
- `firstCardDamageBonus`: bonus damage for the first card played this turn
|
||||||
- `drawDamage`: damage dealt when a card is drawn
|
- `drawDamage`: damage dealt when a card is drawn
|
||||||
|
- `blockPerDamageDealtThisTurn`: gain block equal to damage dealt this turn
|
||||||
- `shivDamageBonus`: bonus damage for all Shivs
|
- `shivDamageBonus`: bonus damage for all Shivs
|
||||||
- `firstShivDamageBonus`: bonus damage for the first Shiv each turn
|
- `firstShivDamageBonus`: bonus damage for the first Shiv each turn
|
||||||
- `attackDamageVsWeakMultiplier`: multiplier when the attack hits Weak targets
|
- `attackDamageVsWeakMultiplier`: multiplier when the attack hits Weak targets
|
||||||
|
- `useAllEnergy`: treat the card as spending all available energy
|
||||||
|
- `xDamagePerEnergy`: scale attack damage by energy spent
|
||||||
|
- `xWeakPerEnergy`: scale Weak applied by energy spent
|
||||||
|
|
||||||
## Block and utility
|
## Block and utility
|
||||||
|
|
||||||
@@ -31,12 +39,15 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
|
|||||||
- `drawUntilHandSize`: draw until hand reaches a target size
|
- `drawUntilHandSize`: draw until hand reaches a target size
|
||||||
- `drawSkillBlock`: gain block for each Skill drawn
|
- `drawSkillBlock`: gain block for each Skill drawn
|
||||||
- `drawPoison`: apply poison when a card is drawn
|
- `drawPoison`: apply poison when a card is drawn
|
||||||
|
- `handCostZeroThisTurn`: make hand cards cost 0 this turn
|
||||||
|
- `drawDisabledThisTurn`: disable draw for the rest of the turn
|
||||||
- `heal`: heal immediately
|
- `heal`: heal immediately
|
||||||
- `gainEnergy`: gain energy immediately
|
- `gainEnergy`: gain energy immediately
|
||||||
- `strength`: gain Strength
|
- `strength`: gain Strength
|
||||||
- `dex`: gain Dexterity
|
- `dex`: gain Dexterity
|
||||||
- `thorns`: gain Thorns
|
- `thorns`: gain Thorns
|
||||||
- `selfVuln`: apply Vulnerable to self
|
- `selfVuln`: apply Vulnerable to self
|
||||||
|
- `extraPoisonTicks`: add extra poison ticks at enemy turn start
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
@@ -45,6 +56,19 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
|
|||||||
- `poison`: apply Poison
|
- `poison`: apply Poison
|
||||||
- `poisonHits`: apply poison multiple times
|
- `poisonHits`: apply poison multiple times
|
||||||
- `poisonRandomTargets`: spread poison applications across random alive enemies
|
- `poisonRandomTargets`: spread poison applications across random alive enemies
|
||||||
|
- `poisonIfTargetPoisoned`: apply poison only if the target is already poisoned
|
||||||
|
- `poisonApplicationBurstEvery`: trigger a burst every N poison applications
|
||||||
|
- `poisonApplicationBurstDamage`: burst damage when the poison application threshold is reached
|
||||||
|
- `skillSlyOnPlay`: make a played Skill card count as sly when it is later discarded
|
||||||
|
- `turnHandSlyCount`: mark up to N other Skill cards in hand as sly for this turn
|
||||||
|
- `attackPoison`: apply poison when attack damage is dealt
|
||||||
|
- `intangible`: reduce incoming damage to 1 for the duration
|
||||||
|
- `endTurnDexLoss`: lose Dexterity at end of turn
|
||||||
|
- `combatCostReductionOnPlay`: reduce this card's cost each time it is played this combat
|
||||||
|
- `enemyStrengthLossThisTurn`: reduce enemy Strength for the rest of the turn
|
||||||
|
- `affectsAllEnemies`: apply the card's debuffs to every alive enemy
|
||||||
|
- `removeEnemyBlock`: clear enemy block when the card resolves
|
||||||
|
- `removeEnemyArtifact`: consume enemy Artifact when the card resolves
|
||||||
|
|
||||||
`poison` deals damage at enemy turn start and then decreases by 1.
|
`poison` deals damage at enemy turn start and then decreases by 1.
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export function calcAttack(base, str, weak, vulnOnTarget) {
|
|||||||
return dmg;
|
return dmg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calcEnemyAttack(base, str, weak, vulnOnTarget, strengthLoss = 0) {
|
||||||
|
return calcAttack(base, Math.max(0, str - strengthLoss), weak, vulnOnTarget);
|
||||||
|
}
|
||||||
|
|
||||||
// 방어 우선 차감 후 hp 적용 → { hp, block }
|
// 방어 우선 차감 후 hp 적용 → { hp, block }
|
||||||
export function applyDamage(hp, block, amount) {
|
export function applyDamage(hp, block, amount) {
|
||||||
let dmg = amount;
|
let dmg = amount;
|
||||||
@@ -100,12 +104,16 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
|
|||||||
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));
|
||||||
}
|
}
|
||||||
|
if (ctx.combatCardCostReduction && ctx.combatCardCostReduction[x.id] != null) {
|
||||||
|
effectiveCost = Math.max(0, effectiveCost - ctx.combatCardCostReduction[x.id]);
|
||||||
|
}
|
||||||
return card.useAllEnergy === true ? true : 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');
|
||||||
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
|
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
|
||||||
const effectiveCost = (card) => {
|
const effectiveCost = (x) => {
|
||||||
|
const card = cards[x.id];
|
||||||
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.useAllEnergy === true) cost = 1;
|
||||||
@@ -113,10 +121,13 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
|
|||||||
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));
|
||||||
}
|
}
|
||||||
|
if (ctx.combatCardCostReduction && ctx.combatCardCostReduction[x.id] != null) {
|
||||||
|
cost = Math.max(0, cost - ctx.combatCardCostReduction[x.id]);
|
||||||
|
}
|
||||||
return cost;
|
return cost;
|
||||||
};
|
};
|
||||||
const dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(effectiveCost(cards[x.id]), 1);
|
const dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(effectiveCost(x), 1);
|
||||||
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(effectiveCost(cards[x.id]), 1);
|
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(effectiveCost(x), 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;
|
||||||
@@ -155,25 +166,96 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
let nextSkillCostZero = false;
|
let nextSkillCostZero = false;
|
||||||
let nextSkillRepeatCount = 0;
|
let nextSkillRepeatCount = 0;
|
||||||
let skillCostReductionThisTurn = 0;
|
let skillCostReductionThisTurn = 0;
|
||||||
|
const combatCardCostReduction = {};
|
||||||
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 = [];
|
||||||
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
|
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
|
||||||
|
let turnCardsPlayedThisTurn = 0;
|
||||||
|
let damageDealtThisTurn = 0;
|
||||||
let shivFirstDamageBonusUsed = false;
|
let shivFirstDamageBonusUsed = false;
|
||||||
let drawDamageThisTurn = 0;
|
let drawDamageThisTurn = 0;
|
||||||
let drawPoisonThisTurn = 0;
|
let drawPoisonThisTurn = 0;
|
||||||
let shivAoeThisCombat = false;
|
let shivAoeThisCombat = false;
|
||||||
|
const skillSlyOnPlayCards = new Set();
|
||||||
|
const turnSkillSlyCards = new Set();
|
||||||
|
let poisonApplicationsThisCombat = 0;
|
||||||
|
let enemyStrengthLossThisTurn = 0;
|
||||||
let cardsDrawnThisCombat = 0;
|
let cardsDrawnThisCombat = 0;
|
||||||
let bonusRewardScreens = 0;
|
let bonusRewardScreens = 0;
|
||||||
let activeKillReward = 0;
|
let activeKillReward = 0;
|
||||||
let energy = 0;
|
let energy = 0;
|
||||||
const powers = [];
|
const powers = [];
|
||||||
const mob = monsters.map((m) => ({
|
const mob = monsters.map((m) => ({
|
||||||
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: 0, weak: 0, vuln: 0, poison: 0,
|
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: m.str || 0, weak: 0, vuln: 0, poison: 0, artifact: m.artifact || 0,
|
||||||
intents: m.intents, intentIdx: 0, alive: true,
|
intents: m.intents, intentIdx: 0, alive: true,
|
||||||
}));
|
}));
|
||||||
let turns = 0;
|
let turns = 0;
|
||||||
|
|
||||||
|
const aliveMonsters = () => mob.filter((m) => m.alive);
|
||||||
|
const countAliveMonsters = () => aliveMonsters().length;
|
||||||
|
const randomAliveMonster = () => {
|
||||||
|
const alive = aliveMonsters();
|
||||||
|
if (!alive.length) return null;
|
||||||
|
return alive[Math.floor(rng() * alive.length)];
|
||||||
|
};
|
||||||
|
const removeEnemyBlock = (target) => {
|
||||||
|
if (target) target.block = 0;
|
||||||
|
};
|
||||||
|
const removeEnemyArtifact = (target) => {
|
||||||
|
if (target) target.artifact = 0;
|
||||||
|
};
|
||||||
|
const applyMonsterWeak = (target, amount) => {
|
||||||
|
if (!target || !amount || amount <= 0) return;
|
||||||
|
if (target.artifact > 0) { target.artifact--; return; }
|
||||||
|
target.weak += amount;
|
||||||
|
};
|
||||||
|
const applyMonsterVuln = (target, amount) => {
|
||||||
|
if (!target || !amount || amount <= 0) return;
|
||||||
|
if (target.artifact > 0) { target.artifact--; return; }
|
||||||
|
target.vuln += amount;
|
||||||
|
};
|
||||||
|
const applyPoisonToMonster = (target, amount) => {
|
||||||
|
if (!target || !target.alive || !amount || amount <= 0) return;
|
||||||
|
if (target.artifact > 0) { target.artifact--; return; }
|
||||||
|
target.poison += amount;
|
||||||
|
poisonApplicationsThisCombat += 1;
|
||||||
|
const burstEvery = powerFieldTotal('poisonApplicationBurstEvery');
|
||||||
|
const burstDamage = powerFieldTotal('poisonApplicationBurstDamage');
|
||||||
|
if (burstEvery > 0 && burstDamage > 0 && poisonApplicationsThisCombat % burstEvery === 0) {
|
||||||
|
for (const m of mob) {
|
||||||
|
if (!m.alive) continue;
|
||||||
|
const r = applyDamage(m.hp, m.block, burstDamage);
|
||||||
|
m.hp = r.hp; m.block = r.block;
|
||||||
|
if (burstDamage > 0) damageDealtThisTurn += burstDamage;
|
||||||
|
if (m.hp <= 0) m.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dealDamageToMonster = (target, amount, pierce = false) => {
|
||||||
|
if (!target || !target.alive) return false;
|
||||||
|
let dmg = amount;
|
||||||
|
const effectiveStr = Math.max(0, target.str - enemyStrengthLossThisTurn);
|
||||||
|
dmg = calcAttack(dmg, effectiveStr, target.weak, 0);
|
||||||
|
if (target.vuln > 0) dmg = Math.floor(dmg * 1.5);
|
||||||
|
if (target.block > 0 && !pierce) {
|
||||||
|
const absorbed = Math.min(target.block, dmg);
|
||||||
|
target.block -= absorbed;
|
||||||
|
dmg -= absorbed;
|
||||||
|
}
|
||||||
|
target.hp -= dmg;
|
||||||
|
if (dmg > 0) {
|
||||||
|
const attackPoison = powerFieldTotal('attackPoison');
|
||||||
|
if (attackPoison > 0) applyPoisonToMonster(target, attackPoison);
|
||||||
|
}
|
||||||
|
if (target.hp <= 0) {
|
||||||
|
target.hp = 0;
|
||||||
|
target.alive = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
function draw(n) {
|
function draw(n) {
|
||||||
const drawn = [];
|
const drawn = [];
|
||||||
if (drawDisabledThisTurn === true) return drawn;
|
if (drawDisabledThisTurn === true) return drawn;
|
||||||
@@ -195,8 +277,11 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
m.block -= absorbed;
|
m.block -= absorbed;
|
||||||
dmg -= absorbed;
|
dmg -= absorbed;
|
||||||
}
|
}
|
||||||
if (drawPoison > 0) m.poison += drawPoison;
|
if (drawPoison > 0) applyPoisonToMonster(m, drawPoison);
|
||||||
if (dmg > 0) m.hp -= dmg;
|
if (dmg > 0) {
|
||||||
|
m.hp -= dmg;
|
||||||
|
damageDealtThisTurn += dmg;
|
||||||
|
}
|
||||||
if (m.hp <= 0) { m.hp = 0; m.alive = false; }
|
if (m.hp <= 0) { m.hp = 0; m.alive = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,6 +342,7 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
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 (c.damagePerCardDrawnThisCombat) base += cardsDrawnThisCombat * c.damagePerCardDrawnThisCombat;
|
||||||
|
if (c.class === 'Attack' && turnCardsPlayedThisTurn === 0 && c.firstCardDamageBonus) base += c.firstCardDamageBonus;
|
||||||
if (c.class === 'shiv') {
|
if (c.class === 'shiv') {
|
||||||
if (powerFieldTotal('shivDamageBonus') > 0) base += powerFieldTotal('shivDamageBonus');
|
if (powerFieldTotal('shivDamageBonus') > 0) base += powerFieldTotal('shivDamageBonus');
|
||||||
if (!shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) base += powerFieldTotal('firstShivDamageBonus');
|
if (!shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) base += powerFieldTotal('firstShivDamageBonus');
|
||||||
@@ -314,6 +400,19 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (c.drawDamage && c.kind !== 'Power') drawDamageThisTurn += c.drawDamage;
|
if (c.drawDamage && c.kind !== 'Power') drawDamageThisTurn += c.drawDamage;
|
||||||
if (c.drawPoison && c.kind !== 'Power') drawPoisonThisTurn += c.drawPoison;
|
if (c.drawPoison && c.kind !== 'Power') drawPoisonThisTurn += c.drawPoison;
|
||||||
if (c.shivAoe === true && c.kind !== 'Power') shivAoeThisCombat = true;
|
if (c.shivAoe === true && c.kind !== 'Power') shivAoeThisCombat = true;
|
||||||
|
if (c.skillSlyOnPlay === true && c.kind === 'Skill') skillSlyOnPlayCards.add(id);
|
||||||
|
if (c.turnHandSlyCount && c.turnHandSlyCount > 0) {
|
||||||
|
let picked = 0;
|
||||||
|
for (const hid of hand) {
|
||||||
|
if (hid === id) continue;
|
||||||
|
const hc = cards[hid];
|
||||||
|
if (hc?.kind === 'Skill' && !turnSkillSlyCards.has(hid) && !skillSlyOnPlayCards.has(hid) && hc.sly !== true) {
|
||||||
|
turnSkillSlyCards.add(hid);
|
||||||
|
picked++;
|
||||||
|
if (picked >= c.turnHandSlyCount) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const xEnergy = costSpent || 0;
|
const xEnergy = costSpent || 0;
|
||||||
if (c.kind === 'Attack') {
|
if (c.kind === 'Attack') {
|
||||||
if (alive.length && (c.damage || c.xDamagePerEnergy)) {
|
if (alive.length && (c.damage || c.xDamagePerEnergy)) {
|
||||||
@@ -321,49 +420,70 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
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;
|
||||||
const preview = calcAttack(baseDamage || 0, pStr, pWeak, 0) * turnAttackMultiplier;
|
|
||||||
const target = chooseTarget(alive, preview);
|
|
||||||
if (c.weak) target.weak += c.weak;
|
|
||||||
if (c.vuln) target.vuln += c.vuln;
|
|
||||||
let totalNv = 0;
|
|
||||||
for (let h = 0; h < hitN; h++) totalNv += calcAttack(baseDamage || 0, pStr, pWeak, 0) * turnAttackMultiplier;
|
|
||||||
dmg = totalNv;
|
|
||||||
let useAoe = c.aoe === true;
|
let useAoe = c.aoe === true;
|
||||||
if (c.class === 'shiv' && shivAoeThisCombat === true) useAoe = true;
|
if (c.class === 'shiv' && shivAoeThisCombat === true) useAoe = true;
|
||||||
if (useAoe === true) {
|
const perHit = calcAttack(baseDamage || 0, pStr, pWeak, 0) * turnAttackMultiplier;
|
||||||
for (const m2 of aliveList()) {
|
const dealToTarget = (target, amount) => {
|
||||||
let d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
if (!target || !target.alive) return { killed: false, dealt: 0 };
|
||||||
if (m2.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) {
|
let dealt = amount;
|
||||||
d2 = Math.floor(d2 * c.attackDamageVsWeakMultiplier);
|
if (target.vuln > 0) dealt = Math.floor(dealt * 1.5);
|
||||||
}
|
|
||||||
const r2 = applyDamage(m2.hp, m2.block, d2);
|
|
||||||
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 (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
|
||||||
if (target.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) {
|
if (target.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) {
|
||||||
dmg = Math.floor(dmg * c.attackDamageVsWeakMultiplier);
|
dealt = Math.floor(dealt * c.attackDamageVsWeakMultiplier);
|
||||||
}
|
}
|
||||||
if (c.pierce === true) {
|
if (c.pierce === true) {
|
||||||
target.hp -= dmg;
|
target.hp -= dealt;
|
||||||
if (target.hp < 0) target.hp = 0;
|
if (target.hp < 0) target.hp = 0;
|
||||||
} else {
|
} else {
|
||||||
const r = applyDamage(target.hp, target.block, dmg);
|
const r = applyDamage(target.hp, target.block, dealt);
|
||||||
target.hp = r.hp; target.block = r.block;
|
target.hp = r.hp; target.block = r.block;
|
||||||
}
|
}
|
||||||
const attackPoison = powerFieldTotal('attackPoison');
|
const attackPoison = powerFieldTotal('attackPoison');
|
||||||
if (dmg > 0 && attackPoison > 0) target.poison += attackPoison;
|
if (dealt > 0 && attackPoison > 0) applyPoisonToMonster(target, attackPoison);
|
||||||
|
let killed = false;
|
||||||
if (target.hp <= 0) {
|
if (target.hp <= 0) {
|
||||||
target.alive = false;
|
target.alive = false;
|
||||||
|
killed = true;
|
||||||
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
|
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
|
||||||
}
|
}
|
||||||
|
return { killed, dealt };
|
||||||
|
};
|
||||||
|
const resolveAttackRound = () => {
|
||||||
|
let roundKilled = false;
|
||||||
|
let roundDamage = 0;
|
||||||
|
if (useAoe === true) {
|
||||||
|
for (const m2 of aliveList()) {
|
||||||
|
const r2 = dealToTarget(m2, perHit);
|
||||||
|
roundDamage += r2.dealt;
|
||||||
|
if (r2.killed) roundKilled = true;
|
||||||
}
|
}
|
||||||
|
} else if (c.randomTargetEachHit === true) {
|
||||||
|
for (let h = 0; h < hitN; h++) {
|
||||||
|
const target = randomAliveMonster();
|
||||||
|
if (!target) break;
|
||||||
|
const r = dealToTarget(target, perHit);
|
||||||
|
roundDamage += r.dealt;
|
||||||
|
if (r.killed) roundKilled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const preview = perHit;
|
||||||
|
const target = chooseTarget(aliveList(), preview);
|
||||||
|
if (target) {
|
||||||
|
if (c.weak) applyMonsterWeak(target, c.weak);
|
||||||
|
if (c.vuln) applyMonsterVuln(target, c.vuln);
|
||||||
|
const totalNv = perHit * hitN;
|
||||||
|
const r = dealToTarget(target, totalNv);
|
||||||
|
roundDamage += r.dealt;
|
||||||
|
if (r.killed) roundKilled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dmg += roundDamage;
|
||||||
|
damageDealtThisTurn += roundDamage;
|
||||||
|
return roundKilled;
|
||||||
|
};
|
||||||
|
let roundKilled = false;
|
||||||
|
do {
|
||||||
|
roundKilled = resolveAttackRound();
|
||||||
|
} while (c.repeatOnKill === true && roundKilled === true && countAliveMonsters() > 0);
|
||||||
}
|
}
|
||||||
if (c.block) blockGained = addBlock(c.block);
|
if (c.block) blockGained = addBlock(c.block);
|
||||||
} else if (c.kind === 'Power') {
|
} else if (c.kind === 'Power') {
|
||||||
@@ -371,17 +491,28 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
} else {
|
} else {
|
||||||
if (c.block) blockGained = addBlock(c.block);
|
if (c.block) blockGained = addBlock(c.block);
|
||||||
const weakAmount = (c.weak || 0) + (c.xWeakPerEnergy || 0) * xEnergy;
|
const weakAmount = (c.weak || 0) + (c.xWeakPerEnergy || 0) * xEnergy;
|
||||||
if ((weakAmount || c.vuln || c.poison) && alive.length) {
|
const vulnAmount = c.vuln || 0;
|
||||||
const target = chooseTarget(alive, 0);
|
if ((weakAmount || vulnAmount || c.poison || c.removeEnemyBlock || c.removeEnemyArtifact || c.enemyStrengthLossThisTurn) && alive.length) {
|
||||||
if (weakAmount) target.weak += weakAmount;
|
const targets = c.affectsAllEnemies === true ? aliveList() : [chooseTarget(alive, 0)];
|
||||||
if (c.vuln) target.vuln += c.vuln;
|
if (c.enemyStrengthLossThisTurn && c.enemyStrengthLossThisTurn > 0) {
|
||||||
|
enemyStrengthLossThisTurn += c.enemyStrengthLossThisTurn;
|
||||||
|
}
|
||||||
|
for (const target of targets) {
|
||||||
|
if (!target || !target.alive) continue;
|
||||||
|
if (c.removeEnemyBlock === true) removeEnemyBlock(target);
|
||||||
|
if (c.removeEnemyArtifact === true) removeEnemyArtifact(target);
|
||||||
|
if (weakAmount) applyMonsterWeak(target, weakAmount);
|
||||||
|
if (vulnAmount) applyMonsterVuln(target, vulnAmount);
|
||||||
if (c.poison) {
|
if (c.poison) {
|
||||||
|
if (c.poisonIfTargetPoisoned !== true || target.poison > 0) {
|
||||||
const poisonHits = c.poisonHits || 1;
|
const poisonHits = c.poisonHits || 1;
|
||||||
for (let i = 0; i < poisonHits; i++) {
|
for (let i = 0; i < poisonHits; i++) {
|
||||||
const target2 = c.poisonRandomTargets === true
|
const target2 = c.poisonRandomTargets === true
|
||||||
? alive[Math.floor(rng() * alive.length)]
|
? alive[Math.floor(rng() * alive.length)]
|
||||||
: target;
|
: target;
|
||||||
if (target2) target2.poison += c.poison;
|
if (target2) applyPoisonToMonster(target2, c.poison);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (c.class === 'shiv' && !shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) {
|
if (c.class === 'shiv' && !shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) {
|
||||||
@@ -398,6 +529,7 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
activeKillReward = c.rewardOnKill || 0;
|
activeKillReward = c.rewardOnKill || 0;
|
||||||
if (c.intangible) pIntangible += c.intangible;
|
if (c.intangible) pIntangible += c.intangible;
|
||||||
queueNextTurnEffects(c);
|
queueNextTurnEffects(c);
|
||||||
|
turnCardsPlayedThisTurn++;
|
||||||
let drawnCards = [];
|
let drawnCards = [];
|
||||||
if (c.draw) drawnCards = drawnCards.concat(draw(c.draw));
|
if (c.draw) drawnCards = drawnCards.concat(draw(c.draw));
|
||||||
if (c.drawUntilHandSize) {
|
if (c.drawUntilHandSize) {
|
||||||
@@ -415,6 +547,7 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (target && target.alive) {
|
if (target && target.alive) {
|
||||||
target.hp -= c.cardPlayedDamage;
|
target.hp -= c.cardPlayedDamage;
|
||||||
dmg += c.cardPlayedDamage;
|
dmg += c.cardPlayedDamage;
|
||||||
|
damageDealtThisTurn += c.cardPlayedDamage;
|
||||||
if (target.hp <= 0) target.alive = false;
|
if (target.hp <= 0) target.alive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,15 +558,20 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.hp -= c.cardPlayedRandomDamage;
|
target.hp -= c.cardPlayedRandomDamage;
|
||||||
dmg += c.cardPlayedRandomDamage;
|
dmg += c.cardPlayedRandomDamage;
|
||||||
|
damageDealtThisTurn += c.cardPlayedRandomDamage;
|
||||||
if (target.hp <= 0) target.alive = false;
|
if (target.hp <= 0) target.alive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (c.blockPerDamageDealtThisTurn && c.blockPerDamageDealtThisTurn > 0 && c.kind !== 'Power') {
|
||||||
|
blockGained += Math.max(0, damageDealtThisTurn * c.blockPerDamageDealtThisTurn);
|
||||||
|
}
|
||||||
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) {
|
||||||
const c = cards[id];
|
const c = cards[id];
|
||||||
if (!c?.sly) return;
|
if (!c) return;
|
||||||
|
if (!c.sly && !skillSlyOnPlayCards.has(id) && !turnSkillSlyCards.has(id)) return;
|
||||||
resolveCardEffects(id, c, 0, false);
|
resolveCardEffects(id, c, 0, false);
|
||||||
}
|
}
|
||||||
function discardHandCard(idx, trigger = true) {
|
function discardHandCard(idx, trigger = true) {
|
||||||
@@ -464,6 +602,8 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
drawDamageThisTurn = 0;
|
drawDamageThisTurn = 0;
|
||||||
drawPoisonThisTurn = 0;
|
drawPoisonThisTurn = 0;
|
||||||
shivAoeThisCombat = false;
|
shivAoeThisCombat = false;
|
||||||
|
turnSkillSlyCards.clear();
|
||||||
|
enemyStrengthLossThisTurn = 0;
|
||||||
blockGainMultiplier = 1;
|
blockGainMultiplier = 1;
|
||||||
handCostZeroThisTurn = false;
|
handCostZeroThisTurn = false;
|
||||||
drawDisabledThisTurn = false;
|
drawDisabledThisTurn = false;
|
||||||
@@ -483,7 +623,7 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
|
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
|
||||||
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
|
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
|
||||||
else if (pc.powerEffect === 'poisonPerTurn') {
|
else if (pc.powerEffect === 'poisonPerTurn') {
|
||||||
for (const m of mob) if (m.alive) m.poison += pc.value;
|
for (const m of mob) if (m.alive) applyPoisonToMonster(m, pc.value);
|
||||||
} else if (pc.powerEffect === 'damagePerTurn') {
|
} else if (pc.powerEffect === 'damagePerTurn') {
|
||||||
for (const m of mob) {
|
for (const m of mob) {
|
||||||
if (!m.alive) continue;
|
if (!m.alive) continue;
|
||||||
@@ -509,60 +649,25 @@ 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, nextSkillCostZero, skillCostReductionThisTurn, handCostZeroThisTurn });
|
const idx = chooseAction(hand, cards, energy, { drawPileCount: drawPile.length, nextSkillCostZero, skillCostReductionThisTurn, handCostZeroThisTurn, combatCardCostReduction });
|
||||||
if (idx < 0) break;
|
if (idx < 0) break;
|
||||||
const id = hand[idx], c = cards[id];
|
const id = hand[idx], c = cards[id];
|
||||||
|
let dmg = 0;
|
||||||
const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
|
const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
|
||||||
const skillRepeat = c.kind === 'Skill' ? nextSkillRepeatCount : 0;
|
const skillRepeat = c.kind === 'Skill' ? nextSkillRepeatCount : 0;
|
||||||
const baseCost = c.cost || 0;
|
const baseCost = c.cost || 0;
|
||||||
|
const combatReduction = combatCardCostReduction[id] || 0;
|
||||||
const cost = handCostZeroThisTurn === true ? 0 : (c.useAllEnergy === true ? energy : (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;
|
const finalCost = Math.max(0, cost - combatReduction);
|
||||||
resolveCardEffects(id, c, cost);
|
energy -= finalCost;
|
||||||
|
resolveCardEffects(id, c, finalCost);
|
||||||
const playedBlock = powerFieldTotal('cardPlayedBlock');
|
const playedBlock = powerFieldTotal('cardPlayedBlock');
|
||||||
if (playedBlock > 0) addBlock(playedBlock);
|
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) {
|
if (skillRepeat > 0) {
|
||||||
nextSkillRepeatCount = Math.max(0, nextSkillRepeatCount - skillRepeat);
|
nextSkillRepeatCount = Math.max(0, nextSkillRepeatCount - skillRepeat);
|
||||||
for (let r = 0; r < skillRepeat; r++) {
|
for (let r = 0; r < skillRepeat; r++) {
|
||||||
resolveCardEffects(id, c, cost);
|
resolveCardEffects(id, c, finalCost);
|
||||||
if (playedBlock > 0) addBlock(playedBlock);
|
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 (c.kind === 'Attack') turnAttackCardsPlayed++;
|
||||||
@@ -571,6 +676,9 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
queueSelectedReserve(c);
|
queueSelectedReserve(c);
|
||||||
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
|
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
|
||||||
else if (c.kind !== 'Power') discard.push(id);
|
else if (c.kind !== 'Power') discard.push(id);
|
||||||
|
if (c.combatCostReductionOnPlay && c.combatCostReductionOnPlay > 0) {
|
||||||
|
combatCardCostReduction[id] = (combatCardCostReduction[id] || 0) + c.combatCostReductionOnPlay;
|
||||||
|
}
|
||||||
applyDiscardEffects(c);
|
applyDiscardEffects(c);
|
||||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
|
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
|
||||||
}
|
}
|
||||||
@@ -600,17 +708,20 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
for (const m of mob) {
|
for (const m of mob) {
|
||||||
if (!m.alive) continue;
|
if (!m.alive) continue;
|
||||||
// 독 틱 — 행동 시작 시 (Lua EnemyActStep 동기화). 사망 시 행동 생략
|
// 독 틱 — 행동 시작 시 (Lua EnemyActStep 동기화). 사망 시 행동 생략
|
||||||
if (m.poison > 0) {
|
const poisonTicks = 1 + Math.max(0, powerFieldTotal('extraPoisonTicks'));
|
||||||
|
for (let tick = 0; tick < poisonTicks; tick++) {
|
||||||
|
if (m.poison <= 0) break;
|
||||||
m.hp -= m.poison;
|
m.hp -= m.poison;
|
||||||
m.poison--;
|
m.poison--;
|
||||||
if (m.hp <= 0) { m.hp = 0; m.alive = false; continue; }
|
if (m.hp <= 0) { m.hp = 0; m.alive = false; break; }
|
||||||
}
|
}
|
||||||
|
if (!m.alive) continue;
|
||||||
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
|
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
|
||||||
// 정의된 intent 중 랜덤 선택 (Lua EnemyActStep 동기화 — 순차→랜덤)
|
// 정의된 intent 중 랜덤 선택 (Lua EnemyActStep 동기화 — 순차→랜덤)
|
||||||
const it = m.intents.length ? m.intents[Math.floor(rng() * m.intents.length)] : null;
|
const it = m.intents.length ? m.intents[Math.floor(rng() * m.intents.length)] : null;
|
||||||
if (it) {
|
if (it) {
|
||||||
if (it.kind === 'Attack') {
|
if (it.kind === 'Attack') {
|
||||||
const atk = calcAttack(it.value, m.str, m.weak, pVuln);
|
const atk = calcAttack(it.value, Math.max(0, m.str - enemyStrengthLossThisTurn), m.weak, pVuln);
|
||||||
const beforeHp = pHp;
|
const beforeHp = pHp;
|
||||||
let incoming = atk;
|
let incoming = atk;
|
||||||
if (pIntangible > 0 && incoming > 1) incoming = 1;
|
if (pIntangible > 0 && incoming > 1) incoming = 1;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, rarityForRoll,
|
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, calcEnemyAttack, rarityForRoll,
|
||||||
} from './sim-balance.mjs';
|
} from './sim-balance.mjs';
|
||||||
|
|
||||||
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
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);
|
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", () => {
|
test("simulateCombat: drawSkillBlock grants block for each drawn skill", () => {
|
||||||
const data = {
|
const data = {
|
||||||
cards: {
|
cards: {
|
||||||
@@ -821,10 +829,181 @@ test("simulateCombat: attackPoison power applies poison on attack damage", () =>
|
|||||||
assert.equal(r.turns, 1);
|
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 = {
|
const data = {
|
||||||
cards: {
|
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"],
|
starterDeck: ["Strangle"],
|
||||||
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
|
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);
|
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", () => {
|
test("simulateCombat: cardPlayedRandomDamage hits a random enemy on card play", () => {
|
||||||
const data = {
|
const data = {
|
||||||
cards: {
|
cards: {
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ end
|
|||||||
if c.kind == "Skill" and self.SkillCostReductionThisTurn ~= nil and self.SkillCostReductionThisTurn > 0 then
|
if c.kind == "Skill" and self.SkillCostReductionThisTurn ~= nil and self.SkillCostReductionThisTurn > 0 then
|
||||||
cost = math.max(0, cost - self.SkillCostReductionThisTurn)
|
cost = math.max(0, cost - self.SkillCostReductionThisTurn)
|
||||||
end
|
end
|
||||||
|
if self.CombatCardCostReduction ~= nil and self.CombatCardCostReduction[cardId] ~= nil then
|
||||||
|
cost = math.max(0, cost - self.CombatCardCostReduction[cardId])
|
||||||
|
end
|
||||||
if c.kind == "Skill" and self.NextSkillRepeatCount ~= nil and self.NextSkillRepeatCount > 0 then
|
if c.kind == "Skill" and self.NextSkillRepeatCount ~= nil and self.NextSkillRepeatCount > 0 then
|
||||||
skillRepeat = self.NextSkillRepeatCount
|
skillRepeat = self.NextSkillRepeatCount
|
||||||
end
|
end
|
||||||
@@ -103,6 +106,12 @@ end
|
|||||||
if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then
|
if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then
|
||||||
self.ActiveKillReward = 0
|
self.ActiveKillReward = 0
|
||||||
end
|
end
|
||||||
|
if c.combatCostReductionOnPlay ~= nil and c.combatCostReductionOnPlay > 0 then
|
||||||
|
if self.CombatCardCostReduction == nil then
|
||||||
|
self.CombatCardCostReduction = {}
|
||||||
|
end
|
||||||
|
self.CombatCardCostReduction[cardId] = (self.CombatCardCostReduction[cardId] or 0) + c.combatCostReductionOnPlay
|
||||||
|
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
|
||||||
@@ -283,7 +292,7 @@ m.hp = m.hp - dmg
|
|||||||
if dmg > 0 then
|
if dmg > 0 then
|
||||||
local poison = self:AddPowerFieldTotal("attackPoison")
|
local poison = self:AddPowerFieldTotal("attackPoison")
|
||||||
if poison ~= nil and poison > 0 then
|
if poison ~= nil and poison > 0 then
|
||||||
m.poison = (m.poison or 0) + poison
|
self:ApplyPoisonToMonster(m, poison)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:MonsterHitMotion(m.slot)
|
self:MonsterHitMotion(m.slot)
|
||||||
@@ -345,6 +354,62 @@ end
|
|||||||
return killed`, [
|
return killed`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
], 0, 'boolean'),
|
], 0, 'boolean'),
|
||||||
|
method('ApplyPoisonToMonster', `if target == nil or target.alive ~= true or amount == nil or amount <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if target.artifact ~= nil and target.artifact > 0 then
|
||||||
|
target.artifact = target.artifact - 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
target.poison = (target.poison or 0) + amount
|
||||||
|
self.PoisonApplicationsThisCombat = (self.PoisonApplicationsThisCombat or 0) + 1
|
||||||
|
local burstEvery = self:AddPowerFieldTotal("poisonApplicationBurstEvery")
|
||||||
|
local burstDamage = self:AddPowerFieldTotal("poisonApplicationBurstDamage")
|
||||||
|
if burstEvery ~= nil and burstEvery > 0 and burstDamage ~= nil and burstDamage > 0 then
|
||||||
|
if (self.PoisonApplicationsThisCombat % burstEvery) == 0 then
|
||||||
|
self:DealDamageToAllMonsters(burstDamage)
|
||||||
|
end
|
||||||
|
end`, [
|
||||||
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'target' },
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
|
]),
|
||||||
|
method('DealDamageToAllMonsters', `if self.Monsters == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local killCount = 0
|
||||||
|
for i = 1, #self.Monsters do
|
||||||
|
local m = self.Monsters[i]
|
||||||
|
if m ~= nil and m.alive == true then
|
||||||
|
local dmg = amount
|
||||||
|
if m.vuln > 0 then
|
||||||
|
dmg = math.floor(dmg * 1.5)
|
||||||
|
end
|
||||||
|
if m.block > 0 then
|
||||||
|
local absorbed = math.min(m.block, dmg)
|
||||||
|
m.block = m.block - absorbed
|
||||||
|
dmg = dmg - absorbed
|
||||||
|
end
|
||||||
|
m.hp = m.hp - dmg
|
||||||
|
if dmg > 0 then
|
||||||
|
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + dmg
|
||||||
|
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:RenderCombat()
|
||||||
|
self:CheckCombatEnd()
|
||||||
|
return killCount > 0`, [
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
|
], 0, 'boolean'),
|
||||||
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)
|
||||||
@@ -426,7 +491,7 @@ _TimerService:SetTimerOnce(function()
|
|||||||
if dmg > 0 then
|
if dmg > 0 then
|
||||||
local poison = self:AddPowerFieldTotal("attackPoison")
|
local poison = self:AddPowerFieldTotal("attackPoison")
|
||||||
if poison ~= nil and poison > 0 then
|
if poison ~= nil and poison > 0 then
|
||||||
m.poison = (m.poison or 0) + poison
|
self:ApplyPoisonToMonster(m, poison)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:ShowDmgPop(i, dmg)
|
self:ShowDmgPop(i, dmg)
|
||||||
@@ -518,6 +583,12 @@ local m = self.Monsters[idx]
|
|||||||
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(idx)
|
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(idx)
|
||||||
self:SetEntityEnabled(base .. "/ActFrame", true)
|
self:SetEntityEnabled(base .. "/ActFrame", true)
|
||||||
_TimerService:SetTimerOnce(function()
|
_TimerService:SetTimerOnce(function()
|
||||||
|
local poisonTicks = 1
|
||||||
|
local bonusTicks = self:AddPowerFieldTotal("extraPoisonTicks")
|
||||||
|
if bonusTicks ~= nil and bonusTicks > 0 then
|
||||||
|
poisonTicks = poisonTicks + bonusTicks
|
||||||
|
end
|
||||||
|
for pt = 1, poisonTicks do
|
||||||
if m.poison ~= nil and m.poison > 0 then
|
if m.poison ~= nil and m.poison > 0 then
|
||||||
m.hp = m.hp - m.poison
|
m.hp = m.hp - m.poison
|
||||||
self:ShowDmgPop(idx, m.poison)
|
self:ShowDmgPop(idx, m.poison)
|
||||||
@@ -532,12 +603,17 @@ _TimerService:SetTimerOnce(function()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
m.block = 0
|
m.block = 0
|
||||||
local intent = m.intents[m.intentIdx]
|
local intent = m.intents[m.intentIdx]
|
||||||
if intent ~= nil then
|
if intent ~= nil then
|
||||||
if intent.kind == "Attack" then
|
if intent.kind == "Attack" then
|
||||||
self:MonsterLunge(idx)
|
self:MonsterLunge(idx)
|
||||||
local atk = intent.value + m.str
|
local atk = intent.value + m.str
|
||||||
|
if self.EnemyStrengthLossThisTurn ~= nil and self.EnemyStrengthLossThisTurn > 0 then
|
||||||
|
atk = atk - self.EnemyStrengthLossThisTurn
|
||||||
|
if atk < 0 then atk = 0 end
|
||||||
|
end
|
||||||
if m.weak > 0 then
|
if m.weak > 0 then
|
||||||
atk = math.floor(atk * 0.75)
|
atk = math.floor(atk * 0.75)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -228,6 +228,8 @@ self.RetainSelectActive = false
|
|||||||
self.ReserveSelectActive = false
|
self.ReserveSelectActive = false
|
||||||
self.TurnAttackCardsPlayed = 0
|
self.TurnAttackCardsPlayed = 0
|
||||||
self.TurnDiscardedCards = 0
|
self.TurnDiscardedCards = 0
|
||||||
|
self.TurnCardsPlayedThisTurn = 0
|
||||||
|
self.DamageDealtThisTurn = 0
|
||||||
self.NextTurnSelectCopies = 0
|
self.NextTurnSelectCopies = 0
|
||||||
self.NextTurnSelectPrompt = ""
|
self.NextTurnSelectPrompt = ""
|
||||||
self.SkillCostReductionThisTurn = 0
|
self.SkillCostReductionThisTurn = 0
|
||||||
@@ -252,6 +254,9 @@ self.ActiveAttackDamageVsWeakMultiplier = 1
|
|||||||
self.DrawDamageThisTurn = 0
|
self.DrawDamageThisTurn = 0
|
||||||
self.DrawPoisonThisTurn = 0
|
self.DrawPoisonThisTurn = 0
|
||||||
self.ShivAoeThisCombat = false
|
self.ShivAoeThisCombat = false
|
||||||
|
self.SkillSlyOnPlayCards = self.SkillSlyOnPlayCards or {}
|
||||||
|
self.TurnSkillSlyCards = {}
|
||||||
|
self.EnemyStrengthLossThisTurn = 0
|
||||||
self.HandCostZeroThisTurn = false
|
self.HandCostZeroThisTurn = false
|
||||||
self.DrawDisabledThisTurn = false
|
self.DrawDisabledThisTurn = false
|
||||||
local powerTurnDraw = 0
|
local powerTurnDraw = 0
|
||||||
@@ -271,7 +276,7 @@ if self.PlayerPowers ~= nil then
|
|||||||
for j = 1, #self.Monsters do
|
for j = 1, #self.Monsters do
|
||||||
local tm = self.Monsters[j]
|
local tm = self.Monsters[j]
|
||||||
if tm ~= nil and tm.alive == true then
|
if tm ~= nil and tm.alive == true then
|
||||||
tm.poison = (tm.poison or 0) + pc.value
|
self:ApplyPoisonToMonster(tm, pc.value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -448,6 +453,7 @@ 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)
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
|
self.TurnSkillSlyCards = {}
|
||||||
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 drawnCards = {}
|
||||||
|
|||||||
@@ -311,6 +311,9 @@ end
|
|||||||
if c.damagePerCardDrawnThisCombat ~= nil then
|
if c.damagePerCardDrawnThisCombat ~= nil then
|
||||||
base2 = base2 + (self.CardsDrawnThisCombat or 0) * c.damagePerCardDrawnThisCombat
|
base2 = base2 + (self.CardsDrawnThisCombat or 0) * c.damagePerCardDrawnThisCombat
|
||||||
end
|
end
|
||||||
|
if c.class == "Attack" and (self.TurnCardsPlayedThisTurn or 0) == 0 and c.firstCardDamageBonus ~= nil then
|
||||||
|
base2 = base2 + c.firstCardDamageBonus
|
||||||
|
end
|
||||||
if c.class == "shiv" then
|
if c.class == "shiv" then
|
||||||
if self:HasPowerField("shivDamageBonus") == true then
|
if self:HasPowerField("shivDamageBonus") == true then
|
||||||
base2 = base2 + self:AddPowerFieldTotal("shivDamageBonus")
|
base2 = base2 + self:AddPowerFieldTotal("shivDamageBonus")
|
||||||
@@ -411,6 +414,33 @@ end
|
|||||||
if c.shivAoe == true and c.kind ~= "Power" then
|
if c.shivAoe == true and c.kind ~= "Power" then
|
||||||
self.ShivAoeThisCombat = true
|
self.ShivAoeThisCombat = true
|
||||||
end
|
end
|
||||||
|
if c.skillSlyOnPlay == true and c.kind == "Skill" then
|
||||||
|
if self.SkillSlyOnPlayCards == nil then
|
||||||
|
self.SkillSlyOnPlayCards = {}
|
||||||
|
end
|
||||||
|
self.SkillSlyOnPlayCards[cardId] = true
|
||||||
|
end
|
||||||
|
if c.turnHandSlyCount ~= nil and c.turnHandSlyCount > 0 then
|
||||||
|
if self.TurnSkillSlyCards == nil then
|
||||||
|
self.TurnSkillSlyCards = {}
|
||||||
|
end
|
||||||
|
local picked = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
for i = 1, #self.Hand do
|
||||||
|
local hid = self.Hand[i]
|
||||||
|
if hid ~= nil and hid ~= cardId then
|
||||||
|
local hc = self.Cards[hid]
|
||||||
|
if hc ~= nil and hc.kind == "Skill" and self.TurnSkillSlyCards[hid] ~= true and self.SkillSlyOnPlayCards[hid] ~= true and hc.sly ~= true then
|
||||||
|
self.TurnSkillSlyCards[hid] = true
|
||||||
|
picked = picked + 1
|
||||||
|
if picked >= c.turnHandSlyCount then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
local xEnergy = energySpent or 0
|
local xEnergy = energySpent or 0
|
||||||
local weakAmount = c.weak or 0
|
local weakAmount = c.weak or 0
|
||||||
local vulnAmount = c.vuln or 0
|
local vulnAmount = c.vuln or 0
|
||||||
@@ -448,12 +478,61 @@ if c.kind == "Attack" then
|
|||||||
if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
|
if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
|
||||||
self.ShivFirstDamageBonusUsed = true
|
self.ShivFirstDamageBonusUsed = true
|
||||||
end
|
end
|
||||||
if useAoe == true then
|
local function countAliveMonsters()
|
||||||
self:PlayAoeFx(c.fx or c.image, total)
|
local n = 0
|
||||||
else
|
if self.Monsters ~= nil then
|
||||||
self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true)
|
for mi = 1, #self.Monsters do
|
||||||
|
local om = self.Monsters[mi]
|
||||||
|
if om ~= nil and om.alive == true then n = n + 1 end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
local function randomAliveMonsterIndex()
|
||||||
|
local alive = {}
|
||||||
|
if self.Monsters ~= nil then
|
||||||
|
for mi = 1, #self.Monsters do
|
||||||
|
local om = self.Monsters[mi]
|
||||||
|
if om ~= nil and om.alive == true then
|
||||||
|
table.insert(alive, mi)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #alive <= 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
return alive[math.random(1, #alive)]
|
||||||
|
end
|
||||||
|
local function resolveAttackRound()
|
||||||
|
local roundKilled = false
|
||||||
|
if useAoe == true then
|
||||||
|
local killed = self:DealDamageToAllMonsters(total)
|
||||||
|
if killed == true then roundKilled = true end
|
||||||
|
elseif c.randomTargetEachHit == true then
|
||||||
|
for h = 1, hitN do
|
||||||
|
local targetIdx = randomAliveMonsterIndex()
|
||||||
|
if targetIdx ~= nil and targetIdx > 0 then
|
||||||
|
local prev = self.TargetIndex
|
||||||
|
self.TargetIndex = targetIdx
|
||||||
|
local killed = self:DealDamageToTarget(total / hitN, c.pierce == true)
|
||||||
|
self.TargetIndex = prev
|
||||||
|
if killed == true then roundKilled = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local killed = self:DealDamageToTarget(total, c.pierce == true)
|
||||||
|
if killed == true then roundKilled = true end
|
||||||
|
end
|
||||||
|
return roundKilled
|
||||||
|
end
|
||||||
|
local totalDamage = 0
|
||||||
|
local roundKilled = false
|
||||||
|
repeat
|
||||||
|
roundKilled = resolveAttackRound()
|
||||||
|
totalDamage = totalDamage + total
|
||||||
|
until c.repeatOnKill ~= true or roundKilled ~= true or countAliveMonsters() <= 0
|
||||||
|
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + totalDamage
|
||||||
|
end
|
||||||
if c.block ~= nil then
|
if c.block ~= nil then
|
||||||
self:AddCardBlock(c.block)
|
self:AddCardBlock(c.block)
|
||||||
end
|
end
|
||||||
@@ -490,20 +569,59 @@ end
|
|||||||
if c.intangible ~= nil and c.intangible > 0 then
|
if c.intangible ~= nil and c.intangible > 0 then
|
||||||
self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible
|
self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible
|
||||||
end
|
end
|
||||||
|
self.TurnCardsPlayedThisTurn = (self.TurnCardsPlayedThisTurn or 0) + 1
|
||||||
|
if c.blockPerDamageDealtThisTurn ~= nil and c.blockPerDamageDealtThisTurn > 0 then
|
||||||
|
self:AddCardBlock((self.DamageDealtThisTurn or 0) * c.blockPerDamageDealtThisTurn)
|
||||||
|
end
|
||||||
self:QueueNextTurnEffects(c)
|
self:QueueNextTurnEffects(c)
|
||||||
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil or c.xWeakPerEnergy ~= nil then
|
if c.combatCostReductionOnPlay ~= nil and c.combatCostReductionOnPlay > 0 then
|
||||||
|
if self.CombatCardCostReduction == nil then
|
||||||
|
self.CombatCardCostReduction = {}
|
||||||
|
end
|
||||||
|
self.CombatCardCostReduction[cardId] = (self.CombatCardCostReduction[cardId] or 0) + c.combatCostReductionOnPlay
|
||||||
|
end
|
||||||
|
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil or c.xWeakPerEnergy ~= nil or c.affectsAllEnemies == true or c.removeEnemyBlock == true or c.removeEnemyArtifact == true or (c.enemyStrengthLossThisTurn ~= nil and c.enemyStrengthLossThisTurn > 0) 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
|
||||||
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
|
if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if tm ~= nil and tm.alive == true then
|
local targets = {}
|
||||||
if weakAmount ~= nil and weakAmount > 0 then tm.weak = tm.weak + weakAmount end
|
if c.affectsAllEnemies == true and self.Monsters ~= nil then
|
||||||
|
for mi = 1, #self.Monsters do
|
||||||
|
local om = self.Monsters[mi]
|
||||||
|
if om ~= nil and om.alive == true then
|
||||||
|
table.insert(targets, om)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif tm ~= nil and tm.alive == true then
|
||||||
|
table.insert(targets, tm)
|
||||||
|
end
|
||||||
|
if c.enemyStrengthLossThisTurn ~= nil and c.enemyStrengthLossThisTurn > 0 then
|
||||||
|
self.EnemyStrengthLossThisTurn = (self.EnemyStrengthLossThisTurn or 0) + c.enemyStrengthLossThisTurn
|
||||||
|
end
|
||||||
|
for ti = 1, #targets do
|
||||||
|
local target = targets[ti]
|
||||||
|
if target ~= nil and target.alive == true then
|
||||||
|
if c.removeEnemyBlock == true then
|
||||||
|
target.block = 0
|
||||||
|
end
|
||||||
|
if c.removeEnemyArtifact == true then
|
||||||
|
target.artifact = 0
|
||||||
|
end
|
||||||
|
if weakAmount ~= nil and weakAmount > 0 then
|
||||||
|
if target.artifact ~= nil and target.artifact > 0 then
|
||||||
|
target.artifact = target.artifact - 1
|
||||||
|
else
|
||||||
|
target.weak = target.weak + weakAmount
|
||||||
|
end
|
||||||
|
end
|
||||||
if poisonAmount ~= nil and poisonAmount > 0 then
|
if poisonAmount ~= nil and poisonAmount > 0 then
|
||||||
|
if c.poisonIfTargetPoisoned ~= true or (target.poison ~= nil and target.poison > 0) then
|
||||||
local poisonHits = c.poisonHits or 1
|
local poisonHits = c.poisonHits or 1
|
||||||
for pi = 1, poisonHits do
|
for pi = 1, poisonHits do
|
||||||
local target = tm
|
local target2 = target
|
||||||
if c.poisonRandomTargets == true and self.Monsters ~= nil then
|
if c.poisonRandomTargets == true and self.Monsters ~= nil then
|
||||||
local alive = {}
|
local alive = {}
|
||||||
for mi = 1, #self.Monsters do
|
for mi = 1, #self.Monsters do
|
||||||
@@ -513,18 +631,24 @@ if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil or c.xWeakPerEnergy ~= nil
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #alive > 0 then
|
if #alive > 0 then
|
||||||
target = alive[math.random(#alive)]
|
target2 = alive[math.random(#alive)]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if target ~= nil and target.alive == true then
|
if target2 ~= nil and target2.alive == true then
|
||||||
target.poison = (target.poison or 0) + poisonAmount
|
self:ApplyPoisonToMonster(target2, poisonAmount)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if vulnAmount ~= nil and vulnAmount > 0 then
|
if vulnAmount ~= nil and vulnAmount > 0 then
|
||||||
tm.vuln = tm.vuln + vulnAmount
|
if target.artifact ~= nil and target.artifact > 0 then
|
||||||
|
target.artifact = target.artifact - 1
|
||||||
|
else
|
||||||
|
target.vuln = target.vuln + vulnAmount
|
||||||
if self:HasRelic("championBelt") then
|
if self:HasRelic("championBelt") then
|
||||||
tm.weak = tm.weak + 1
|
target.weak = target.weak + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -573,10 +697,11 @@ if (drawDamage ~= nil and drawDamage > 0) or (drawPoison ~= nil and drawPoison >
|
|||||||
dmg = dmg - absorbed
|
dmg = dmg - absorbed
|
||||||
end
|
end
|
||||||
if drawPoison ~= nil and drawPoison > 0 then
|
if drawPoison ~= nil and drawPoison > 0 then
|
||||||
m2.poison = (m2.poison or 0) + drawPoison
|
self:ApplyPoisonToMonster(m2, drawPoison)
|
||||||
end
|
end
|
||||||
if dmg > 0 then
|
if dmg > 0 then
|
||||||
m2.hp = m2.hp - dmg
|
m2.hp = m2.hp - dmg
|
||||||
|
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + dmg
|
||||||
end
|
end
|
||||||
self:ShowDmgPop(mi, dmg)
|
self:ShowDmgPop(mi, dmg)
|
||||||
self:MonsterHitMotion(mi)
|
self:MonsterHitMotion(mi)
|
||||||
@@ -599,9 +724,16 @@ end`, [
|
|||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'energySpent' },
|
{ 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 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if c.sly ~= true then
|
||||||
|
local onPlay = self.SkillSlyOnPlayCards ~= nil and self.SkillSlyOnPlayCards[cardId] == true
|
||||||
|
local tempSly = self.TurnSkillSlyCards ~= nil and self.TurnSkillSlyCards[cardId] == true
|
||||||
|
if onPlay ~= true and tempSly ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
self:Toast("교활 발동: " .. c.name)
|
self:Toast("교활 발동: " .. c.name)
|
||||||
self:ResolveCardEffects(cardId, 0, c, true, 0)`, [{ 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
|
||||||
|
|||||||
@@ -74,11 +74,16 @@ self.DrawDisabledThisTurn = false
|
|||||||
self.NextSkillCostZero = false
|
self.NextSkillCostZero = false
|
||||||
self.NextSkillRepeatCount = 0
|
self.NextSkillRepeatCount = 0
|
||||||
self.SkillCostReductionThisTurn = 0
|
self.SkillCostReductionThisTurn = 0
|
||||||
|
self.CombatCardCostReduction = {}
|
||||||
|
self.SkillSlyOnPlayCards = {}
|
||||||
|
self.TurnSkillSlyCards = {}
|
||||||
self.ShivFirstDamageBonusUsed = false
|
self.ShivFirstDamageBonusUsed = false
|
||||||
self.ActiveAttackDamageVsWeakMultiplier = 1
|
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||||
self.DrawDamageThisTurn = 0
|
self.DrawDamageThisTurn = 0
|
||||||
self.DrawPoisonThisTurn = 0
|
self.DrawPoisonThisTurn = 0
|
||||||
self.ShivAoeThisCombat = false
|
self.ShivAoeThisCombat = false
|
||||||
|
self.PoisonApplicationsThisCombat = 0
|
||||||
|
self.EnemyStrengthLossThisTurn = 0
|
||||||
self.PlayerStr = 0
|
self.PlayerStr = 0
|
||||||
self.PlayerDex = 0
|
self.PlayerDex = 0
|
||||||
self.PlayerThorns = 0
|
self.PlayerThorns = 0
|
||||||
@@ -91,6 +96,8 @@ self.PlayerPowers = {}
|
|||||||
self.FightAttackCount = 0
|
self.FightAttackCount = 0
|
||||||
self.TurnAttackCardsPlayed = 0
|
self.TurnAttackCardsPlayed = 0
|
||||||
self.TurnDiscardedCards = 0
|
self.TurnDiscardedCards = 0
|
||||||
|
self.TurnCardsPlayedThisTurn = 0
|
||||||
|
self.DamageDealtThisTurn = 0
|
||||||
self.DmgPopSeq = 0
|
self.DmgPopSeq = 0
|
||||||
self.FirstHpLossDone = false
|
self.FirstHpLossDone = false
|
||||||
self.ClayBlockNext = 0
|
self.ClayBlockNext = 0
|
||||||
@@ -233,7 +240,7 @@ for i = 1, n do
|
|||||||
local startIdx = 1
|
local startIdx = 1
|
||||||
if #intents > 0 then startIdx = math.random(1, #intents) end
|
if #intents > 0 then startIdx = math.random(1, #intents) end
|
||||||
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
||||||
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0,
|
hp = maxHp, maxHp = maxHp, block = 0, str = e.str or 0, weak = 0, vuln = 0, poison = 0, artifact = e.artifact or 0,
|
||||||
hitClip = hitClip, standClip = standClip, motionBusy = false,
|
hitClip = hitClip, standClip = standClip, motionBusy = false,
|
||||||
intents = intents, intentIdx = startIdx, alive = true, slot = i }
|
intents = intents, intentIdx = startIdx, alive = true, slot = i }
|
||||||
self:ReviveMonsterEntity(item.entity)
|
self:ReviveMonsterEntity(item.entity)
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ function luaCardsTable(cards) {
|
|||||||
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.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
|
||||||
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
|
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
|
||||||
|
if (c.firstCardDamageBonus != null) fields.push(`firstCardDamageBonus = ${c.firstCardDamageBonus}`);
|
||||||
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`);
|
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`);
|
||||||
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
|
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
|
||||||
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
|
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
|
||||||
@@ -175,6 +176,7 @@ function luaCardsTable(cards) {
|
|||||||
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.blockGainMultiplier != null) fields.push(`blockGainMultiplier = ${c.blockGainMultiplier}`);
|
||||||
|
if (c.blockPerDamageDealtThisTurn != null) fields.push(`blockPerDamageDealtThisTurn = ${c.blockPerDamageDealtThisTurn}`);
|
||||||
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}`);
|
||||||
@@ -215,6 +217,7 @@ function luaCardsTable(cards) {
|
|||||||
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${c.attackDamageVsWeakMultiplier}`);
|
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${c.attackDamageVsWeakMultiplier}`);
|
||||||
if (c.poisonHits != null) fields.push(`poisonHits = ${c.poisonHits}`);
|
if (c.poisonHits != null) fields.push(`poisonHits = ${c.poisonHits}`);
|
||||||
if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = true');
|
if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = true');
|
||||||
|
if (c.poisonIfTargetPoisoned === true) fields.push('poisonIfTargetPoisoned = true');
|
||||||
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${c.xDamagePerEnergy}`);
|
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${c.xDamagePerEnergy}`);
|
||||||
if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${c.xWeakPerEnergy}`);
|
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}`);
|
||||||
@@ -227,6 +230,18 @@ function luaCardsTable(cards) {
|
|||||||
if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${c.nextSkillRepeatCount}`);
|
if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${c.nextSkillRepeatCount}`);
|
||||||
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
|
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
|
||||||
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`);
|
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`);
|
||||||
|
if (c.skillSlyOnPlay === true) fields.push('skillSlyOnPlay = true');
|
||||||
|
if (c.turnHandSlyCount != null) fields.push(`turnHandSlyCount = ${c.turnHandSlyCount}`);
|
||||||
|
if (c.combatCostReductionOnPlay != null) fields.push(`combatCostReductionOnPlay = ${c.combatCostReductionOnPlay}`);
|
||||||
|
if (c.randomTargetEachHit === true) fields.push('randomTargetEachHit = true');
|
||||||
|
if (c.repeatOnKill === true) fields.push('repeatOnKill = true');
|
||||||
|
if (c.affectsAllEnemies === true) fields.push('affectsAllEnemies = true');
|
||||||
|
if (c.removeEnemyBlock === true) fields.push('removeEnemyBlock = true');
|
||||||
|
if (c.removeEnemyArtifact === true) fields.push('removeEnemyArtifact = true');
|
||||||
|
if (c.enemyStrengthLossThisTurn != null) fields.push(`enemyStrengthLossThisTurn = ${c.enemyStrengthLossThisTurn}`);
|
||||||
|
if (c.extraPoisonTicks != null) fields.push(`extraPoisonTicks = ${c.extraPoisonTicks}`);
|
||||||
|
if (c.poisonApplicationBurstEvery != null) fields.push(`poisonApplicationBurstEvery = ${c.poisonApplicationBurstEvery}`);
|
||||||
|
if (c.poisonApplicationBurstDamage != null) fields.push(`poisonApplicationBurstDamage = ${c.poisonApplicationBurstDamage}`);
|
||||||
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');
|
||||||
|
|||||||
Reference in New Issue
Block a user