밴딧 공용 효과와 문서 정리
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -237,7 +237,8 @@
|
|||||||
"kind": "Skill",
|
"kind": "Skill",
|
||||||
"class": "magician",
|
"class": "magician",
|
||||||
"block": 3,
|
"block": 3,
|
||||||
"draw": 1,
|
"discardAll": true,
|
||||||
|
"drawPerDiscarded": 1,
|
||||||
"desc": "방어도 3, 드로 1",
|
"desc": "방어도 3, 드로 1",
|
||||||
"image": "7f70a9dc7e304433bb8121dd9c4df98b",
|
"image": "7f70a9dc7e304433bb8121dd9c4df98b",
|
||||||
"rarity": "normal"
|
"rarity": "normal"
|
||||||
@@ -453,7 +454,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "normal",
|
"rarity": "normal",
|
||||||
"desc": "피해를 9 줍니다. 카드를 1장 뽑습니다. 카드를 1장 버립니다.",
|
"desc": "피해를 9 줍니다. 카드를 1장 뽑습니다. 카드를 1장 버립니다.",
|
||||||
"draw": 1,
|
"drawUntilHandSize": 6,
|
||||||
"damage": 9,
|
"damage": 9,
|
||||||
"discard": 1,
|
"discard": 1,
|
||||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||||
@@ -499,6 +500,8 @@
|
|||||||
"rarity": "normal",
|
"rarity": "normal",
|
||||||
"desc": "피해를 7 줍니다. 손에 다른 카드가 5장 이상 있다면, 1번 추가로 적중합니다.",
|
"desc": "피해를 7 줍니다. 손에 다른 카드가 5장 이상 있다면, 1번 추가로 적중합니다.",
|
||||||
"damage": 7,
|
"damage": 7,
|
||||||
|
"otherHandAtLeast": 5,
|
||||||
|
"bonusHitsWhenOtherHandAtLeast": 1,
|
||||||
"image": "92a5020c978c46bdabab910598118b86"
|
"image": "92a5020c978c46bdabab910598118b86"
|
||||||
},
|
},
|
||||||
"FlickFlack": {
|
"FlickFlack": {
|
||||||
@@ -586,6 +589,7 @@
|
|||||||
"rarity": "normal",
|
"rarity": "normal",
|
||||||
"desc": "방어도를 4 얻습니다. 다음 턴에, 방어도를 4 얻습니다",
|
"desc": "방어도를 4 얻습니다. 다음 턴에, 방어도를 4 얻습니다",
|
||||||
"block": 4,
|
"block": 4,
|
||||||
|
"nextTurnBlock": 4,
|
||||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||||
},
|
},
|
||||||
"PiercingWail": {
|
"PiercingWail": {
|
||||||
@@ -658,6 +662,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "선천성. 피해를 11 줍니다. 소멸.",
|
"desc": "선천성. 피해를 11 줍니다. 소멸.",
|
||||||
|
"innate": true,
|
||||||
"damage": 11,
|
"damage": 11,
|
||||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||||
},
|
},
|
||||||
@@ -669,6 +674,7 @@
|
|||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "피해를 13 줍니다. 손에 있는 다른 카드 1장당 피해량이 2 감소합니다.",
|
"desc": "피해를 13 줍니다. 손에 있는 다른 카드 1장당 피해량이 2 감소합니다.",
|
||||||
"damage": 13,
|
"damage": 13,
|
||||||
|
"damagePerOtherHandCard": -2,
|
||||||
"image": "92a5020c978c46bdabab910598118b86"
|
"image": "92a5020c978c46bdabab910598118b86"
|
||||||
},
|
},
|
||||||
"Finisher": {
|
"Finisher": {
|
||||||
@@ -678,7 +684,8 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "이번 턴에 사용한 공격 카드 1장당 피해를 6 줍니다.",
|
"desc": "이번 턴에 사용한 공격 카드 1장당 피해를 6 줍니다.",
|
||||||
"damage": 6,
|
"damage": 0,
|
||||||
|
"damagePerAttackPlayedThisTurn": 6,
|
||||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||||
},
|
},
|
||||||
"MementoMori": {
|
"MementoMori": {
|
||||||
@@ -689,6 +696,7 @@
|
|||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "피해를 9 줍니다. 이번 턴에 버린 카드 1장당 피해량이 4 증가합니다.",
|
"desc": "피해를 9 줍니다. 이번 턴에 버린 카드 1장당 피해량이 4 증가합니다.",
|
||||||
"damage": 9,
|
"damage": 9,
|
||||||
|
"damagePerDiscardedThisTurn": 4,
|
||||||
"image": "0946f69d84464df29b24b94c744c868d"
|
"image": "0946f69d84464df29b24b94c744c868d"
|
||||||
},
|
},
|
||||||
"Strangle": {
|
"Strangle": {
|
||||||
@@ -708,7 +716,8 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "손에 있는 스킬 카드 1장당 피해를 5 줍니다.",
|
"desc": "손에 있는 스킬 카드 1장당 피해를 5 줍니다.",
|
||||||
"damage": 5,
|
"damage": 0,
|
||||||
|
"damagePerSkillInHand": 5,
|
||||||
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
"image": "1b0f2dc8abd0434990eee1befefcbe0d"
|
||||||
},
|
},
|
||||||
"Pounce": {
|
"Pounce": {
|
||||||
@@ -739,7 +748,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "피해를 15 줍니다. 다음 턴에, 카드를 2장 뽑습니다.",
|
"desc": "피해를 15 줍니다. 다음 턴에, 카드를 2장 뽑습니다.",
|
||||||
"draw": 2,
|
"nextTurnDraw": 2,
|
||||||
"damage": 15,
|
"damage": 15,
|
||||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||||
},
|
},
|
||||||
@@ -760,8 +769,9 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "손에 있는 모든 카드를 버린 뒤, 버린 카드의 수만큼 카드를 뽑습니다. 소멸.",
|
"desc": "손에 있는 모든 카드를 버린 뒤, 버린 카드의 수만큼 카드를 뽑습니다. 소멸.",
|
||||||
"draw": 1,
|
"image": "c1e19219745e44c39ae6ac2f77e347d9",
|
||||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
"discardAll": true,
|
||||||
|
"drawPerDiscarded": 1
|
||||||
},
|
},
|
||||||
"Expose": {
|
"Expose": {
|
||||||
"name": "들춰내기",
|
"name": "들춰내기",
|
||||||
@@ -833,8 +843,8 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "손에 있는 카드가 6장이 될 때까지 카드를 뽑습니다.",
|
"desc": "손에 있는 카드가 6장이 될 때까지 카드를 뽑습니다.",
|
||||||
"draw": 1,
|
"image": "c1e19219745e44c39ae6ac2f77e347d9",
|
||||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
"drawUntilHandSize": 6
|
||||||
},
|
},
|
||||||
"BubbleBubble": {
|
"BubbleBubble": {
|
||||||
"name": "차오르는 독",
|
"name": "차오르는 독",
|
||||||
@@ -854,6 +864,7 @@
|
|||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "방어도를 5 얻습니다. 다음 턴 시작 시 방어도가 사라지지 않습니다.",
|
"desc": "방어도를 5 얻습니다. 다음 턴 시작 시 방어도가 사라지지 않습니다.",
|
||||||
"block": 5,
|
"block": 5,
|
||||||
|
"nextTurnKeepBlock": true,
|
||||||
"image": "0946f69d84464df29b24b94c744c868d"
|
"image": "0946f69d84464df29b24b94c744c868d"
|
||||||
},
|
},
|
||||||
"LegSweep": {
|
"LegSweep": {
|
||||||
@@ -916,8 +927,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "교활. 을 얻습니다.",
|
"desc": "교활. 을 얻습니다.",
|
||||||
"powerEffect": "energyPerTurn",
|
"gainEnergy": 1,
|
||||||
"value": 1,
|
|
||||||
"sly": true,
|
"sly": true,
|
||||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
||||||
},
|
},
|
||||||
@@ -1019,6 +1029,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "뽑을 카드 더미에 카드가 없을 때만 사용할 수 있습니다. 모든 적에게 피해를 60 줍니다.",
|
"desc": "뽑을 카드 더미에 카드가 없을 때만 사용할 수 있습니다. 모든 적에게 피해를 60 줍니다.",
|
||||||
|
"playableWhenDrawPileEmpty": true,
|
||||||
"aoe": true,
|
"aoe": true,
|
||||||
"damage": 60,
|
"damage": 60,
|
||||||
"image": "dbdbb1b56ae54672ae68ac6882fff6a2"
|
"image": "dbdbb1b56ae54672ae68ac6882fff6a2"
|
||||||
@@ -1030,6 +1041,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "선천성. 피해를 10 줍니다. 취약을 1 부여합니다. 소멸.",
|
"desc": "선천성. 피해를 10 줍니다. 취약을 1 부여합니다. 소멸.",
|
||||||
|
"innate": true,
|
||||||
"vuln": 1,
|
"vuln": 1,
|
||||||
"damage": 10,
|
"damage": 10,
|
||||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||||
@@ -1083,8 +1095,7 @@
|
|||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "를 얻습니다. 카드를 2장 뽑습니다. 소멸.",
|
"desc": "를 얻습니다. 카드를 2장 뽑습니다. 소멸.",
|
||||||
"draw": 2,
|
"draw": 2,
|
||||||
"powerEffect": "energyPerTurn",
|
"gainEnergy": 1,
|
||||||
"value": 1,
|
|
||||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||||
},
|
},
|
||||||
"StormOfSteel": {
|
"StormOfSteel": {
|
||||||
@@ -1105,7 +1116,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "손에 있는 모든 카드를 버립니다. 다음 턴에, 공격 카드의 피해량이 2배가 됩니다.",
|
"desc": "손에 있는 모든 카드를 버립니다. 다음 턴에, 공격 카드의 피해량이 2배가 됩니다.",
|
||||||
"draw": 1,
|
"nextTurnAttackMultiplier": 2,
|
||||||
"discardAll": true,
|
"discardAll": true,
|
||||||
"image": "0946f69d84464df29b24b94c744c868d"
|
"image": "0946f69d84464df29b24b94c744c868d"
|
||||||
},
|
},
|
||||||
@@ -1177,7 +1188,9 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "카드를 1장 선택합니다. 다음 턴에, 그 카드의 복사본을 3장 손으로 가져옵니다. 소멸.",
|
"desc": "카드를 1장 선택합니다. 다음 턴에, 그 카드의 복사본을 3장 손으로 가져옵니다. 소멸.",
|
||||||
"draw": 1,
|
"nextTurnCopies": 3,
|
||||||
|
"nextTurnSelectHandCard": true,
|
||||||
|
"nextTurnSelectPrompt": "복사할 카드를 선택하세요",
|
||||||
"image": "0946f69d84464df29b24b94c744c868d"
|
"image": "0946f69d84464df29b24b94c744c868d"
|
||||||
},
|
},
|
||||||
"ToolsOfTheTrade": {
|
"ToolsOfTheTrade": {
|
||||||
@@ -1187,10 +1200,8 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "내 턴 시작 시, 카드를 1장 뽑고 카드를 1장 버립니다.",
|
"desc": "내 턴 시작 시, 카드를 1장 뽑고 카드를 1장 버립니다.",
|
||||||
"draw": 1,
|
"turnStartDraw": 1,
|
||||||
"powerEffect": "energyPerTurn",
|
"turnStartDiscard": 1,
|
||||||
"value": 1,
|
|
||||||
"discard": 1,
|
|
||||||
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
"image": "c1e19219745e44c39ae6ac2f77e347d9"
|
||||||
},
|
},
|
||||||
"Afterimage": {
|
"Afterimage": {
|
||||||
@@ -1200,10 +1211,8 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "카드를 사용할 때마다, 방어도를 1 얻습니다.",
|
"desc": "카드를 사용할 때마다, 방어도를 1 얻습니다.",
|
||||||
"block": 1,
|
"image": "0946f69d84464df29b24b94c744c868d",
|
||||||
"powerEffect": "blockPerTurn",
|
"cardPlayedBlock": 1
|
||||||
"value": 2,
|
|
||||||
"image": "0946f69d84464df29b24b94c744c868d"
|
|
||||||
},
|
},
|
||||||
"Accelerant": {
|
"Accelerant": {
|
||||||
"name": "촉진제",
|
"name": "촉진제",
|
||||||
@@ -1293,6 +1302,7 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "선천성. 피해를 11 줍니다. 약화를 3 부여합니다.",
|
"desc": "선천성. 피해를 11 줍니다. 약화를 3 부여합니다.",
|
||||||
|
"innate": true,
|
||||||
"weak": 3,
|
"weak": 3,
|
||||||
"damage": 11,
|
"damage": 11,
|
||||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||||
|
|||||||
106
docs/bandit-card-audit.md
Normal file
106
docs/bandit-card-audit.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Bandit Card Audit
|
||||||
|
|
||||||
|
`bandit` 카드의 구현 상태를 카드별로 정리한 문서입니다.
|
||||||
|
|
||||||
|
상태 기준:
|
||||||
|
|
||||||
|
- `구현됨`: 공용 필드와 공용 로직으로 처리됨
|
||||||
|
- `부분구현`: 카드 설명의 일부만 맞음
|
||||||
|
- `미구현`: 아직 전용 메커니즘이 없음
|
||||||
|
|
||||||
|
## 구현됨
|
||||||
|
|
||||||
|
`Neutralize`, `SilentStrike`, `Survivor`, `SilentDefend`, `Slice`, `DaggerSpray`, `DaggerThrow`, `PoisonedStab`, `SuckerPunch`, `LeadingStrike`, `FollowThrough`, `FlickFlack`, `Prepared`, `Deflect`, `BladeDance`, `Backflip`, `DodgeAndRoll`, `CloakAndDagger`, `DeadlyPoison`, `Snakebite`, `Untouchable`, `Backstab`, `PreciseCut`, `Finisher`, `MementoMori`, `Flechettes`, `Dash`, `Predator`, `CalculatedGamble`, `HiddenDaggers`, `Acrobatics`, `Blur`, `LegSweep`, `Reflex`, `Haze`, `Tactician`, `WellLaidPlans`, `InfiniteBlades`, `Footwork`, `GrandFinale`, `Adrenaline`, `ShadowStep`, `Assassinate`, `Nightmare`, `ToolsOfTheTrade`, `Afterimage`, `StormOfSteel`, `Abrasive`, `Suppress`, `Expertise`
|
||||||
|
|
||||||
|
공용 메모:
|
||||||
|
|
||||||
|
- `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` 구현됨
|
||||||
|
|
||||||
|
## 부분구현
|
||||||
|
|
||||||
|
`Ricochet`: 무작위 적 4회 타격이 아니라 일반 분산 공격으로만 처리됨
|
||||||
|
|
||||||
|
`Anticipate`: 이번 턴 동안 민첩 2가 아니라 전투 전체 민첩 증가
|
||||||
|
|
||||||
|
`PiercingWail`: 이번 턴 적 공격 감소가 아니라 공용 약화/취약 계열만 적용
|
||||||
|
|
||||||
|
`Expose`: 방어도/인공물 제거는 없고 취약만 적용됨
|
||||||
|
|
||||||
|
`BubbleBubble`: 적이 독을 보유한 경우라는 조건이 아직 없음
|
||||||
|
|
||||||
|
`BouncingFlask`: 무작위 적 3번 분산 대신 단일 독 9 처리
|
||||||
|
|
||||||
|
## 미구현
|
||||||
|
|
||||||
|
`Skewer`: X코스트 연타 공격
|
||||||
|
|
||||||
|
`Outbreak`: 독 3번 부여 시 전체 피해 트리거
|
||||||
|
|
||||||
|
`Strangle`: 이번 턴 카드 사용마다 추가 피해
|
||||||
|
|
||||||
|
`Pounce`: 다음 스킬 카드 비용 0
|
||||||
|
|
||||||
|
`Pinpoint`: 이번 턴 스킬 사용 시 비용 감소
|
||||||
|
|
||||||
|
`EscapePlan`: 드로우한 카드가 스킬이면 방어도 3
|
||||||
|
|
||||||
|
`HandTrick`: 손패의 스킬 카드 하나에 교활 부여
|
||||||
|
|
||||||
|
`Mirage`: 모든 적의 독 총합만큼 방어 획득
|
||||||
|
|
||||||
|
`UpMySleeve`: 표창 생성 + 비용 감소
|
||||||
|
|
||||||
|
`NoxiousFumes`: 턴 시작 전체 적 독 부여 파워
|
||||||
|
|
||||||
|
`Accuracy`: 표창 피해 증가 파워
|
||||||
|
|
||||||
|
`PhantomBlades`: 표창 보존 + 첫 표창 강화
|
||||||
|
|
||||||
|
`Speedster`: 드로우할 때마다 전체 피해
|
||||||
|
|
||||||
|
`EchoingSlash`: 처치 시 반복
|
||||||
|
|
||||||
|
`TheHunt`: 처치 조건 보상
|
||||||
|
|
||||||
|
`Murder`: 이번 전투 동안 뽑은 카드 수 비례 피해
|
||||||
|
|
||||||
|
`Malaise`: X코스트 약화/피해 감소
|
||||||
|
|
||||||
|
`Shadowmeld`: 이번 턴 얻는 방어도 2배
|
||||||
|
|
||||||
|
`CorrosiveWave`: 드로우할 때마다 독
|
||||||
|
|
||||||
|
`BladeOfInk`: 전용 표창 생성
|
||||||
|
|
||||||
|
`Burst`: 다음 스킬 1회 추가 사용
|
||||||
|
|
||||||
|
`KnifeTrap`: 소멸된 표창 전부 사용
|
||||||
|
|
||||||
|
`BulletTime`: 드로우 금지 + 손패 무료 사용
|
||||||
|
|
||||||
|
`Accelerant`: 추가 독 발동
|
||||||
|
|
||||||
|
`Envenom`: 공격 적중 시 독 부여
|
||||||
|
|
||||||
|
`MasterPlanner`: 스킬 사용 시 교활 부여
|
||||||
|
|
||||||
|
`Tracking`: 약화된 적이 공격 피해를 2배로 받음
|
||||||
|
|
||||||
|
`FanOfKnives`: 표창이 모든 적 대상
|
||||||
|
|
||||||
|
`SerpentForm`: 카드 사용할 때마다 무작위 적에게 피해
|
||||||
|
|
||||||
|
`WraithForm`: 불가침 2 + 턴 종료 시 민첩 감소
|
||||||
|
|
||||||
|
## 다음 축
|
||||||
|
|
||||||
|
- 조건부 피해
|
||||||
|
- 카드 사용 트리거
|
||||||
|
- 비용/X코스트
|
||||||
|
- 드로우 연동 파워
|
||||||
|
|
||||||
84
docs/card-effect-fields.md
Normal file
84
docs/card-effect-fields.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Card Effect Fields
|
||||||
|
|
||||||
|
`data/cards.json`의 카드 효과를 공용 데이터 필드로 표현하는 기준 문서입니다.
|
||||||
|
|
||||||
|
## 피해 수치
|
||||||
|
|
||||||
|
- `damage`: 기본 피해
|
||||||
|
- `damagePerOtherHandCard`: 손패의 다른 카드 수만큼 피해 증감
|
||||||
|
- `damagePerAttackPlayedThisTurn`: 이번 턴에 사용한 공격 카드 수만큼 피해 증감
|
||||||
|
- `damagePerDiscardedThisTurn`: 이번 턴에 버린 카드 수만큼 피해 증감
|
||||||
|
- `damagePerSkillInHand`: 손패의 스킬 카드 수만큼 피해 증감
|
||||||
|
- `otherHandAtLeast`: 손패의 다른 카드가 이 수 이상일 때 조건 충족
|
||||||
|
- `bonusHitsWhenOtherHandAtLeast`: 조건 충족 시 추가 적중 수
|
||||||
|
|
||||||
|
## 방어/상태
|
||||||
|
|
||||||
|
- `block`: 방어도 획득
|
||||||
|
- `cardPlayedBlock`: 카드를 사용할 때마다 방어도 획득
|
||||||
|
- `hits`: 다단히트 횟수
|
||||||
|
- `aoe`: 모든 적 대상
|
||||||
|
- `pierce`: 방어도 무시
|
||||||
|
- `draw`: 즉시 드로우
|
||||||
|
- `drawUntilHandSize`: 손패가 지정 장수에 도달할 때까지 드로우
|
||||||
|
- `heal`: 즉시 회복
|
||||||
|
- `gainEnergy`: 즉시 에너지 획득
|
||||||
|
- `strength`: 힘 획득
|
||||||
|
- `dex`: 민첩 획득
|
||||||
|
- `thorns`: 가시 획득
|
||||||
|
- `selfVuln`: 자신에게 취약 부여
|
||||||
|
|
||||||
|
## 상태이상
|
||||||
|
|
||||||
|
- `weak`: 약화 부여
|
||||||
|
- `vuln`: 취약 부여
|
||||||
|
- `poison`: 중독 부여
|
||||||
|
|
||||||
|
`poison`은 적 턴 시작 시 피해를 주고 1 감소합니다.
|
||||||
|
|
||||||
|
## 드로우/버리기
|
||||||
|
|
||||||
|
- `discard`: 손패에서 지정 장수 버리기
|
||||||
|
- `discardAll`: 손패 전부 버리기
|
||||||
|
- `drawPerDiscarded`: 버린 카드 1장당 추가 드로우
|
||||||
|
- `addShiv`: 표창 생성
|
||||||
|
- `addShivPerDiscard`: 버린 장수만큼 표창 생성
|
||||||
|
- `sly`: 버려질 때 교활 발동
|
||||||
|
- `retain`: 턴 종료 시 해당 카드 보존
|
||||||
|
|
||||||
|
## 파워/턴 효과
|
||||||
|
|
||||||
|
- `powerEffect: "strengthPerTurn"`
|
||||||
|
- `powerEffect: "energyPerTurn"`
|
||||||
|
- `powerEffect: "blockPerTurn"`
|
||||||
|
- `powerEffect: "retainOne"`
|
||||||
|
- `turnStartShiv`: 턴 시작 시 표창 생성
|
||||||
|
- `turnStartDraw`: 턴 시작 시 추가 드로우
|
||||||
|
- `turnStartDiscard`: 턴 시작 시 카드 버리기
|
||||||
|
|
||||||
|
## 다음 턴 예약
|
||||||
|
|
||||||
|
- `nextTurnBlock`: 다음 턴 시작 시 방어도 획득
|
||||||
|
- `nextTurnDraw`: 다음 턴 시작 시 추가 드로우
|
||||||
|
- `nextTurnKeepBlock`: 다음 턴 시작 시 기존 방어도 유지
|
||||||
|
- `nextTurnAttackMultiplier`: 다음 턴 공격 피해 배수
|
||||||
|
- `nextTurnCopies`: 다음 턴에 손패에서 가져올 복사본 수
|
||||||
|
- `nextTurnSelectHandCard`: 현재 손패에서 카드 1장 선택
|
||||||
|
- `nextTurnSelectPrompt`: 선택 UI 문구
|
||||||
|
|
||||||
|
## 기타
|
||||||
|
|
||||||
|
- `innate`: 전투 시작 시 첫 손패에 우선 진입
|
||||||
|
- `playableWhenDrawPileEmpty`: 뽑을 카드 더미가 비었을 때만 사용 가능
|
||||||
|
- `exhaust`: 사용 후 소멸
|
||||||
|
- `unplayable`: 사용 불가
|
||||||
|
- `curse`: 저주 카드
|
||||||
|
- `token`: 토큰 카드
|
||||||
|
- `endTurnDamage`: 턴 종료 시 손패에 있으면 피해
|
||||||
|
|
||||||
|
## 사용 원칙
|
||||||
|
|
||||||
|
- 카드 전용 분기보다 공용 필드를 먼저 쓴다.
|
||||||
|
- 같은 효과는 같은 필드로 재사용한다.
|
||||||
|
- 새 카드가 같은 패턴이면 먼저 공용 필드를 추가한다.
|
||||||
|
|
||||||
31
docs/codex-workflow.md
Normal file
31
docs/codex-workflow.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Codex Workflow
|
||||||
|
|
||||||
|
이 저장소에서 작업할 때는 토큰과 변경량을 아끼는 쪽을 기본으로 둔다.
|
||||||
|
|
||||||
|
## 작업 원칙
|
||||||
|
|
||||||
|
- 이미 확인한 사실은 다시 읽지 않는다.
|
||||||
|
- 같은 내용을 통째로 지우고 새로 쓰지 않는다.
|
||||||
|
- 수정은 가능한 한 `apply_patch`로 섹션 단위만 한다.
|
||||||
|
- 문서는 전체 재작성보다 부분 수정으로 유지한다.
|
||||||
|
- 카드 구현은 한 번에 하나씩, 공용 필드 우선으로 넣는다.
|
||||||
|
- 새 기능은 `데이터 1곳 + 런타임 1곳 + 테스트 1곳` 순서로 맞춘다.
|
||||||
|
|
||||||
|
## 읽기 원칙
|
||||||
|
|
||||||
|
- 파일은 필요한 것만 읽는다.
|
||||||
|
- 비슷한 파일은 병렬로 한 번에 확인한다.
|
||||||
|
- 같은 정보를 여러 번 요약하지 않는다.
|
||||||
|
|
||||||
|
## 쓰기 원칙
|
||||||
|
|
||||||
|
- 공용으로 표현 가능한 효과는 카드 전용 분기로 만들지 않는다.
|
||||||
|
- 같은 의미의 효과는 같은 필드 이름을 쓴다.
|
||||||
|
- 문서는 카드별 상태표와 공용 필드 사전을 분리해서 유지한다.
|
||||||
|
|
||||||
|
## 응답 원칙
|
||||||
|
|
||||||
|
- 중간 보고는 짧게 한다.
|
||||||
|
- 바뀐 점과 남은 점만 말한다.
|
||||||
|
- 불필요한 재설명은 줄인다.
|
||||||
|
|
||||||
@@ -27,6 +27,16 @@ export function shuffle(arr, rng) {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareCombatDrawPile(deck, cards) {
|
||||||
|
const rest = [];
|
||||||
|
const innate = [];
|
||||||
|
for (const id of deck) {
|
||||||
|
if (cards[id]?.innate === true) innate.push(id);
|
||||||
|
else rest.push(id);
|
||||||
|
}
|
||||||
|
return rest.concat(innate);
|
||||||
|
}
|
||||||
|
|
||||||
// 공격 피해 공식 — Lua CalcPlayerAttack(힘·약화) + DealDamageToTarget(취약)과 동기화.
|
// 공격 피해 공식 — Lua CalcPlayerAttack(힘·약화) + DealDamageToTarget(취약)과 동기화.
|
||||||
// floor((base + str) * (weak>0 ? 0.75 : 1)) → floor(... * (vulnOnTarget>0 ? 1.5 : 1))
|
// floor((base + str) * (weak>0 ? 0.75 : 1)) → floor(... * (vulnOnTarget>0 ? 1.5 : 1))
|
||||||
// 보상 카드 등급 추첨 (Lua OfferReward 미러) — roll ∈ 1..100, normal 70 / unique 25 / legend 5
|
// 보상 카드 등급 추첨 (Lua OfferReward 미러) — roll ∈ 1..100, normal 70 / unique 25 / legend 5
|
||||||
@@ -70,11 +80,17 @@ export function loadData() {
|
|||||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDecks.warrior, monsters };
|
return { cards: cardsData.cards, starterDeck: cardsData.starterDecks.warrior, monsters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canPlayCardNow(card, ctx = {}) {
|
||||||
|
if (!card) return false;
|
||||||
|
if (card.playableWhenDrawPileEmpty === true && (ctx.drawPileCount || 0) > 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
||||||
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
|
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
|
||||||
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
|
// 손패에서 낼 카드 인덱스(-1=종료). 파워 우선(지속 가치) → 공격 → 스킬.
|
||||||
export function chooseAction(hand, cards, energy) {
|
export function chooseAction(hand, cards, energy, ctx = {}) {
|
||||||
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id] && cards[x.id].cost <= energy && !cards[x.id].unplayable);
|
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id] && cards[x.id].cost <= energy && !cards[x.id].unplayable && canPlayCardNow(cards[x.id], ctx));
|
||||||
const 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');
|
||||||
@@ -106,12 +122,17 @@ function bump(s, cost, dmg, blk) {
|
|||||||
export function simulateCombat(data, rng, stats) {
|
export function simulateCombat(data, rng, stats) {
|
||||||
const { cards, starterDeck, monsters } = data;
|
const { cards, starterDeck, monsters } = data;
|
||||||
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: PLAYER_HP };
|
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: PLAYER_HP };
|
||||||
let drawPile = shuffle(starterDeck, rng);
|
let drawPile = prepareCombatDrawPile(shuffle(starterDeck, rng), cards);
|
||||||
let discard = [];
|
let discard = [];
|
||||||
const exhaust = [];
|
const exhaust = [];
|
||||||
let hand = [];
|
let hand = [];
|
||||||
let pHp = PLAYER_HP, pBlock = 0;
|
let pHp = PLAYER_HP, pBlock = 0;
|
||||||
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0;
|
let pStr = 0, pDex = 0, pThorns = 0, pWeak = 0, pVuln = 0;
|
||||||
|
let nextTurnBlock = 0, nextTurnDraw = 0, nextTurnKeepBlock = false;
|
||||||
|
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
|
||||||
|
let nextTurnAddCards = [];
|
||||||
|
let turnAttackCardsPlayed = 0, turnDiscardedCards = 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: 0, weak: 0, vuln: 0, poison: 0,
|
||||||
@@ -137,19 +158,103 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
else hand.push(id);
|
else hand.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function addBlock(base) {
|
||||||
|
let amount = base || 0;
|
||||||
|
if (amount > 0) amount += pDex;
|
||||||
|
if (amount < 0) amount = 0;
|
||||||
|
pBlock += amount;
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
function discardForTurnStart(n) {
|
||||||
|
const cnt = Math.min(n, hand.length);
|
||||||
|
for (let i = 0; i < cnt; i++) {
|
||||||
|
const idx = hand
|
||||||
|
.map((id, k) => ({ id, k, card: cards[id] }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ac = a.card?.cost || 0;
|
||||||
|
const bc = b.card?.cost || 0;
|
||||||
|
if (ac !== bc) return ac - bc;
|
||||||
|
const ad = a.card?.damage || 0;
|
||||||
|
const bd = b.card?.damage || 0;
|
||||||
|
if (ad !== bd) return ad - bd;
|
||||||
|
return a.k - b.k;
|
||||||
|
})[0]?.k;
|
||||||
|
if (idx == null) break;
|
||||||
|
discardHandCard(idx, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function countOtherHandSkills(currentId) {
|
||||||
|
let n = 0;
|
||||||
|
let skippedSelf = false;
|
||||||
|
for (const id of hand) {
|
||||||
|
if (!skippedSelf && id === currentId) { skippedSelf = true; continue; }
|
||||||
|
if (cards[id]?.kind === 'Skill') n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function attackBaseForCard(id, c) {
|
||||||
|
let base = c.damage || 0;
|
||||||
|
const otherHand = Math.max(0, hand.length - 1);
|
||||||
|
if (c.damagePerOtherHandCard) base += otherHand * c.damagePerOtherHandCard;
|
||||||
|
if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn;
|
||||||
|
if (c.damagePerDiscardedThisTurn) base += turnDiscardedCards * c.damagePerDiscardedThisTurn;
|
||||||
|
if (c.damagePerSkillInHand) base += countOtherHandSkills(id) * c.damagePerSkillInHand;
|
||||||
|
if (base < 0) base = 0;
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
function queueNextTurnAddCard(id, n) {
|
||||||
|
if (!id || !n || n <= 0) return;
|
||||||
|
const entry = nextTurnAddCards.find((x) => x.cardId === id);
|
||||||
|
if (entry) entry.amount += n;
|
||||||
|
else nextTurnAddCards.push({ cardId: id, amount: n });
|
||||||
|
}
|
||||||
|
function queueNextTurnEffects(c) {
|
||||||
|
if (!c) return;
|
||||||
|
if (c.nextTurnBlock) nextTurnBlock += c.nextTurnBlock;
|
||||||
|
if (c.nextTurnDraw) nextTurnDraw += c.nextTurnDraw;
|
||||||
|
if (c.nextTurnKeepBlock === true) nextTurnKeepBlock = true;
|
||||||
|
if (c.nextTurnAttackMultiplier && c.nextTurnAttackMultiplier > 0) nextTurnAttackMultiplier *= c.nextTurnAttackMultiplier;
|
||||||
|
}
|
||||||
|
function queueSelectedReserve(c) {
|
||||||
|
if (!c?.nextTurnSelectHandCard || !c.nextTurnCopies || hand.length === 0) return;
|
||||||
|
const choice = hand
|
||||||
|
.map((id, i) => ({ id, i, card: cards[id] }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ak = a.card?.kind === 'Attack' ? 3 : a.card?.kind === 'Skill' ? 2 : 1;
|
||||||
|
const bk = b.card?.kind === 'Attack' ? 3 : b.card?.kind === 'Skill' ? 2 : 1;
|
||||||
|
if (bk !== ak) return bk - ak;
|
||||||
|
const ad = a.card?.damage || 0;
|
||||||
|
const bd = b.card?.damage || 0;
|
||||||
|
if (bd !== ad) return bd - ad;
|
||||||
|
return a.i - b.i;
|
||||||
|
})[0];
|
||||||
|
if (choice?.id) queueNextTurnAddCard(choice.id, c.nextTurnCopies);
|
||||||
|
}
|
||||||
const aliveList = () => mob.filter((m) => m.alive);
|
const aliveList = () => mob.filter((m) => m.alive);
|
||||||
|
function powerFieldTotal(field) {
|
||||||
|
let total = 0;
|
||||||
|
for (const pid of powers) {
|
||||||
|
const pc = cards[pid];
|
||||||
|
if (pc?.[field] != null) total += pc[field];
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
function resolveCardEffects(id, c, costSpent, recordStats = true) {
|
function resolveCardEffects(id, c, costSpent, recordStats = true) {
|
||||||
const alive = aliveList();
|
const alive = aliveList();
|
||||||
let dmg = 0;
|
let dmg = 0;
|
||||||
let blockGained = 0;
|
let blockGained = 0;
|
||||||
if (c.kind === 'Attack') {
|
if (c.kind === 'Attack') {
|
||||||
if (alive.length && c.damage) {
|
if (alive.length && c.damage) {
|
||||||
const target = chooseTarget(alive, calcAttack(c.damage || 0, pStr, pWeak, 0));
|
const baseDamage = attackBaseForCard(id, c);
|
||||||
|
const bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast)
|
||||||
|
? c.bonusHitsWhenOtherHandAtLeast : 0;
|
||||||
|
const hitN = (c.hits || 1) + bonusHits;
|
||||||
|
const preview = calcAttack(baseDamage || 0, pStr, pWeak, 0) * turnAttackMultiplier;
|
||||||
|
const target = chooseTarget(alive, preview);
|
||||||
if (c.weak) target.weak += c.weak;
|
if (c.weak) target.weak += c.weak;
|
||||||
if (c.vuln) target.vuln += c.vuln;
|
if (c.vuln) target.vuln += c.vuln;
|
||||||
const hitN = c.hits || 1;
|
|
||||||
let totalNv = 0;
|
let totalNv = 0;
|
||||||
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
|
for (let h = 0; h < hitN; h++) totalNv += calcAttack(baseDamage || 0, pStr, pWeak, 0) * turnAttackMultiplier;
|
||||||
dmg = totalNv;
|
dmg = totalNv;
|
||||||
if (c.aoe === true) {
|
if (c.aoe === true) {
|
||||||
for (const m2 of aliveList()) {
|
for (const m2 of aliveList()) {
|
||||||
@@ -170,11 +275,11 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (target.hp <= 0) target.alive = false;
|
if (target.hp <= 0) target.alive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
|
if (c.block) blockGained = addBlock(c.block);
|
||||||
} else if (c.kind === 'Power') {
|
} else if (c.kind === 'Power') {
|
||||||
if (recordStats) powers.push(id);
|
if (recordStats) powers.push(id);
|
||||||
} else {
|
} else {
|
||||||
if (c.block) { blockGained = Math.max(0, c.block + pDex); pBlock += blockGained; }
|
if (c.block) blockGained = addBlock(c.block);
|
||||||
if ((c.weak || c.vuln || c.poison) && alive.length) {
|
if ((c.weak || c.vuln || c.poison) && alive.length) {
|
||||||
const target = chooseTarget(alive, 0);
|
const target = chooseTarget(alive, 0);
|
||||||
if (c.weak) target.weak += c.weak;
|
if (c.weak) target.weak += c.weak;
|
||||||
@@ -187,7 +292,13 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (c.thorns) pThorns += c.thorns;
|
if (c.thorns) pThorns += c.thorns;
|
||||||
if (c.selfVuln) pVuln += c.selfVuln;
|
if (c.selfVuln) pVuln += c.selfVuln;
|
||||||
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
||||||
|
if (c.gainEnergy) energy += c.gainEnergy;
|
||||||
|
queueNextTurnEffects(c);
|
||||||
if (c.draw) draw(c.draw);
|
if (c.draw) draw(c.draw);
|
||||||
|
if (c.drawUntilHandSize) {
|
||||||
|
const need = c.drawUntilHandSize - Math.max(0, hand.length - 1);
|
||||||
|
if (need > 0) draw(need);
|
||||||
|
}
|
||||||
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
|
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
|
||||||
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
|
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
|
||||||
}
|
}
|
||||||
@@ -200,6 +311,7 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
const [id] = hand.splice(idx, 1);
|
const [id] = hand.splice(idx, 1);
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
discard.push(id);
|
discard.push(id);
|
||||||
|
turnDiscardedCards++;
|
||||||
if (trigger) triggerSly(id);
|
if (trigger) triggerSly(id);
|
||||||
}
|
}
|
||||||
function applyDiscardEffects(c) {
|
function applyDiscardEffects(c) {
|
||||||
@@ -212,13 +324,21 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
}
|
}
|
||||||
if (c.addShiv && (c.discard || c.discardAll === true)) addCardsToHand('Shiv', c.addShiv);
|
if (c.addShiv && (c.discard || c.discardAll === true)) addCardsToHand('Shiv', c.addShiv);
|
||||||
if (c.addShivPerDiscard === true) addCardsToHand('Shiv', discarded);
|
if (c.addShivPerDiscard === true) addCardsToHand('Shiv', discarded);
|
||||||
|
if (c.drawPerDiscarded) draw(discarded * c.drawPerDiscarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (turns < MAX_TURNS) {
|
while (turns < MAX_TURNS) {
|
||||||
turns++;
|
turns++;
|
||||||
|
turnAttackCardsPlayed = 0;
|
||||||
|
turnDiscardedCards = 0;
|
||||||
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
|
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
|
||||||
pBlock = 0;
|
if (nextTurnKeepBlock === true) nextTurnKeepBlock = false;
|
||||||
|
else pBlock = 0;
|
||||||
|
turnAttackMultiplier = nextTurnAttackMultiplier;
|
||||||
|
nextTurnAttackMultiplier = 1;
|
||||||
let energyBonus = 0;
|
let energyBonus = 0;
|
||||||
|
let powerTurnDraw = 0;
|
||||||
|
let powerTurnDiscard = 0;
|
||||||
for (const pid of powers) {
|
for (const pid of powers) {
|
||||||
const pc = cards[pid];
|
const pc = cards[pid];
|
||||||
if (!pc) continue;
|
if (!pc) continue;
|
||||||
@@ -226,17 +346,32 @@ 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;
|
||||||
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
|
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
|
||||||
|
if (pc.turnStartDraw) powerTurnDraw += pc.turnStartDraw;
|
||||||
|
if (pc.turnStartDiscard) powerTurnDiscard += pc.turnStartDiscard;
|
||||||
}
|
}
|
||||||
let energy = ENERGY + energyBonus; draw(HAND_SIZE);
|
if (nextTurnBlock > 0) { addBlock(nextTurnBlock); nextTurnBlock = 0; }
|
||||||
|
if (nextTurnAddCards.length) {
|
||||||
|
for (const entry of nextTurnAddCards) addCardsToHand(entry.cardId, entry.amount);
|
||||||
|
nextTurnAddCards = [];
|
||||||
|
}
|
||||||
|
energy = ENERGY + energyBonus;
|
||||||
|
const drawBonus = nextTurnDraw + powerTurnDraw;
|
||||||
|
nextTurnDraw = 0;
|
||||||
|
draw(HAND_SIZE + drawBonus);
|
||||||
|
if (powerTurnDiscard > 0) discardForTurnStart(powerTurnDiscard);
|
||||||
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);
|
const idx = chooseAction(hand, cards, energy, { drawPileCount: drawPile.length });
|
||||||
if (idx < 0) break;
|
if (idx < 0) break;
|
||||||
const id = hand[idx], c = cards[id];
|
const id = hand[idx], c = cards[id];
|
||||||
energy -= c.cost;
|
energy -= c.cost;
|
||||||
resolveCardEffects(id, c, c.cost);
|
resolveCardEffects(id, c, c.cost);
|
||||||
|
if (c.kind === 'Attack') turnAttackCardsPlayed++;
|
||||||
|
const playedBlock = powerFieldTotal('cardPlayedBlock');
|
||||||
|
if (playedBlock > 0) addBlock(playedBlock);
|
||||||
hand.splice(idx, 1);
|
hand.splice(idx, 1);
|
||||||
|
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);
|
||||||
applyDiscardEffects(c);
|
applyDiscardEffects(c);
|
||||||
|
|||||||
@@ -13,6 +13,85 @@ test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
|||||||
assert.equal(rarityForRoll(100), 'legend');
|
assert.equal(rarityForRoll(100), 'legend');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnBlock grants block on the following turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
GuardLater: { name: "예약 방어", cost: 0, kind: "Skill", nextTurnBlock: 4 },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["GuardLater", "Pass"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 99, intents: [{ kind: "Attack", value: 3 }, { kind: "Attack", value: 3 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, false);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 77);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnDraw draws extra cards next turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Setup: { name: "설치", cost: 0, kind: "Skill", nextTurnDraw: 2 },
|
||||||
|
Hit1: { name: "타격1", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Hit2: { name: "타격2", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass4: { name: "대기4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Hit1", "Hit2", "Pass1", "Pass2", "Pass3", "Pass4", "Setup"],
|
||||||
|
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, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnKeepBlock preserves current block", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
BlurLater: { name: "흐릿함", cost: 0, kind: "Skill", block: 5, nextTurnKeepBlock: true },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["BlurLater", "Pass"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 99, intents: [{ kind: "Attack", value: 3 }, { kind: "Attack", value: 3 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, false);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnAttackMultiplier boosts attacks next turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Prep: { name: "그림자 걸음", cost: 0, kind: "Skill", nextTurnAttackMultiplier: 2 },
|
||||||
|
Hit: { name: "타격", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Prep", "Pass", "Hit"],
|
||||||
|
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, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextTurnSelectHandCard queues selected copies for next turn", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Nightmare: { name: "악몽", cost: 0, kind: "Skill", nextTurnCopies: 3, nextTurnSelectHandCard: true },
|
||||||
|
Hit: { name: "타격", cost: 0, kind: "Attack", damage: 2 },
|
||||||
|
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Pass", "Nightmare", "Hit"],
|
||||||
|
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, 4);
|
||||||
|
});
|
||||||
|
|
||||||
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
||||||
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
||||||
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
||||||
@@ -461,3 +540,147 @@ test("simulateCombat: addShiv creates shuriken cards in hand", () => {
|
|||||||
assert.equal(r.win, true);
|
assert.equal(r.win, true);
|
||||||
assert.equal(r.turns, 1);
|
assert.equal(r.turns, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: innate cards are drawn into the opening hand first", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Backstab: { name: "배신", cost: 0, kind: "Attack", damage: 11, innate: true, exhaust: true },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass4: { name: "대기4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass5: { name: "대기5", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Pass1", "Pass2", "Pass3", "Pass4", "Pass5", "Backstab"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 11, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: GrandFinale waits until draw pile is empty", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Finale: { name: "피날레", cost: 0, kind: "Attack", damage: 60, aoe: true, playableWhenDrawPileEmpty: true },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass4: { name: "대기4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass5: { name: "대기5", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Pass1", "Pass2", "Pass3", "Pass4", "Pass5", "Finale"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 60, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, false);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: turnStartDraw and turnStartDiscard powers resolve at turn start", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Tool: { name: "작업 도구", cost: 0, kind: "Power", turnStartDraw: 1, turnStartDiscard: 1 },
|
||||||
|
Hit1: { name: "타격1", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Hit2: { name: "타격2", cost: 0, kind: "Attack", damage: 3 },
|
||||||
|
Pass1: { name: "대기1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass2: { name: "대기2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Pass3: { name: "대기3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Tool", "Pass1", "Pass2", "Pass3", "Hit1", "Hit2"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chooseAction: GrandFinale is blocked until draw pile is empty", () => {
|
||||||
|
const cards = {
|
||||||
|
Finale: { name: "피날레", cost: 0, kind: "Attack", damage: 60, playableWhenDrawPileEmpty: true },
|
||||||
|
Defend: { name: "방어", cost: 1, kind: "Skill", block: 5 },
|
||||||
|
};
|
||||||
|
assert.equal(chooseAction(["Finale", "Defend"], cards, 3, { drawPileCount: 1 }), 1);
|
||||||
|
assert.equal(chooseAction(["Finale"], cards, 3, { drawPileCount: 0 }), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerAttackPlayedThisTurn scales Finisher", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Hit: { name: "타격", cost: 0, kind: "Attack", damage: 6 },
|
||||||
|
Finisher: { name: "마무리", cost: 0, kind: "Attack", damage: 0, damagePerAttackPlayedThisTurn: 6 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Hit", "Finisher"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 12, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerOtherHandCard and damagePerSkillInHand are applied", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Precise: { name: "정밀", cost: 0, kind: "Attack", damage: 13, damagePerOtherHandCard: -2 },
|
||||||
|
Flechettes: { name: "프레췌", cost: 0, kind: "Attack", damage: 0, damagePerSkillInHand: 5 },
|
||||||
|
Skill1: { name: "스킬1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Skill2: { name: "스킬2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Blank: { name: "공백", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Skill1", "Skill2", "Blank", "Precise", "Flechettes"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 21, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: damagePerDiscardedThisTurn and bonusHitsWhenOtherHandAtLeast work", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Toss: { name: "버리기", cost: 0, kind: "Skill", discard: 1 },
|
||||||
|
Memento: { name: "메멘토", cost: 0, kind: "Attack", damage: 9, damagePerDiscardedThisTurn: 4 },
|
||||||
|
Follow: { name: "완수", cost: 0, kind: "Attack", damage: 7, otherHandAtLeast: 2, bonusHitsWhenOtherHandAtLeast: 1 },
|
||||||
|
Blank1: { name: "공백1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Blank2: { name: "공백2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Toss", "Memento", "Follow", "Blank1", "Blank2"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 27, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: gainEnergy, drawUntilHandSize, and drawPerDiscarded are applied", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Adrenaline: { name: "Adrenaline", cost: 0, kind: "Skill", gainEnergy: 1, draw: 2, exhaust: true },
|
||||||
|
Expertise: { name: "Expertise", cost: 1, kind: "Skill", drawUntilHandSize: 6 },
|
||||||
|
Gamble: { name: "Gamble", cost: 0, kind: "Skill", discardAll: true, drawPerDiscarded: 1, exhaust: true },
|
||||||
|
Tactician: { name: "Tactician", cost: 99, kind: "Skill", gainEnergy: 1, sly: true },
|
||||||
|
Hit1: { name: "Hit1", cost: 1, kind: "Attack", damage: 6 },
|
||||||
|
Hit2: { name: "Hit2", cost: 1, kind: "Attack", damage: 6 },
|
||||||
|
Hit3: { name: "Hit3", cost: 1, kind: "Attack", damage: 6 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Adrenaline", "Expertise", "Gamble", "Tactician", "Hit1", "Hit2", "Hit3"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 18, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.turns, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: cardPlayedBlock grants block whenever a card is played", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
After: { name: "Afterimage", cost: 1, kind: "Power", cardPlayedBlock: 1 },
|
||||||
|
Hit: { name: "Hit", cost: 1, kind: "Attack", damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["After", "Hit", "Hit", "Hit", "Hit"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 1 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
|
|||||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||||
|
|
||||||
export const combatMethods = [
|
export const combatMethods = [
|
||||||
|
method('CanPlayCardNow', `if c == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if c.playableWhenDrawPileEmpty == true and self.DrawPile ~= nil and #self.DrawPile > 0 then
|
||||||
|
self:Toast("뽑을 카드 더미가 비어 있을 때만 사용할 수 있습니다.")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||||
method('PlayCard', `if self:IsDiscardSelecting() == true then
|
method('PlayCard', `if self:IsDiscardSelecting() == true then
|
||||||
self:SelectDiscardSlot(slot)
|
self:SelectDiscardSlot(slot)
|
||||||
return
|
return
|
||||||
@@ -11,6 +19,10 @@ if self:IsRetainSelecting() == true then
|
|||||||
self:SelectRetainSlot(slot)
|
self:SelectRetainSlot(slot)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:IsReserveSelecting() == true then
|
||||||
|
self:SelectReserveSlot(slot)
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -29,12 +41,21 @@ if c.unplayable == true then
|
|||||||
self:Toast("사용할 수 없는 카드입니다")
|
self:Toast("사용할 수 없는 카드입니다")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:CanPlayCardNow(c) ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.Energy < c.cost then
|
if self.Energy < c.cost then
|
||||||
self:Toast("에너지가 부족합니다")
|
self:Toast("에너지가 부족합니다")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.Energy = self.Energy - c.cost
|
self.Energy = self.Energy - c.cost
|
||||||
self:ResolveCardEffects(cardId, c, false)
|
self:ResolveCardEffects(cardId, slot, c, false)
|
||||||
|
if c.kind == "Attack" then
|
||||||
|
self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1
|
||||||
|
end
|
||||||
|
if self:HasPowerField("cardPlayedBlock") == true then
|
||||||
|
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
|
||||||
|
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
|
||||||
@@ -48,6 +69,9 @@ self:RenderCombat()
|
|||||||
if self:BeginDiscardSelection(c) == true then
|
if self:BeginDiscardSelection(c) == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:BeginReserveSelection(c) == true then
|
||||||
|
return
|
||||||
|
end
|
||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
@@ -56,6 +80,8 @@ self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0,
|
|||||||
self:SelectDiscardSlot(slot)
|
self:SelectDiscardSlot(slot)
|
||||||
elseif self:IsRetainSelecting() == true then
|
elseif self:IsRetainSelecting() == true then
|
||||||
self:SelectRetainSlot(slot)
|
self:SelectRetainSlot(slot)
|
||||||
|
elseif self:IsReserveSelecting() == true then
|
||||||
|
self:SelectReserveSlot(slot)
|
||||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||||
method('FindMonsterAtTouch', `local best = 0
|
method('FindMonsterAtTouch', `local best = 0
|
||||||
local bestDist = 200
|
local bestDist = 200
|
||||||
@@ -146,6 +172,10 @@ if self:IsRetainSelecting() == true then
|
|||||||
self:SelectRetainSlot(slot)
|
self:SelectRetainSlot(slot)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:IsReserveSelecting() == true then
|
||||||
|
self:SelectReserveSlot(slot)
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -427,6 +457,17 @@ self.DiscardSelectTotal = 0
|
|||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
self.RetainSelectActive = false
|
self.RetainSelectActive = false
|
||||||
|
self.TurnAttackCardsPlayed = 0
|
||||||
|
self.TurnDiscardedCards = 0
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnBlock = 0
|
||||||
|
self.NextTurnDraw = 0
|
||||||
|
self.NextTurnKeepBlock = false
|
||||||
|
self.NextTurnAttackMultiplier = 1
|
||||||
|
self.TurnAttackMultiplier = 1
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()`),
|
self:RenderPiles()`),
|
||||||
|
|||||||
@@ -225,14 +225,27 @@ for i = 1, 3 do
|
|||||||
end`),
|
end`),
|
||||||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||||||
self.RetainSelectActive = false
|
self.RetainSelectActive = false
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.TurnAttackCardsPlayed = 0
|
||||||
|
self.TurnDiscardedCards = 0
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
self.Energy = self.MaxEnergy
|
self.Energy = self.MaxEnergy
|
||||||
self:ApplyRelics("turnStart")
|
self:ApplyRelics("turnStart")
|
||||||
self.PlayerBlock = 0
|
if self.NextTurnKeepBlock == true then
|
||||||
|
self.NextTurnKeepBlock = false
|
||||||
|
else
|
||||||
|
self.PlayerBlock = 0
|
||||||
|
end
|
||||||
if self.ClayBlockNext > 0 then
|
if self.ClayBlockNext > 0 then
|
||||||
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
|
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
|
||||||
self.ClayBlockNext = 0
|
self.ClayBlockNext = 0
|
||||||
end
|
end
|
||||||
|
self.TurnAttackMultiplier = self.NextTurnAttackMultiplier or 1
|
||||||
|
self.NextTurnAttackMultiplier = 1
|
||||||
|
local powerTurnDraw = 0
|
||||||
|
local powerTurnDiscard = 0
|
||||||
if self.PlayerPowers ~= nil then
|
if self.PlayerPowers ~= nil then
|
||||||
for i = 1, #self.PlayerPowers do
|
for i = 1, #self.PlayerPowers do
|
||||||
local pc = self.Cards[self.PlayerPowers[i]]
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
@@ -247,12 +260,59 @@ if self.PlayerPowers ~= nil then
|
|||||||
if pc.turnStartShiv ~= nil then
|
if pc.turnStartShiv ~= nil then
|
||||||
self:AddCardsToHand("Shiv", pc.turnStartShiv)
|
self:AddCardsToHand("Shiv", pc.turnStartShiv)
|
||||||
end
|
end
|
||||||
|
if pc.turnStartDraw ~= nil then
|
||||||
|
powerTurnDraw = powerTurnDraw + pc.turnStartDraw
|
||||||
|
end
|
||||||
|
if pc.turnStartDiscard ~= nil then
|
||||||
|
powerTurnDiscard = powerTurnDiscard + pc.turnStartDiscard
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:DrawCards(5)
|
if self.NextTurnBlock ~= nil and self.NextTurnBlock > 0 then
|
||||||
|
self:AddCardBlock(self.NextTurnBlock)
|
||||||
|
self.NextTurnBlock = 0
|
||||||
|
end
|
||||||
|
if self.NextTurnAddCards ~= nil then
|
||||||
|
for i = 1, #self.NextTurnAddCards do
|
||||||
|
local entry = self.NextTurnAddCards[i]
|
||||||
|
if entry ~= nil and entry.cardId ~= nil and entry.amount ~= nil and entry.amount > 0 then
|
||||||
|
self:AddCardsToHand(entry.cardId, entry.amount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
|
end
|
||||||
|
local drawN = 5 + (self.NextTurnDraw or 0) + powerTurnDraw
|
||||||
|
self.NextTurnDraw = 0
|
||||||
|
self:DrawCards(drawN)
|
||||||
self:RenderHand(true)
|
self:RenderHand(true)
|
||||||
|
self:RenderCombat()
|
||||||
|
if powerTurnDiscard > 0 then
|
||||||
|
self:BeginDiscardSelection({ discard = math.min(powerTurnDiscard, #self.Hand) })
|
||||||
|
return
|
||||||
|
end
|
||||||
self:RenderCombat()`),
|
self:RenderCombat()`),
|
||||||
|
method('PrepareCombatDrawPile', `if self.DrawPile == nil or self.Cards == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local rest = {}
|
||||||
|
local innate = {}
|
||||||
|
for i = 1, #self.DrawPile do
|
||||||
|
local cardId = self.DrawPile[i]
|
||||||
|
local c = self.Cards[cardId]
|
||||||
|
if c ~= nil and c.innate == true then
|
||||||
|
table.insert(innate, cardId)
|
||||||
|
else
|
||||||
|
table.insert(rest, cardId)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.DrawPile = {}
|
||||||
|
for i = 1, #rest do
|
||||||
|
table.insert(self.DrawPile, rest[i])
|
||||||
|
end
|
||||||
|
for i = 1, #innate do
|
||||||
|
table.insert(self.DrawPile, innate[i])
|
||||||
|
end`, []),
|
||||||
method('HasPowerEffect', `if self.PlayerPowers == nil then
|
method('HasPowerEffect', `if self.PlayerPowers == nil then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -263,6 +323,27 @@ for i = 1, #self.PlayerPowers do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'effect' }], 0, 'boolean'),
|
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'effect' }], 0, 'boolean'),
|
||||||
|
method('HasPowerField', `if self.PlayerPowers == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil and pc[field] ~= nil and pc[field] ~= 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'field' }], 0, 'boolean'),
|
||||||
|
method('AddPowerFieldTotal', `local total = 0
|
||||||
|
if self.PlayerPowers == nil then
|
||||||
|
return total
|
||||||
|
end
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil and pc[field] ~= nil then
|
||||||
|
total = total + pc[field]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return total`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'field' }], 0, 'number'),
|
||||||
method('ShouldOfferRetain', `if self:HasPowerEffect("retainOne") ~= true then
|
method('ShouldOfferRetain', `if self:HasPowerEffect("retainOne") ~= true then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -291,12 +372,19 @@ if self:IsRetainSelecting() == true then
|
|||||||
self:FinishPlayerTurn(0)
|
self:FinishPlayerTurn(0)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self:IsReserveSelecting() == true then
|
||||||
|
self:Toast("예약할 카드를 먼저 선택하세요")
|
||||||
|
return
|
||||||
|
end
|
||||||
if self:ShouldOfferRetain() == true then
|
if self:ShouldOfferRetain() == true then
|
||||||
self:BeginRetainSelection()
|
self:BeginRetainSelection()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:FinishPlayerTurn(0)`),
|
self:FinishPlayerTurn(0)`),
|
||||||
method('FinishPlayerTurn', `self.RetainSelectActive = false
|
method('FinishPlayerTurn', `self.RetainSelectActive = false
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
local burn = 0
|
local burn = 0
|
||||||
for bi = 1, #self.Hand do
|
for bi = 1, #self.Hand do
|
||||||
|
|||||||
@@ -274,6 +274,44 @@ if amount < 0 then
|
|||||||
end
|
end
|
||||||
self.PlayerBlock = self.PlayerBlock + amount
|
self.PlayerBlock = self.PlayerBlock + amount
|
||||||
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||||
|
method('CountOtherHandSkills', `if self.Hand == nil then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
local n = 0
|
||||||
|
for i = 1, #self.Hand do
|
||||||
|
if i ~= slot then
|
||||||
|
local hc = self.Cards[self.Hand[i]]
|
||||||
|
if hc ~= nil and hc.kind == "Skill" then
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return n`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'),
|
||||||
|
method('AttackBaseForCard', `local base2 = c.damage or 0
|
||||||
|
local otherHand = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
otherHand = #self.Hand - 1
|
||||||
|
if otherHand < 0 then otherHand = 0 end
|
||||||
|
end
|
||||||
|
if c.damagePerOtherHandCard ~= nil then
|
||||||
|
base2 = base2 + otherHand * c.damagePerOtherHandCard
|
||||||
|
end
|
||||||
|
if c.damagePerAttackPlayedThisTurn ~= nil then
|
||||||
|
base2 = base2 + (self.TurnAttackCardsPlayed or 0) * c.damagePerAttackPlayedThisTurn
|
||||||
|
end
|
||||||
|
if c.damagePerDiscardedThisTurn ~= nil then
|
||||||
|
base2 = base2 + (self.TurnDiscardedCards or 0) * c.damagePerDiscardedThisTurn
|
||||||
|
end
|
||||||
|
if c.damagePerSkillInHand ~= nil then
|
||||||
|
base2 = base2 + self:CountOtherHandSkills(slot) * c.damagePerSkillInHand
|
||||||
|
end
|
||||||
|
if base2 < 0 then
|
||||||
|
base2 = 0
|
||||||
|
end
|
||||||
|
return base2`, [
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
||||||
|
], 0, 'number'),
|
||||||
method('CalcPlayerAttack', `local base2 = base
|
method('CalcPlayerAttack', `local base2 = base
|
||||||
self.FightAttackCount = self.FightAttackCount + 1
|
self.FightAttackCount = self.FightAttackCount + 1
|
||||||
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
|
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
|
||||||
@@ -286,6 +324,9 @@ end
|
|||||||
if self.PlayerWeak > 0 then
|
if self.PlayerWeak > 0 then
|
||||||
dmg = math.floor(dmg * 0.75)
|
dmg = math.floor(dmg * 0.75)
|
||||||
end
|
end
|
||||||
|
if self.TurnAttackMultiplier ~= nil and self.TurnAttackMultiplier > 1 then
|
||||||
|
dmg = dmg * self.TurnAttackMultiplier
|
||||||
|
end
|
||||||
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
|
if dmg > 0 and dmg < 5 and self:HasRelic("boot") then
|
||||||
dmg = 5
|
dmg = 5
|
||||||
end
|
end
|
||||||
@@ -293,16 +334,60 @@ if dmg < 0 then
|
|||||||
dmg = 0
|
dmg = 0
|
||||||
end
|
end
|
||||||
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
|
||||||
|
method('QueueNextTurnAddCard', `if cardId == nil or cardId == "" or amount == nil or amount <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self.NextTurnAddCards == nil then
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
|
end
|
||||||
|
for i = 1, #self.NextTurnAddCards do
|
||||||
|
local entry = self.NextTurnAddCards[i]
|
||||||
|
if entry ~= nil and entry.cardId == cardId then
|
||||||
|
entry.amount = (entry.amount or 0) + amount
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(self.NextTurnAddCards, { cardId = cardId, amount = amount })`, [
|
||||||
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
|
]),
|
||||||
|
method('QueueNextTurnEffects', `if c == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if c.nextTurnBlock ~= nil then
|
||||||
|
self.NextTurnBlock = (self.NextTurnBlock or 0) + c.nextTurnBlock
|
||||||
|
end
|
||||||
|
if c.nextTurnDraw ~= nil then
|
||||||
|
self.NextTurnDraw = (self.NextTurnDraw or 0) + c.nextTurnDraw
|
||||||
|
end
|
||||||
|
if c.nextTurnKeepBlock == true then
|
||||||
|
self.NextTurnKeepBlock = true
|
||||||
|
end
|
||||||
|
if c.nextTurnAttackMultiplier ~= nil and c.nextTurnAttackMultiplier > 0 then
|
||||||
|
local cur = self.NextTurnAttackMultiplier or 1
|
||||||
|
self.NextTurnAttackMultiplier = cur * c.nextTurnAttackMultiplier
|
||||||
|
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }]),
|
||||||
method('ResolveCardEffects', `if c == nil then
|
method('ResolveCardEffects', `if c == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if c.kind == "Attack" then
|
if c.kind == "Attack" then
|
||||||
if c.damage ~= nil then
|
if c.damage ~= nil then
|
||||||
self:PlayerAttackMotion()
|
self:PlayerAttackMotion()
|
||||||
|
local baseDmg = self:AttackBaseForCard(slot, c)
|
||||||
local total = 0
|
local total = 0
|
||||||
local hitN = c.hits or 1
|
local hitN = c.hits or 1
|
||||||
|
if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then
|
||||||
|
local otherHand = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
otherHand = #self.Hand - 1
|
||||||
|
if otherHand < 0 then otherHand = 0 end
|
||||||
|
end
|
||||||
|
if otherHand >= c.otherHandAtLeast then
|
||||||
|
hitN = hitN + c.bonusHitsWhenOtherHandAtLeast
|
||||||
|
end
|
||||||
|
end
|
||||||
for h = 1, hitN do
|
for h = 1, hitN do
|
||||||
total = total + self:CalcPlayerAttack(c.damage)
|
total = total + self:CalcPlayerAttack(baseDmg)
|
||||||
end
|
end
|
||||||
if c.aoe == true then
|
if c.aoe == true then
|
||||||
self:PlayAoeFx(c.fx or c.image, total)
|
self:PlayAoeFx(c.fx or c.image, total)
|
||||||
@@ -340,6 +425,10 @@ end
|
|||||||
if c.heal ~= nil then
|
if c.heal ~= nil then
|
||||||
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
||||||
end
|
end
|
||||||
|
if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then
|
||||||
|
self.Energy = self.Energy + c.gainEnergy
|
||||||
|
end
|
||||||
|
self:QueueNextTurnEffects(c)
|
||||||
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
|
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil 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
|
||||||
@@ -361,10 +450,24 @@ end
|
|||||||
if c.draw ~= nil then
|
if c.draw ~= nil then
|
||||||
self:DrawCards(c.draw, true)
|
self:DrawCards(c.draw, true)
|
||||||
end
|
end
|
||||||
|
if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then
|
||||||
|
local currentHand = 0
|
||||||
|
if self.Hand ~= nil then
|
||||||
|
currentHand = #self.Hand
|
||||||
|
if slot ~= nil and slot > 0 and self.Hand[slot] == cardId then
|
||||||
|
currentHand = currentHand - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local need = c.drawUntilHandSize - currentHand
|
||||||
|
if need > 0 then
|
||||||
|
self:DrawCards(need, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
||||||
self:AddCardsToHand("Shiv", c.addShiv)
|
self:AddCardsToHand("Shiv", c.addShiv)
|
||||||
end`, [
|
end`, [
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' },
|
||||||
]),
|
]),
|
||||||
@@ -373,7 +476,7 @@ if c == nil or c.sly ~= true then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:Toast("교활 발동: " .. c.name)
|
self:Toast("교활 발동: " .. c.name)
|
||||||
self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
|
self:ResolveCardEffects(cardId, 0, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
|
||||||
method('DiscardHandCard', `if self.Hand == nil then
|
method('DiscardHandCard', `if self.Hand == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -384,6 +487,7 @@ end
|
|||||||
local startX = self:GetHandSlotX(slot)
|
local startX = self:GetHandSlotX(slot)
|
||||||
table.remove(self.Hand, slot)
|
table.remove(self.Hand, slot)
|
||||||
table.insert(self.DiscardPile, cardId)
|
table.insert(self.DiscardPile, cardId)
|
||||||
|
self.TurnDiscardedCards = (self.TurnDiscardedCards or 0) + 1
|
||||||
if triggerSly == true then
|
if triggerSly == true then
|
||||||
self:TriggerSly(cardId)
|
self:TriggerSly(cardId)
|
||||||
end
|
end
|
||||||
@@ -396,6 +500,7 @@ end`, [
|
|||||||
]),
|
]),
|
||||||
method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'),
|
method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'),
|
||||||
method('IsRetainSelecting', `return self.RetainSelectActive == true`, [], 0, 'boolean'),
|
method('IsRetainSelecting', `return self.RetainSelectActive == true`, [], 0, 'boolean'),
|
||||||
|
method('IsReserveSelecting', `return self.ReserveSelectActive == true`, [], 0, 'boolean'),
|
||||||
method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/DiscardPrompt")
|
method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/DiscardPrompt")
|
||||||
if e == nil then
|
if e == nil then
|
||||||
return
|
return
|
||||||
@@ -407,6 +512,13 @@ if self:IsDiscardSelecting() == true then
|
|||||||
elseif self:IsRetainSelecting() == true then
|
elseif self:IsRetainSelecting() == true then
|
||||||
self:SetText("/ui/RunUIGroup/CombatHud/DiscardPrompt", "보존할 카드 선택 (턴 종료: 건너뛰기)")
|
self:SetText("/ui/RunUIGroup/CombatHud/DiscardPrompt", "보존할 카드 선택 (턴 종료: 건너뛰기)")
|
||||||
e.Enable = true
|
e.Enable = true
|
||||||
|
elseif self:IsReserveSelecting() == true then
|
||||||
|
local msg = self.NextTurnSelectPrompt or ""
|
||||||
|
if msg == "" then
|
||||||
|
msg = "다음 턴에 예약할 카드를 선택하세요"
|
||||||
|
end
|
||||||
|
self:SetText("/ui/RunUIGroup/CombatHud/DiscardPrompt", msg)
|
||||||
|
e.Enable = true
|
||||||
else
|
else
|
||||||
e.Enable = false
|
e.Enable = false
|
||||||
end`),
|
end`),
|
||||||
@@ -427,15 +539,56 @@ self.DiscardSelectRemaining = n
|
|||||||
self.DiscardSelectTotal = n
|
self.DiscardSelectTotal = n
|
||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
|
self.DiscardPostDraw = 0
|
||||||
|
self.DiscardDrawPerPick = 0
|
||||||
if c.addShiv ~= nil then
|
if c.addShiv ~= nil then
|
||||||
self.DiscardPostShiv = c.addShiv
|
self.DiscardPostShiv = c.addShiv
|
||||||
end
|
end
|
||||||
if c.addShivPerDiscard == true then
|
if c.addShivPerDiscard == true then
|
||||||
self.DiscardShivPerPick = 1
|
self.DiscardShivPerPick = 1
|
||||||
end
|
end
|
||||||
|
if c.drawPerDiscarded ~= nil and c.drawPerDiscarded > 0 then
|
||||||
|
self.DiscardDrawPerPick = c.drawPerDiscarded
|
||||||
|
end
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
self:Toast("버릴 카드를 선택하세요")
|
self:Toast("버릴 카드를 선택하세요")
|
||||||
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||||
|
method('BeginReserveSelection', `if c == nil or c.nextTurnSelectHandCard ~= true or c.nextTurnCopies == nil or c.nextTurnCopies <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.Hand == nil or #self.Hand <= 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.ReserveSelectActive = true
|
||||||
|
self.NextTurnSelectCopies = c.nextTurnCopies
|
||||||
|
self.NextTurnSelectPrompt = c.nextTurnSelectPrompt or ""
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
|
self:Toast("예약할 카드를 선택하세요")
|
||||||
|
self:RenderHand(false)
|
||||||
|
return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'),
|
||||||
|
method('SelectReserveSlot', `if self:IsReserveSelecting() ~= true then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.Hand == nil or self.Hand[slot] == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local cardId = self.Hand[slot]
|
||||||
|
local amount = self.NextTurnSelectCopies or 0
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self:UpdateDiscardPrompt()
|
||||||
|
if amount > 0 and cardId ~= nil then
|
||||||
|
self:QueueNextTurnAddCard(cardId, amount)
|
||||||
|
local label = cardId
|
||||||
|
if self.Cards[cardId] ~= nil and self.Cards[cardId].name ~= nil then
|
||||||
|
label = self.Cards[cardId].name
|
||||||
|
end
|
||||||
|
self:Toast("다음 턴 예약: " .. label .. " " .. self:FormatNumber(amount) .. "장")
|
||||||
|
end
|
||||||
|
self:RenderPiles()
|
||||||
|
self:RenderCombat()
|
||||||
|
return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'),
|
||||||
method('SelectRetainSlot', `if self:IsRetainSelecting() ~= true then
|
method('SelectRetainSlot', `if self:IsRetainSelecting() ~= true then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -457,6 +610,7 @@ for i = 1, n do
|
|||||||
table.insert(startXs, self:GetHandSlotX(i))
|
table.insert(startXs, self:GetHandSlotX(i))
|
||||||
table.insert(slots, i)
|
table.insert(slots, i)
|
||||||
table.insert(self.DiscardPile, cardId)
|
table.insert(self.DiscardPile, cardId)
|
||||||
|
self.TurnDiscardedCards = (self.TurnDiscardedCards or 0) + 1
|
||||||
end
|
end
|
||||||
self.Hand = {}
|
self.Hand = {}
|
||||||
local shivCount = 0
|
local shivCount = 0
|
||||||
@@ -466,6 +620,8 @@ self.DiscardSelectRemaining = 0
|
|||||||
self.DiscardSelectTotal = 0
|
self.DiscardSelectTotal = 0
|
||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
|
self.DiscardPostDraw = 0
|
||||||
|
self.DiscardDrawPerPick = 0
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
self:AnimateDiscardCards(cardIds, startXs, slots)
|
self:AnimateDiscardCards(cardIds, startXs, slots)
|
||||||
for i = 1, #cardIds do
|
for i = 1, #cardIds do
|
||||||
@@ -480,6 +636,9 @@ _TimerService:SetTimerOnce(function()
|
|||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
end
|
end
|
||||||
|
if c.drawPerDiscarded ~= nil and c.drawPerDiscarded > 0 then
|
||||||
|
self:DrawCards(n * c.drawPerDiscarded, true)
|
||||||
|
end
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()
|
self:CheckCombatEnd()
|
||||||
end, 0.22)
|
end, 0.22)
|
||||||
@@ -487,8 +646,11 @@ return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes:
|
|||||||
method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0
|
method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0
|
||||||
self.DiscardSelectTotal = 0
|
self.DiscardSelectTotal = 0
|
||||||
local shivCount = self.DiscardPostShiv or 0
|
local shivCount = self.DiscardPostShiv or 0
|
||||||
|
local drawCount = self.DiscardPostDraw or 0
|
||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
|
self.DiscardPostDraw = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
|
self.DiscardDrawPerPick = 0
|
||||||
self:UpdateDiscardPrompt()
|
self:UpdateDiscardPrompt()
|
||||||
local finish = function()
|
local finish = function()
|
||||||
if shivCount > 0 then
|
if shivCount > 0 then
|
||||||
@@ -497,6 +659,9 @@ local finish = function()
|
|||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
end
|
end
|
||||||
|
if drawCount > 0 then
|
||||||
|
self:DrawCards(drawCount, true)
|
||||||
|
end
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()
|
self:CheckCombatEnd()
|
||||||
end
|
end
|
||||||
@@ -516,6 +681,9 @@ self:DiscardHandCard(slot, true, true)
|
|||||||
if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then
|
if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then
|
||||||
self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick
|
self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick
|
||||||
end
|
end
|
||||||
|
if discarded ~= nil and self.DiscardDrawPerPick ~= nil and self.DiscardDrawPerPick > 0 then
|
||||||
|
self.DiscardPostDraw = (self.DiscardPostDraw or 0) + self.DiscardDrawPerPick
|
||||||
|
end
|
||||||
self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1
|
self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1
|
||||||
if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then
|
if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then
|
||||||
self:FinishDiscardSelection(true)
|
self:FinishDiscardSelection(true)
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ self.PlayerWeak = 0
|
|||||||
self.PlayerVuln = 0
|
self.PlayerVuln = 0
|
||||||
self.PlayerPowers = {}
|
self.PlayerPowers = {}
|
||||||
self.FightAttackCount = 0
|
self.FightAttackCount = 0
|
||||||
|
self.TurnAttackCardsPlayed = 0
|
||||||
|
self.TurnDiscardedCards = 0
|
||||||
self.DmgPopSeq = 0
|
self.DmgPopSeq = 0
|
||||||
self.FirstHpLossDone = false
|
self.FirstHpLossDone = false
|
||||||
self.ClayBlockNext = 0
|
self.ClayBlockNext = 0
|
||||||
@@ -82,6 +84,15 @@ self.DiscardSelectTotal = 0
|
|||||||
self.DiscardPostShiv = 0
|
self.DiscardPostShiv = 0
|
||||||
self.DiscardShivPerPick = 0
|
self.DiscardShivPerPick = 0
|
||||||
self.RetainSelectActive = false
|
self.RetainSelectActive = false
|
||||||
|
self.ReserveSelectActive = false
|
||||||
|
self.NextTurnBlock = 0
|
||||||
|
self.NextTurnDraw = 0
|
||||||
|
self.NextTurnKeepBlock = false
|
||||||
|
self.NextTurnAttackMultiplier = 1
|
||||||
|
self.TurnAttackMultiplier = 1
|
||||||
|
self.NextTurnSelectPrompt = ""
|
||||||
|
self.NextTurnSelectCopies = 0
|
||||||
|
self.NextTurnAddCards = {}
|
||||||
self.CombatOver = false
|
self.CombatOver = false
|
||||||
self.DiscardPile = {}
|
self.DiscardPile = {}
|
||||||
self.ExhaustPile = {}
|
self.ExhaustPile = {}
|
||||||
@@ -92,6 +103,7 @@ for i = 1, #self.RunDeck do
|
|||||||
self.DrawPile[i] = self.RunDeck[i]
|
self.DrawPile[i] = self.RunDeck[i]
|
||||||
end
|
end
|
||||||
self:Shuffle(self.DrawPile)
|
self:Shuffle(self.DrawPile)
|
||||||
|
self:PrepareCombatDrawPile()
|
||||||
self:BuildMonsters()
|
self:BuildMonsters()
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:StartPlayerTurn()
|
self:StartPlayerTurn()
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ function writeCodeblocks() {
|
|||||||
prop('string', 'ShopPotion', '""'),
|
prop('string', 'ShopPotion', '""'),
|
||||||
prop('boolean', 'ShopPotionBought', 'false'),
|
prop('boolean', 'ShopPotionBought', 'false'),
|
||||||
prop('number', 'FightAttackCount', '0'),
|
prop('number', 'FightAttackCount', '0'),
|
||||||
|
prop('number', 'TurnAttackCardsPlayed', '0'),
|
||||||
|
prop('number', 'TurnDiscardedCards', '0'),
|
||||||
prop('boolean', 'FirstHpLossDone', 'false'),
|
prop('boolean', 'FirstHpLossDone', 'false'),
|
||||||
prop('number', 'ClayBlockNext', '0'),
|
prop('number', 'ClayBlockNext', '0'),
|
||||||
prop('number', 'PotionMenuSlot', '0'),
|
prop('number', 'PotionMenuSlot', '0'),
|
||||||
@@ -131,6 +133,15 @@ function writeCodeblocks() {
|
|||||||
prop('number', 'DiscardPostShiv', '0'),
|
prop('number', 'DiscardPostShiv', '0'),
|
||||||
prop('number', 'DiscardShivPerPick', '0'),
|
prop('number', 'DiscardShivPerPick', '0'),
|
||||||
prop('boolean', 'RetainSelectActive', 'false'),
|
prop('boolean', 'RetainSelectActive', 'false'),
|
||||||
|
prop('boolean', 'ReserveSelectActive', 'false'),
|
||||||
|
prop('number', 'NextTurnBlock', '0'),
|
||||||
|
prop('number', 'NextTurnDraw', '0'),
|
||||||
|
prop('boolean', 'NextTurnKeepBlock', 'false'),
|
||||||
|
prop('number', 'NextTurnAttackMultiplier', '1'),
|
||||||
|
prop('number', 'TurnAttackMultiplier', '1'),
|
||||||
|
prop('string', 'NextTurnSelectPrompt', '""'),
|
||||||
|
prop('number', 'NextTurnSelectCopies', '0'),
|
||||||
|
prop('any', 'NextTurnAddCards'),
|
||||||
], [
|
], [
|
||||||
...bootMethods,
|
...bootMethods,
|
||||||
...stateMethods,
|
...stateMethods,
|
||||||
|
|||||||
@@ -158,10 +158,17 @@ function luaCardsTable(cards) {
|
|||||||
const lines = Object.entries(cards).map(([id, c]) => {
|
const lines = Object.entries(cards).map(([id, c]) => {
|
||||||
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
|
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
|
||||||
if (c.damage != null) fields.push(`damage = ${c.damage}`);
|
if (c.damage != null) fields.push(`damage = ${c.damage}`);
|
||||||
|
if (c.damagePerOtherHandCard != null) fields.push(`damagePerOtherHandCard = ${c.damagePerOtherHandCard}`);
|
||||||
|
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`);
|
||||||
|
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`);
|
||||||
|
if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${c.damagePerSkillInHand}`);
|
||||||
|
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`);
|
||||||
|
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`);
|
||||||
if (c.block != null) fields.push(`block = ${c.block}`);
|
if (c.block != null) fields.push(`block = ${c.block}`);
|
||||||
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}`);
|
||||||
|
if (c.cardPlayedBlock != null) fields.push(`cardPlayedBlock = ${c.cardPlayedBlock}`);
|
||||||
if (c.weak != null) fields.push(`weak = ${c.weak}`);
|
if (c.weak != null) fields.push(`weak = ${c.weak}`);
|
||||||
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
|
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
|
||||||
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
|
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
|
||||||
@@ -173,13 +180,27 @@ function luaCardsTable(cards) {
|
|||||||
if (c.pierce === true) fields.push('pierce = true');
|
if (c.pierce === true) fields.push('pierce = true');
|
||||||
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
||||||
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
||||||
|
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
|
||||||
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
||||||
|
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
|
||||||
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
||||||
if (c.discard != null) fields.push(`discard = ${c.discard}`);
|
if (c.discard != null) fields.push(`discard = ${c.discard}`);
|
||||||
if (c.discardAll === true) fields.push('discardAll = true');
|
if (c.discardAll === true) fields.push('discardAll = true');
|
||||||
|
if (c.drawPerDiscarded != null) fields.push(`drawPerDiscarded = ${c.drawPerDiscarded}`);
|
||||||
if (c.addShiv != null) fields.push(`addShiv = ${c.addShiv}`);
|
if (c.addShiv != null) fields.push(`addShiv = ${c.addShiv}`);
|
||||||
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`);
|
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`);
|
||||||
|
if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${c.turnStartDraw}`);
|
||||||
|
if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${c.turnStartDiscard}`);
|
||||||
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
|
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
|
||||||
|
if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${c.nextTurnBlock}`);
|
||||||
|
if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`);
|
||||||
|
if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true');
|
||||||
|
if (c.nextTurnAttackMultiplier != null) fields.push(`nextTurnAttackMultiplier = ${c.nextTurnAttackMultiplier}`);
|
||||||
|
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
|
||||||
|
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
|
||||||
|
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
|
||||||
|
if (c.innate === true) fields.push('innate = 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');
|
||||||
if (c.retain === true) fields.push('retain = true');
|
if (c.retain === true) fields.push('retain = true');
|
||||||
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) fields.push('exhaust = true');
|
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) fields.push('exhaust = true');
|
||||||
|
|||||||
Reference in New Issue
Block a user