Merge pull request '도적 카드 공통 효과 훅 정리' (#88) from codex/bandit-shared-hooks-pr into main
This commit was merged in pull request #88.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -900,7 +900,9 @@
|
||||
"class": "bandit",
|
||||
"rarity": "unique",
|
||||
"desc": "무작위 적에게 중독을 3만큼 3번 부여합니다.",
|
||||
"poison": 9,
|
||||
"poison": 3,
|
||||
"poisonHits": 3,
|
||||
"poisonRandomTargets": true,
|
||||
"image": "19361e72087946b1888684185b40d935"
|
||||
},
|
||||
"Reflex": {
|
||||
@@ -999,8 +1001,7 @@
|
||||
"class": "bandit",
|
||||
"rarity": "unique",
|
||||
"desc": "표창의 피해량이 4 증가합니다.",
|
||||
"powerEffect": "strengthPerTurn",
|
||||
"value": 1,
|
||||
"shivDamageBonus": 4,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"PhantomBlades": {
|
||||
@@ -1010,8 +1011,8 @@
|
||||
"class": "bandit",
|
||||
"rarity": "unique",
|
||||
"desc": "표창이 보존을 얻습니다. 매 턴마다 처음으로 사용하는 표창의 피해량이 9 증가합니다.",
|
||||
"powerEffect": "strengthPerTurn",
|
||||
"value": 1,
|
||||
"shivRetain": true,
|
||||
"firstShivDamageBonus": 9,
|
||||
"image": "0946f69d84464df29b24b94c744c868d"
|
||||
},
|
||||
"Speedster": {
|
||||
@@ -1022,8 +1023,7 @@
|
||||
"rarity": "unique",
|
||||
"desc": "내 턴 동안 카드를 뽑을 때마다, 모든 적에게 피해를 2 줍니다.",
|
||||
"aoe": true,
|
||||
"powerEffect": "damagePerTurn",
|
||||
"value": 2,
|
||||
"drawDamage": 2,
|
||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||
},
|
||||
"GrandFinale": {
|
||||
@@ -1144,7 +1144,7 @@
|
||||
"class": "bandit",
|
||||
"rarity": "legend",
|
||||
"desc": "이번 턴에 카드를 뽑을 때마다, 모든 적에게 중독을 2 부여합니다.",
|
||||
"poison": 2,
|
||||
"drawPoison": 2,
|
||||
"image": "19361e72087946b1888684185b40d935"
|
||||
},
|
||||
"BladeOfInk": {
|
||||
@@ -1261,8 +1261,7 @@
|
||||
"class": "bandit",
|
||||
"rarity": "legend",
|
||||
"desc": "약화 상태의 적이 공격 카드로 받는 피해가 2배가 됩니다.",
|
||||
"powerEffect": "strengthPerTurn",
|
||||
"value": 1,
|
||||
"attackDamageVsWeakMultiplier": 2,
|
||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||
},
|
||||
"FanOfKnives": {
|
||||
@@ -1272,9 +1271,8 @@
|
||||
"class": "bandit",
|
||||
"rarity": "legend",
|
||||
"desc": "표창이 이제 모든 적을 대상으로 합니다. 표창을 4장 손으로 가져옵니다.",
|
||||
"powerEffect": "strengthPerTurn",
|
||||
"value": 1,
|
||||
"addShiv": 4,
|
||||
"shivAoe": true,
|
||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||
},
|
||||
"SerpentForm": {
|
||||
|
||||
@@ -1,100 +1,79 @@
|
||||
# Bandit Card Audit
|
||||
|
||||
`bandit` 카드의 구현 상태를 카드별로 정리한 문서입니다.
|
||||
Current status of bandit cards and shared effect hooks.
|
||||
|
||||
상태 기준:
|
||||
## 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`
|
||||
|
||||
## 구현됨
|
||||
Shared hooks already in use:
|
||||
|
||||
`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`
|
||||
- `poison`, `innate`, `playableWhenDrawPileEmpty`
|
||||
- `retain`, `sly`, `discard`, `discardAll`, `addShiv`, `addShivPerDiscard`, `turnStartShiv`, `retainOne`
|
||||
- `turnStartDraw`, `turnStartDiscard`
|
||||
- `nextTurnBlock`, `nextTurnDraw`, `nextTurnKeepBlock`, `nextTurnAttackMultiplier`, `nextTurnCopies`, `nextTurnSelectHandCard`
|
||||
- `damagePerOtherHandCard`, `damagePerAttackPlayedThisTurn`, `damagePerDiscardedThisTurn`, `damagePerSkillInHand`, `otherHandAtLeast`, `bonusHitsWhenOtherHandAtLeast`
|
||||
- `gainEnergy`, `drawUntilHandSize`, `drawPerDiscarded`, `cardPlayedBlock`, `blockGainMultiplier`, `nextSkillCostZero`, `skillCostReductionThisTurn`
|
||||
- `drawDamage`, `drawPoison`, `shivDamageBonus`, `firstShivDamageBonus`, `shivRetain`, `shivAoe`, `attackDamageVsWeakMultiplier`, `poisonHits`, `poisonRandomTargets`
|
||||
|
||||
공용 메모:
|
||||
## Partial
|
||||
|
||||
- `poison`, `innate`, `playableWhenDrawPileEmpty` 구현됨
|
||||
- `retain`, `sly`, `discard`, `discardAll`, `addShiv`, `addShivPerDiscard`, `turnStartShiv`, `retainOne` 구현됨
|
||||
- `turnStartDraw`, `turnStartDiscard` 구현됨
|
||||
- `nextTurnBlock`, `nextTurnDraw`, `nextTurnKeepBlock`, `nextTurnAttackMultiplier`, `nextTurnCopies`, `nextTurnSelectHandCard` 구현됨
|
||||
- `damagePerOtherHandCard`, `damagePerAttackPlayedThisTurn`, `damagePerDiscardedThisTurn`, `damagePerSkillInHand`, `otherHandAtLeast`, `bonusHitsWhenOtherHandAtLeast` 구현됨
|
||||
- `gainEnergy`, `drawUntilHandSize`, `drawPerDiscarded`, `cardPlayedBlock`, `blockGainMultiplier`, `nextSkillCostZero`, `skillCostReductionThisTurn` 구현됨
|
||||
`Ricochet`: random split attacks need fuller targeting rules.
|
||||
|
||||
## 부분구현
|
||||
`Anticipate`: next-turn enemy intent preview plus block still needs exact behavior review.
|
||||
|
||||
`Ricochet`: 무작위 적 4회 타격이 아니라 일반 분산 공격으로만 처리됨
|
||||
`PiercingWail`: enemy attack reduction is implemented as a shared Weak/Vulnerable style effect, but needs final balance review.
|
||||
|
||||
`Anticipate`: 이번 턴 동안 민첩 2가 아니라 전투 전체 민첩 증가
|
||||
`Expose`: weak/vulnerable interaction is only partially modeled.
|
||||
|
||||
`PiercingWail`: 이번 턴 적 공격 감소가 아니라 공용 약화/취약 계열만 적용
|
||||
`BubbleBubble`: still missing the condition for poison dragon form ownership.
|
||||
|
||||
`Expose`: 방어도/인공물 제거는 없고 취약만 적용됨
|
||||
`Skewer`: X-cost attack.
|
||||
|
||||
`BubbleBubble`: 적이 독을 보유한 경우라는 조건이 아직 없음
|
||||
`Outbreak`: every 3 poison applications deal 11 to all enemies.
|
||||
|
||||
`BouncingFlask`: 무작위 적 3번 분산 대신 단일 독 9 처리
|
||||
`Strangle`: extra damage the first time a card is used.
|
||||
|
||||
## 미구현
|
||||
`EscapePlan`: gain block when discarded.
|
||||
|
||||
`Skewer`: X코스트 연타 공격
|
||||
`HandTrick`: one card becomes sly and grants block when discarded.
|
||||
|
||||
`Outbreak`: 독 3번 부여 시 전체 피해 트리거
|
||||
`Mirage`: gain block equal to total damage dealt this turn.
|
||||
|
||||
`Strangle`: 이번 턴 카드 사용마다 추가 피해
|
||||
`UpMySleeve`: create Shiv plus reduce discard cost.
|
||||
|
||||
`EscapePlan`: 드로우한 카드가 스킬이면 방어도 3
|
||||
`NoxiousFumes`: poison all enemies at turn start.
|
||||
|
||||
`HandTrick`: 손패의 스킬 카드 하나에 교활 부여
|
||||
`EchoingSlash`: repeat on kill.
|
||||
|
||||
`Mirage`: 모든 적의 독 총합만큼 방어 획득
|
||||
`TheHunt`: kill reward is implemented.
|
||||
|
||||
`UpMySleeve`: 표창 생성 + 비용 감소
|
||||
`Murder`: damage scales with cards drawn this combat.
|
||||
|
||||
`NoxiousFumes`: 턴 시작 전체 적 독 부여 파워
|
||||
`Malaise`: X-cost weak/vulnerable reduction.
|
||||
|
||||
`Accuracy`: 표창 피해 증가 파워
|
||||
`Pinpoint`: per-turn discard-based attack damage.
|
||||
|
||||
`PhantomBlades`: 표창 보존 + 첫 표창 강화
|
||||
`BladeOfInk`: turn-based Shiv creation.
|
||||
|
||||
`Speedster`: 드로우할 때마다 전체 피해
|
||||
`KnifeTrap`: playable only when draw pile is empty.
|
||||
|
||||
`EchoingSlash`: 처치 시 반복
|
||||
`BulletTime`: hand cost zero plus draw disabled.
|
||||
|
||||
`TheHunt`: 처치 조건 보상
|
||||
`Accelerant`: poison trigger timing is not finalized.
|
||||
|
||||
`Murder`: 이번 전투 동안 뽑은 카드 수 비례 피해
|
||||
`Envenom`: attack poison on hit.
|
||||
|
||||
`Malaise`: X코스트 약화/피해 감소
|
||||
`MasterPlanner`: skill discard interaction still needs a clear rules decision.
|
||||
|
||||
`Pinpoint`: 이번 턴 스킬 비용 감소
|
||||
`SerpentForm`: damage on card play.
|
||||
|
||||
`CorrosiveWave`: 드로우할 때마다 독
|
||||
`WraithForm`: intangible plus turn-end block loss.
|
||||
|
||||
`BladeOfInk`: 전용 표창 생성
|
||||
## Open questions
|
||||
|
||||
`KnifeTrap`: 소멸된 표창 전부 사용
|
||||
|
||||
`BulletTime`: 드로우 금지 + 손패 무료 사용
|
||||
|
||||
`Accelerant`: 추가 독 발동
|
||||
|
||||
`Envenom`: 공격 적중 시 독 부여
|
||||
|
||||
`MasterPlanner`: 스킬 사용 시 교활 부여
|
||||
|
||||
`Tracking`: 약화된 적이 공격 피해를 2배로 받음
|
||||
|
||||
`FanOfKnives`: 표창이 모든 적 대상
|
||||
|
||||
`SerpentForm`: 카드 사용할 때마다 무작위 적에게 피해
|
||||
|
||||
`WraithForm`: 불가침 2 + 턴 종료 시 민첩 감소
|
||||
|
||||
## 다음 축
|
||||
|
||||
- 조건부 피해
|
||||
- 카드 사용 트리거
|
||||
- 비용/X코스트
|
||||
- 드로우 연동 파워
|
||||
- `MasterPlanner`
|
||||
- `Accelerant`
|
||||
- `Outbreak`
|
||||
|
||||
These three still need a confirmed rule interpretation before we lock them in.
|
||||
|
||||
@@ -1,88 +1,102 @@
|
||||
# Card Effect Fields
|
||||
|
||||
`data/cards.json`의 카드 효과를 공용 데이터 필드로 표현하는 기준 문서입니다.
|
||||
This file tracks the shared data fields used by `data/cards.json`.
|
||||
The goal is to keep card behavior reusable instead of hardcoding one-off card names.
|
||||
|
||||
## 피해 수치
|
||||
## Damage
|
||||
|
||||
- `damage`: 기본 피해
|
||||
- `damagePerOtherHandCard`: 손패의 다른 카드 수만큼 피해 증감
|
||||
- `damagePerAttackPlayedThisTurn`: 이번 턴에 사용한 공격 카드 수만큼 피해 증감
|
||||
- `damagePerDiscardedThisTurn`: 이번 턴에 버린 카드 수만큼 피해 증감
|
||||
- `damagePerSkillInHand`: 손패의 스킬 카드 수만큼 피해 증감
|
||||
- `otherHandAtLeast`: 손패의 다른 카드가 이 수 이상일 때 조건 충족
|
||||
- `bonusHitsWhenOtherHandAtLeast`: 조건 충족 시 추가 적중 수
|
||||
- `damage`: base attack damage
|
||||
- `damagePerOtherHandCard`: bonus damage per other card in hand
|
||||
- `damagePerAttackPlayedThisTurn`: bonus damage per attack played this turn
|
||||
- `damagePerDiscardedThisTurn`: bonus damage per card discarded this turn
|
||||
- `damagePerSkillInHand`: bonus damage per skill card in hand
|
||||
- `damagePerCardDrawnThisCombat`: bonus damage per card drawn this combat
|
||||
- `damagePerTurn`: damage applied at turn start
|
||||
- `cardPlayedDamage`: damage when the card is played
|
||||
- `cardPlayedRandomDamage`: random damage when the card is played
|
||||
- `drawDamage`: damage dealt when a card is drawn
|
||||
- `shivDamageBonus`: bonus damage for all Shivs
|
||||
- `firstShivDamageBonus`: bonus damage for the first Shiv each turn
|
||||
- `attackDamageVsWeakMultiplier`: multiplier when the attack hits Weak targets
|
||||
|
||||
## 방어/상태
|
||||
## Block and utility
|
||||
|
||||
- `block`: 방어도 획득
|
||||
- `cardPlayedBlock`: 카드를 사용할 때마다 방어도 획득
|
||||
- `blockGainMultiplier`: 이번 턴 동안 얻는 방어도 배수
|
||||
- `hits`: 다단히트 횟수
|
||||
- `aoe`: 모든 적 대상
|
||||
- `pierce`: 방어도 무시
|
||||
- `draw`: 즉시 드로우
|
||||
- `drawUntilHandSize`: 손패가 지정 장수에 도달할 때까지 드로우
|
||||
- `heal`: 즉시 회복
|
||||
- `gainEnergy`: 즉시 에너지 획득
|
||||
- `strength`: 힘 획득
|
||||
- `dex`: 민첩 획득
|
||||
- `thorns`: 가시 획득
|
||||
- `selfVuln`: 자신에게 취약 부여
|
||||
- `block`: gain block
|
||||
- `cardPlayedBlock`: gain block whenever a card is played
|
||||
- `blockGainMultiplier`: multiplier for block gained this turn
|
||||
- `hits`: multi-hit count
|
||||
- `aoe`: hit all enemies
|
||||
- `pierce`: ignore block
|
||||
- `draw`: draw cards immediately
|
||||
- `drawUntilHandSize`: draw until hand reaches a target size
|
||||
- `drawSkillBlock`: gain block for each Skill drawn
|
||||
- `drawPoison`: apply poison when a card is drawn
|
||||
- `heal`: heal immediately
|
||||
- `gainEnergy`: gain energy immediately
|
||||
- `strength`: gain Strength
|
||||
- `dex`: gain Dexterity
|
||||
- `thorns`: gain Thorns
|
||||
- `selfVuln`: apply Vulnerable to self
|
||||
|
||||
## 상태이상
|
||||
## Status
|
||||
|
||||
- `weak`: 약화 부여
|
||||
- `vuln`: 취약 부여
|
||||
- `poison`: 중독 부여
|
||||
- `weak`: apply Weak
|
||||
- `vuln`: apply Vulnerable
|
||||
- `poison`: apply Poison
|
||||
- `poisonHits`: apply poison multiple times
|
||||
- `poisonRandomTargets`: spread poison applications across random alive enemies
|
||||
|
||||
`poison`은 적 턴 시작 시 피해를 주고 1 감소합니다.
|
||||
`poison` deals damage at enemy turn start and then decreases by 1.
|
||||
|
||||
## 드로우/버리기
|
||||
## Shivs and discard
|
||||
|
||||
- `discard`: 손패에서 지정 장수 버리기
|
||||
- `discardAll`: 손패 전부 버리기
|
||||
- `drawPerDiscarded`: 버린 카드 1장당 추가 드로우
|
||||
- `addShiv`: 표창 생성
|
||||
- `addShivPerDiscard`: 버린 장수만큼 표창 생성
|
||||
- `sly`: 버려질 때 교활 발동
|
||||
- `retain`: 턴 종료 시 해당 카드 보존
|
||||
- `discard`: discard a chosen number of cards from hand
|
||||
- `discardAll`: discard the whole hand
|
||||
- `drawPerDiscarded`: draw one extra card per discarded card
|
||||
- `addShiv`: create Shiv cards
|
||||
- `addShivPerDiscard`: create one Shiv per discarded card
|
||||
- `shivRetain`: Shiv cards are retained at end of turn
|
||||
- `shivAoe`: Shiv cards hit all enemies for the turn
|
||||
- `sly`: trigger on discard
|
||||
- `retain`: keep the card at end of turn
|
||||
|
||||
## 파워/턴 효과
|
||||
## Powers and turn effects
|
||||
|
||||
- `powerEffect: "strengthPerTurn"`
|
||||
- `powerEffect: "energyPerTurn"`
|
||||
- `powerEffect: "blockPerTurn"`
|
||||
- `powerEffect: "poisonPerTurn"`
|
||||
- `powerEffect: "damagePerTurn"`
|
||||
- `powerEffect: "retainOne"`
|
||||
- `turnStartShiv`: 턴 시작 시 표창 생성
|
||||
- `turnStartDraw`: 턴 시작 시 추가 드로우
|
||||
- `turnStartDiscard`: 턴 시작 시 카드 버리기
|
||||
- `turnStartShiv`: create Shivs at turn start
|
||||
- `turnStartDraw`: draw cards at turn start
|
||||
- `turnStartDiscard`: discard cards at turn start
|
||||
|
||||
## 다음 턴 예약
|
||||
## Next turn planning
|
||||
|
||||
- `nextTurnBlock`: 다음 턴 시작 시 방어도 획득
|
||||
- `nextTurnDraw`: 다음 턴 시작 시 추가 드로우
|
||||
- `nextTurnKeepBlock`: 다음 턴 시작 시 기존 방어도 유지
|
||||
- `nextTurnAttackMultiplier`: 다음 턴 공격 피해 배수
|
||||
- `nextTurnCopies`: 다음 턴에 손패에서 가져올 복사본 수
|
||||
- `nextTurnSelectHandCard`: 현재 손패에서 카드 1장 선택
|
||||
- `nextTurnSelectPrompt`: 선택 UI 문구
|
||||
- `nextSkillRepeatCount`: 다음 스킬 카드의 효과를 추가 횟수만큼 다시 적용
|
||||
- `nextSkillCostZero`: 다음 스킬 카드 비용을 0으로 만듦
|
||||
- `skillCostReductionThisTurn`: 이번 턴 스킬 카드 비용을 일정량 감소
|
||||
- `nextTurnBlock`: gain block next turn
|
||||
- `nextTurnDraw`: draw extra cards next turn
|
||||
- `nextTurnKeepBlock`: keep block next turn
|
||||
- `nextTurnAttackMultiplier`: attack multiplier next turn
|
||||
- `nextTurnCopies`: copy a chosen card next turn
|
||||
- `nextTurnSelectHandCard`: choose a card from the current hand for next turn copies
|
||||
- `nextTurnSelectPrompt`: prompt text for selection UI
|
||||
- `nextSkillRepeatCount`: repeat the next Skill's effect
|
||||
- `nextSkillCostZero`: make the next Skill cost 0
|
||||
- `skillCostReductionThisTurn`: reduce Skill costs this turn
|
||||
|
||||
## 기타
|
||||
## Misc
|
||||
|
||||
- `innate`: 전투 시작 시 첫 손패에 우선 진입
|
||||
- `playableWhenDrawPileEmpty`: 뽑을 카드 더미가 비었을 때만 사용 가능
|
||||
- `exhaust`: 사용 후 소멸
|
||||
- `unplayable`: 사용 불가
|
||||
- `curse`: 저주 카드
|
||||
- `token`: 토큰 카드
|
||||
- `endTurnDamage`: 턴 종료 시 손패에 있으면 피해
|
||||
- `innate`: place the card in the opening hand
|
||||
- `playableWhenDrawPileEmpty`: only playable when the draw pile is empty
|
||||
- `exhaust`: exhaust after use
|
||||
- `unplayable`: cannot be played
|
||||
- `curse`: curse card
|
||||
- `token`: token card
|
||||
- `endTurnDamage`: damage if the card remains in hand at end of turn
|
||||
|
||||
## 사용 원칙
|
||||
|
||||
- 카드 전용 분기보다 공용 필드를 먼저 쓴다.
|
||||
- 같은 효과는 같은 필드로 재사용한다.
|
||||
- 새 카드가 같은 패턴이면 먼저 공용 필드를 추가한다.
|
||||
## Rules
|
||||
|
||||
- Prefer shared fields over card-specific branches.
|
||||
- Reuse the same field name for the same behavior.
|
||||
- Add a new shared field before adding more special-case card logic.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
|
||||
// AI 전투 밸런스 시뮬레이터 — 오프라인 몬테카를로.
|
||||
// ⚠️ 전투 규칙은 tools/deck/gen-slaydeck.mjs 의 Lua(SlayDeckController)와 동기화 유지할 것.
|
||||
// (데이터는 data/*.json 공유, 규칙 로직은 JS로 중복 재현)
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -159,6 +159,10 @@ export function simulateCombat(data, rng, stats) {
|
||||
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
|
||||
let nextTurnAddCards = [];
|
||||
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
|
||||
let shivFirstDamageBonusUsed = false;
|
||||
let drawDamageThisTurn = 0;
|
||||
let drawPoisonThisTurn = 0;
|
||||
let shivAoeThisCombat = false;
|
||||
let cardsDrawnThisCombat = 0;
|
||||
let bonusRewardScreens = 0;
|
||||
let activeKillReward = 0;
|
||||
@@ -179,6 +183,23 @@ export function simulateCombat(data, rng, stats) {
|
||||
const card = drawPile.pop();
|
||||
drawn.push(card);
|
||||
cardsDrawnThisCombat++;
|
||||
const drawDamage = powerFieldTotal('drawDamage') + drawDamageThisTurn;
|
||||
const drawPoison = powerFieldTotal('drawPoison') + drawPoisonThisTurn;
|
||||
if ((drawDamage > 0 || drawPoison > 0) && mob.some((m) => m.alive)) {
|
||||
for (const m of mob) {
|
||||
if (!m.alive) continue;
|
||||
let dmg = drawDamage;
|
||||
if (m.vuln > 0) dmg = Math.floor(dmg * 1.5);
|
||||
if (m.block > 0) {
|
||||
const absorbed = Math.min(m.block, dmg);
|
||||
m.block -= absorbed;
|
||||
dmg -= absorbed;
|
||||
}
|
||||
if (drawPoison > 0) m.poison += drawPoison;
|
||||
if (dmg > 0) m.hp -= dmg;
|
||||
if (m.hp <= 0) { m.hp = 0; m.alive = false; }
|
||||
}
|
||||
}
|
||||
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
|
||||
if (hand.length >= 10) {
|
||||
discard.push(card);
|
||||
@@ -236,6 +257,10 @@ export function simulateCombat(data, rng, stats) {
|
||||
if (c.damagePerDiscardedThisTurn) base += turnDiscardedCards * c.damagePerDiscardedThisTurn;
|
||||
if (c.damagePerSkillInHand) base += countOtherHandSkills(id) * c.damagePerSkillInHand;
|
||||
if (c.damagePerCardDrawnThisCombat) base += cardsDrawnThisCombat * c.damagePerCardDrawnThisCombat;
|
||||
if (c.class === 'shiv') {
|
||||
if (powerFieldTotal('shivDamageBonus') > 0) base += powerFieldTotal('shivDamageBonus');
|
||||
if (!shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) base += powerFieldTotal('firstShivDamageBonus');
|
||||
}
|
||||
if (base < 0) base = 0;
|
||||
return base;
|
||||
}
|
||||
@@ -286,6 +311,9 @@ export function simulateCombat(data, rng, stats) {
|
||||
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
|
||||
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
|
||||
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
|
||||
if (c.drawDamage && c.kind !== 'Power') drawDamageThisTurn += c.drawDamage;
|
||||
if (c.drawPoison && c.kind !== 'Power') drawPoisonThisTurn += c.drawPoison;
|
||||
if (c.shivAoe === true && c.kind !== 'Power') shivAoeThisCombat = true;
|
||||
const xEnergy = costSpent || 0;
|
||||
if (c.kind === 'Attack') {
|
||||
if (alive.length && (c.damage || c.xDamagePerEnergy)) {
|
||||
@@ -300,9 +328,14 @@ export function simulateCombat(data, rng, stats) {
|
||||
let totalNv = 0;
|
||||
for (let h = 0; h < hitN; h++) totalNv += calcAttack(baseDamage || 0, pStr, pWeak, 0) * turnAttackMultiplier;
|
||||
dmg = totalNv;
|
||||
if (c.aoe === true) {
|
||||
let useAoe = c.aoe === true;
|
||||
if (c.class === 'shiv' && shivAoeThisCombat === true) useAoe = true;
|
||||
if (useAoe === true) {
|
||||
for (const m2 of aliveList()) {
|
||||
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
let d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
if (m2.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) {
|
||||
d2 = Math.floor(d2 * c.attackDamageVsWeakMultiplier);
|
||||
}
|
||||
const r2 = applyDamage(m2.hp, m2.block, d2);
|
||||
m2.hp = r2.hp; m2.block = r2.block;
|
||||
const attackPoison = powerFieldTotal('attackPoison');
|
||||
@@ -314,6 +347,9 @@ export function simulateCombat(data, rng, stats) {
|
||||
}
|
||||
} else {
|
||||
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
if (target.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) {
|
||||
dmg = Math.floor(dmg * c.attackDamageVsWeakMultiplier);
|
||||
}
|
||||
if (c.pierce === true) {
|
||||
target.hp -= dmg;
|
||||
if (target.hp < 0) target.hp = 0;
|
||||
@@ -339,7 +375,18 @@ export function simulateCombat(data, rng, stats) {
|
||||
const target = chooseTarget(alive, 0);
|
||||
if (weakAmount) target.weak += weakAmount;
|
||||
if (c.vuln) target.vuln += c.vuln;
|
||||
if (c.poison) target.poison += c.poison;
|
||||
if (c.poison) {
|
||||
const poisonHits = c.poisonHits || 1;
|
||||
for (let i = 0; i < poisonHits; i++) {
|
||||
const target2 = c.poisonRandomTargets === true
|
||||
? alive[Math.floor(rng() * alive.length)]
|
||||
: target;
|
||||
if (target2) target2.poison += c.poison;
|
||||
}
|
||||
}
|
||||
if (c.class === 'shiv' && !shivFirstDamageBonusUsed && powerFieldTotal('firstShivDamageBonus') > 0) {
|
||||
shivFirstDamageBonusUsed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c.strength) pStr += c.strength;
|
||||
@@ -413,6 +460,10 @@ export function simulateCombat(data, rng, stats) {
|
||||
turns++;
|
||||
turnAttackCardsPlayed = 0;
|
||||
turnDiscardedCards = 0;
|
||||
shivFirstDamageBonusUsed = false;
|
||||
drawDamageThisTurn = 0;
|
||||
drawPoisonThisTurn = 0;
|
||||
shivAoeThisCombat = false;
|
||||
blockGainMultiplier = 1;
|
||||
handCostZeroThisTurn = false;
|
||||
drawDisabledThisTurn = false;
|
||||
@@ -530,7 +581,7 @@ export function simulateCombat(data, rng, stats) {
|
||||
const kept = [];
|
||||
for (const hid of hand) {
|
||||
const hc = cards[hid];
|
||||
if (hc?.retain === true) kept.push(hid);
|
||||
if (hc?.retain === true || (hc?.class === 'shiv' && powerFieldTotal('shivRetain') > 0)) kept.push(hid);
|
||||
else discard.push(hid);
|
||||
}
|
||||
hand = kept;
|
||||
|
||||
@@ -914,3 +914,38 @@ test("simulateCombat: damagePerCardDrawnThisCombat scales murder", () => {
|
||||
assert.equal(r.win, true);
|
||||
assert.ok(stats.Murder.damage > 1);
|
||||
});
|
||||
|
||||
test("simulateCombat: shiv damage bonuses stack and first Shiv bonus applies once per turn", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
Accuracy: { name: "Accuracy", cost: 1, kind: "Power", shivDamageBonus: 2 },
|
||||
PhantomBlades: { name: "PhantomBlades", cost: 1, kind: "Power", firstShivDamageBonus: 3 },
|
||||
Shiv: { name: "Shiv", cost: 0, kind: "Attack", class: "shiv", damage: 1 },
|
||||
},
|
||||
starterDeck: ["Accuracy", "PhantomBlades", "Shiv"],
|
||||
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||
};
|
||||
const r = simulateCombat(data, () => 0.999999);
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
test("simulateCombat: shivAoe makes Shivs hit all enemies", () => {
|
||||
const data = {
|
||||
cards: {
|
||||
FanOfKnives: { name: "FanOfKnives", cost: 2, kind: "Skill", addShiv: 2, shivAoe: true },
|
||||
Accuracy: { name: "Accuracy", cost: 1, kind: "Power", shivDamageBonus: 2 },
|
||||
Shiv: { name: "Shiv", cost: 0, kind: "Attack", class: "shiv", damage: 1 },
|
||||
Pass: { name: "Pass", cost: 99, kind: "Skill" },
|
||||
},
|
||||
starterDeck: ["Accuracy", "FanOfKnives", "Pass"],
|
||||
monsters: [
|
||||
{ name: "A", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] },
|
||||
{ name: "B", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] },
|
||||
{ name: "C", maxHp: 3, intents: [{ kind: "Attack", value: 0 }] },
|
||||
],
|
||||
};
|
||||
const r = simulateCombat(data, () => 0.999999);
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
@@ -271,6 +271,9 @@ local dmg = amount
|
||||
if m.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
end
|
||||
if m.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
|
||||
dmg = math.floor(dmg * self.ActiveAttackDamageVsWeakMultiplier)
|
||||
end
|
||||
if m.block > 0 and pierce ~= true then
|
||||
local absorbed = math.min(m.block, dmg)
|
||||
m.block = m.block - absorbed
|
||||
@@ -345,6 +348,7 @@ return killed`, [
|
||||
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)
|
||||
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
return
|
||||
@@ -370,11 +374,15 @@ _TimerService:SetTimerOnce(function()
|
||||
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
||||
shown = math.floor(damage * 1.5)
|
||||
end
|
||||
if mt ~= nil and mt.alive == true and mt.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
|
||||
shown = math.floor(shown * self.ActiveAttackDamageVsWeakMultiplier)
|
||||
end
|
||||
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.ActiveAttackDamageVsWeakMultiplier = 1
|
||||
self:ShowDmgPop(targetIndex, shown)
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
@@ -406,6 +414,9 @@ _TimerService:SetTimerOnce(function()
|
||||
if m.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
end
|
||||
if m.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
|
||||
dmg = math.floor(dmg * self.ActiveAttackDamageVsWeakMultiplier)
|
||||
end
|
||||
if m.block > 0 then
|
||||
local absorbed = math.min(m.block, dmg)
|
||||
m.block = m.block - absorbed
|
||||
@@ -431,6 +442,7 @@ _TimerService:SetTimerOnce(function()
|
||||
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
|
||||
end
|
||||
self.ActiveKillReward = 0
|
||||
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
end, 0.35)`, [
|
||||
|
||||
@@ -247,6 +247,11 @@ end
|
||||
self.TurnAttackMultiplier = self.NextTurnAttackMultiplier or 1
|
||||
self.NextTurnAttackMultiplier = 1
|
||||
self.CardsDrawnThisCombat = self.CardsDrawnThisCombat or 0
|
||||
self.ShivFirstDamageBonusUsed = false
|
||||
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||
self.DrawDamageThisTurn = 0
|
||||
self.DrawPoisonThisTurn = 0
|
||||
self.ShivAoeThisCombat = false
|
||||
self.HandCostZeroThisTurn = false
|
||||
self.DrawDisabledThisTurn = false
|
||||
local powerTurnDraw = 0
|
||||
@@ -419,7 +424,7 @@ local kept = {}
|
||||
for i = 1, #self.Hand do
|
||||
\tlocal cardId = self.Hand[i]
|
||||
\tlocal c = self.Cards[cardId]
|
||||
\tif c ~= nil and (c.retain == true or i == retainSlot) then
|
||||
\tif c ~= nil and (c.retain == true or (c.class == "shiv" and self:HasPowerField("shivRetain") == true) or i == retainSlot) then
|
||||
\t\ttable.insert(kept, cardId)
|
||||
\telse
|
||||
\t\ttable.insert(self.DiscardPile, cardId)
|
||||
|
||||
@@ -311,6 +311,14 @@ end
|
||||
if c.damagePerCardDrawnThisCombat ~= nil then
|
||||
base2 = base2 + (self.CardsDrawnThisCombat or 0) * c.damagePerCardDrawnThisCombat
|
||||
end
|
||||
if c.class == "shiv" then
|
||||
if self:HasPowerField("shivDamageBonus") == true then
|
||||
base2 = base2 + self:AddPowerFieldTotal("shivDamageBonus")
|
||||
end
|
||||
if self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
|
||||
base2 = base2 + self:AddPowerFieldTotal("firstShivDamageBonus")
|
||||
end
|
||||
end
|
||||
if base2 < 0 then
|
||||
base2 = 0
|
||||
end
|
||||
@@ -394,6 +402,15 @@ end
|
||||
if c.drawDisabledThisTurn == true then
|
||||
self.DrawDisabledThisTurn = true
|
||||
end
|
||||
if c.drawDamage ~= nil and c.drawDamage > 0 and c.kind ~= "Power" then
|
||||
self.DrawDamageThisTurn = (self.DrawDamageThisTurn or 0) + c.drawDamage
|
||||
end
|
||||
if c.drawPoison ~= nil and c.drawPoison > 0 and c.kind ~= "Power" then
|
||||
self.DrawPoisonThisTurn = (self.DrawPoisonThisTurn or 0) + c.drawPoison
|
||||
end
|
||||
if c.shivAoe == true and c.kind ~= "Power" then
|
||||
self.ShivAoeThisCombat = true
|
||||
end
|
||||
local xEnergy = energySpent or 0
|
||||
local weakAmount = c.weak or 0
|
||||
local vulnAmount = c.vuln or 0
|
||||
@@ -405,6 +422,7 @@ if c.kind == "Attack" then
|
||||
if c.damage ~= nil or c.xDamagePerEnergy ~= nil then
|
||||
self:PlayerAttackMotion()
|
||||
local baseDmg = self:AttackBaseForCard(slot, c)
|
||||
self.ActiveAttackDamageVsWeakMultiplier = c.attackDamageVsWeakMultiplier or 1
|
||||
if c.xDamagePerEnergy ~= nil and c.xDamagePerEnergy > 0 then
|
||||
baseDmg = xEnergy * c.xDamagePerEnergy
|
||||
end
|
||||
@@ -423,7 +441,14 @@ if c.kind == "Attack" then
|
||||
for h = 1, hitN do
|
||||
total = total + self:CalcPlayerAttack(baseDmg)
|
||||
end
|
||||
if c.aoe == true then
|
||||
local useAoe = c.aoe == true
|
||||
if c.class == "shiv" and (self.ShivAoeThisCombat == true or self:HasPowerField("shivAoe") == true) then
|
||||
useAoe = true
|
||||
end
|
||||
if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
|
||||
self.ShivFirstDamageBonusUsed = true
|
||||
end
|
||||
if useAoe == true then
|
||||
self:PlayAoeFx(c.fx or c.image, total)
|
||||
else
|
||||
self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true)
|
||||
@@ -475,7 +500,27 @@ if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil or c.xWeakPerEnergy ~= nil
|
||||
end
|
||||
if tm ~= nil and tm.alive == true then
|
||||
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 poisonAmount ~= nil and poisonAmount > 0 then
|
||||
local poisonHits = c.poisonHits or 1
|
||||
for pi = 1, poisonHits do
|
||||
local target = tm
|
||||
if c.poisonRandomTargets == true and self.Monsters ~= nil then
|
||||
local alive = {}
|
||||
for mi = 1, #self.Monsters do
|
||||
local om = self.Monsters[mi]
|
||||
if om ~= nil and om.alive == true then
|
||||
table.insert(alive, om)
|
||||
end
|
||||
end
|
||||
if #alive > 0 then
|
||||
target = alive[math.random(#alive)]
|
||||
end
|
||||
end
|
||||
if target ~= nil and target.alive == true then
|
||||
target.poison = (target.poison or 0) + poisonAmount
|
||||
end
|
||||
end
|
||||
end
|
||||
if vulnAmount ~= nil and vulnAmount > 0 then
|
||||
tm.vuln = tm.vuln + vulnAmount
|
||||
if self:HasRelic("championBelt") then
|
||||
@@ -512,6 +557,38 @@ if c.drawSkillBlock ~= nil and c.drawSkillBlock > 0 then
|
||||
end
|
||||
end
|
||||
end
|
||||
local drawDamage = self:AddPowerFieldTotal("drawDamage") + (self.DrawDamageThisTurn or 0)
|
||||
local drawPoison = self:AddPowerFieldTotal("drawPoison") + (self.DrawPoisonThisTurn or 0)
|
||||
if (drawDamage ~= nil and drawDamage > 0) or (drawPoison ~= nil and drawPoison > 0) then
|
||||
for mi = 1, #self.Monsters do
|
||||
local m2 = self.Monsters[mi]
|
||||
if m2 ~= nil and m2.alive == true then
|
||||
local dmg = drawDamage or 0
|
||||
if m2.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
end
|
||||
if m2.block > 0 then
|
||||
local absorbed = math.min(m2.block, dmg)
|
||||
m2.block = m2.block - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
if drawPoison ~= nil and drawPoison > 0 then
|
||||
m2.poison = (m2.poison or 0) + drawPoison
|
||||
end
|
||||
if dmg > 0 then
|
||||
m2.hp = m2.hp - dmg
|
||||
end
|
||||
self:ShowDmgPop(mi, dmg)
|
||||
self:MonsterHitMotion(mi)
|
||||
if m2.hp <= 0 then
|
||||
m2.hp = 0
|
||||
self:KillMonster(m2.slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
end
|
||||
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
||||
self:AddCardsToHand("Shiv", c.addShiv)
|
||||
end`, [
|
||||
|
||||
@@ -74,6 +74,11 @@ self.DrawDisabledThisTurn = false
|
||||
self.NextSkillCostZero = false
|
||||
self.NextSkillRepeatCount = 0
|
||||
self.SkillCostReductionThisTurn = 0
|
||||
self.ShivFirstDamageBonusUsed = false
|
||||
self.ActiveAttackDamageVsWeakMultiplier = 1
|
||||
self.DrawDamageThisTurn = 0
|
||||
self.DrawPoisonThisTurn = 0
|
||||
self.ShivAoeThisCombat = false
|
||||
self.PlayerStr = 0
|
||||
self.PlayerDex = 0
|
||||
self.PlayerThorns = 0
|
||||
|
||||
@@ -192,6 +192,8 @@ function luaCardsTable(cards) {
|
||||
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
||||
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
|
||||
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${c.drawSkillBlock}`);
|
||||
if (c.drawDamage != null) fields.push(`drawDamage = ${c.drawDamage}`);
|
||||
if (c.drawPoison != null) fields.push(`drawPoison = ${c.drawPoison}`);
|
||||
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
||||
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
|
||||
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
||||
@@ -206,6 +208,13 @@ function luaCardsTable(cards) {
|
||||
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.shivDamageBonus != null) fields.push(`shivDamageBonus = ${c.shivDamageBonus}`);
|
||||
if (c.firstShivDamageBonus != null) fields.push(`firstShivDamageBonus = ${c.firstShivDamageBonus}`);
|
||||
if (c.shivRetain === true) fields.push('shivRetain = true');
|
||||
if (c.shivAoe === true) fields.push('shivAoe = true');
|
||||
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${c.attackDamageVsWeakMultiplier}`);
|
||||
if (c.poisonHits != null) fields.push(`poisonHits = ${c.poisonHits}`);
|
||||
if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = 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}`);
|
||||
|
||||
Reference in New Issue
Block a user