enjine 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -1
- data/SUPPORTED.md +12 -9
- data/docs/MECHANICS_COVERAGE.md +1 -1
- data/docs/guides/card-data-format.md +23 -4
- data/docs/plans/dsl-gaps.md +606 -0
- data/docs/plans/standard.md +3 -0
- data/docs/reference/abilities.md +33 -7
- data/docs/reference/costs.md +11 -1
- data/docs/reference/decisions.md +1 -1
- data/docs/reference/effects.md +30 -9
- data/docs/reference/triggers.md +3 -2
- data/lib/enjine/actions/special_action.rb +4 -2
- data/lib/enjine/engine/casting/alt_casting.rb +18 -2
- data/lib/enjine/engine/casting/casting.rb +3 -3
- data/lib/enjine/engine/casting/costs.rb +5 -1
- data/lib/enjine/engine/casting/permissions.rb +1 -1
- data/lib/enjine/engine/characteristics.rb +0 -13
- data/lib/enjine/engine/defined_power_toughness.rb +30 -0
- data/lib/enjine/engine/mana_sources.rb +8 -1
- data/lib/enjine/engine/resolution.rb +1 -0
- data/lib/enjine/engine/triggers.rb +1 -0
- data/lib/enjine/engine/turn_history.rb +25 -1
- data/lib/enjine/engine/zones.rb +16 -6
- data/lib/enjine/mechanics/abilities/additional_triggers.rb +23 -11
- data/lib/enjine/mechanics/abilities/cost_changes.rb +20 -7
- data/lib/enjine/mechanics/abilities/mana_ability.rb +21 -6
- data/lib/enjine/mechanics/abilities/stack_abilities.rb +13 -9
- data/lib/enjine/mechanics/abilities/static.rb +13 -7
- data/lib/enjine/mechanics/costs/sacrifice.rb +28 -2
- data/lib/enjine/mechanics/effects/become.rb +8 -4
- data/lib/enjine/mechanics/effects/choose_cards.rb +9 -7
- data/lib/enjine/mechanics/effects/delayed_triggers.rb +2 -1
- data/lib/enjine/mechanics/effects/search.rb +15 -8
- data/lib/enjine/mechanics/keywords/plot.rb +79 -0
- data/lib/enjine/mechanics/triggers/casting.rb +8 -6
- data/lib/enjine/mechanics/values.rb +82 -14
- data/lib/enjine/version.rb +1 -1
- metadata +4 -1
data/docs/plans/standard.md
CHANGED
|
@@ -107,6 +107,9 @@ rotation is announced, drop the rotating sets from this table.
|
|
|
107
107
|
|
|
108
108
|
## Mechanics gaps
|
|
109
109
|
|
|
110
|
+
For the gaps behind deckfinder's DSL FAIL and PARTIAL cards, ranked by how many
|
|
111
|
+
cards each blocks, with a quick-wins list, see [dsl-gaps.md](dsl-gaps.md).
|
|
112
|
+
|
|
110
113
|
Census taken 2026-11-09 against `SUPPORTED.md` at this commit. It lists what can't
|
|
111
114
|
yet be written in the card DSL. Card lists came from the Scryfall API (main sets,
|
|
112
115
|
`unique=cards`, no bonus sheets) and were checked outside the repository; nothing
|
data/docs/reference/abilities.md
CHANGED
|
@@ -82,18 +82,25 @@ Rules: CR 305.2, CR 305.2a, CR 305.2b, CR 604.1
|
|
|
82
82
|
|
|
83
83
|
## additional_triggers
|
|
84
84
|
|
|
85
|
-
While this permanent is on the battlefield, a triggered ability of a permanent its controller controls
|
|
85
|
+
While this permanent is on the battlefield, a triggered ability of a permanent its controller controls triggers an additional time if it triggers because of `cause` (you_gain_life: its controller gaining life) and/or its permanent matches `of` (a filter: "triggered abilities of Allies you control", {"creature": {"other": true, "subtypes": "chosen"}}). At least one of them.
|
|
86
86
|
|
|
87
|
-
Rules: CR 603.2d, CR 119.9
|
|
87
|
+
Rules: CR 603.2d, CR 119.9, CR 607.2d
|
|
88
88
|
|
|
89
89
|
| Parameter | Description |
|
|
90
90
|
| --- | --- |
|
|
91
91
|
| `cause` | you_gain_life |
|
|
92
|
+
| `of` | filter of the permanent whose ability triggers |
|
|
92
93
|
|
|
93
94
|
```json
|
|
94
95
|
{
|
|
95
96
|
"additional_triggers": {
|
|
96
|
-
"
|
|
97
|
+
"of": {
|
|
98
|
+
"creature": {
|
|
99
|
+
"subtypes": [
|
|
100
|
+
"ally"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
```
|
|
@@ -397,13 +404,13 @@ Rules: CR 717.2a, CR 717.2b, CR 717.2d, CR 717.3, CR 602.5d
|
|
|
397
404
|
|
|
398
405
|
## cost_modifier
|
|
399
406
|
|
|
400
|
-
While on the battlefield: spells cast by `caster` (you, opponent or any; default you) whose card types match `spells` ({"any_of": [types], "not": [types]}, default all spells) cost `reduce` less or `increase` more generic mana. `from` / `not_from`: only spells cast from (not from) that zone (hand | graveyard | exile | library | outside).
|
|
407
|
+
While on the battlefield: spells cast by `caster` (you, opponent or any; default you) whose card types match `spells` ({"any_of": [types], "not": [types]}, default all spells; `subtypes` and `colors` narrow it: "Dragon spells", "red spells") cost `reduce` less or `increase` more generic mana. `from` / `not_from`: only spells cast from (not from) that zone (hand | graveyard | exile | library | outside).
|
|
401
408
|
|
|
402
409
|
Rules: CR 601.2f, CR 118.7a, CR 604.1, CR 601.2a
|
|
403
410
|
|
|
404
411
|
| Parameter | Description |
|
|
405
412
|
| --- | --- |
|
|
406
|
-
| `spells` | {"any_of": [types], "not": [types]} (optional) |
|
|
413
|
+
| `spells` | {"any_of": [types], "not": [types], "subtypes": [subtypes], "colors": [colors]} (optional) |
|
|
407
414
|
| `caster` | you | opponent | any |
|
|
408
415
|
| `reduce` | amount |
|
|
409
416
|
| `increase` | amount |
|
|
@@ -1080,7 +1087,7 @@ Rules: CR 606.3, CR 606.4, CR 606.5, CR 602.2b
|
|
|
1080
1087
|
|
|
1081
1088
|
| Parameter | Description |
|
|
1082
1089
|
| --- | --- |
|
|
1083
|
-
| `cost` | integer, e.g. 1 for +1 or -2 |
|
|
1090
|
+
| `cost` | integer, e.g. 1 for +1 or -2; or "-X" (X is announced, at most its loyalty) |
|
|
1084
1091
|
| `targets` | list of target filters |
|
|
1085
1092
|
| `effects` | list of effects |
|
|
1086
1093
|
|
|
@@ -1107,7 +1114,7 @@ Rules: CR 606.3, CR 606.4, CR 606.5, CR 602.2b
|
|
|
1107
1114
|
|
|
1108
1115
|
## mana
|
|
1109
1116
|
|
|
1110
|
-
An activated mana ability: pay the cost, add the mana to your mana pool immediately. It does not use the stack and can be activated whenever you have priority. `produce`: the fixed mana (`amount` times). Or `color`: `amount` mana (default 1) of one color you choose as you activate it (`"any_one"`) or in any combination of colors (`"any"`); `colors` limits the choice. `"chosen"`: of the color chosen as this entered (choose_color). `spend_only` restricts how the mana can be spent. `only_if`: "Activate only if ...".
|
|
1117
|
+
An activated mana ability: pay the cost, add the mana to your mana pool immediately. It does not use the stack and can be activated whenever you have priority. `produce`: the fixed mana (`amount` times). Or `color`: `amount` mana (default 1) of one color you choose as you activate it (`"any_one"`) or in any combination of colors (`"any"`); `colors` limits the choice. `"chosen"`: of the color chosen as this entered (choose_color). `spend_only` restricts how the mana can be spent. `only_if`: "Activate only if ...". `limit: "once_each_turn"`: "Activate only once each turn."
|
|
1111
1118
|
|
|
1112
1119
|
Rules: CR 605.1a, CR 605.3a, CR 605.3b, CR 106.1a, CR 106.4, CR 106.6, CR 607.2d, CR 602.5b
|
|
1113
1120
|
|
|
@@ -1120,6 +1127,7 @@ Rules: CR 605.1a, CR 605.3a, CR 605.3b, CR 106.1a, CR 106.4, CR 106.6, CR 607.2d
|
|
|
1120
1127
|
| `colors` | list of colors to choose from (default all five) |
|
|
1121
1128
|
| `spend_only` | {"spells": card spec, "abilities": card spec, "from" | "not_from": zone} |
|
|
1122
1129
|
| `only_if` | condition: it can be activated only while it holds (CR 602.5b) |
|
|
1130
|
+
| `limit` | "once_each_turn" (CR 602.5b) |
|
|
1123
1131
|
|
|
1124
1132
|
```json
|
|
1125
1133
|
{
|
|
@@ -1264,6 +1272,24 @@ Rules: CR 702.11c, CR 115.4, CR 508.1c, CR 402.2, CR 514.1, CR 615.1, CR 615.1a,
|
|
|
1264
1272
|
}
|
|
1265
1273
|
```
|
|
1266
1274
|
|
|
1275
|
+
## plot
|
|
1276
|
+
|
|
1277
|
+
Plot `cost`: during your main phase, while the stack is empty, you may pay `cost` and exile this card from your hand (special action `{type: "special_action"}`). It becomes plotted: on a later turn you may cast it from exile without paying its mana cost, during your main phase while the stack is empty (cast option `via: "plotted"`).
|
|
1278
|
+
|
|
1279
|
+
Rules: CR 702.170a, CR 702.170b, CR 702.170d, CR 116.2k
|
|
1280
|
+
|
|
1281
|
+
| Parameter | Description |
|
|
1282
|
+
| --- | --- |
|
|
1283
|
+
| `cost` | mana cost string |
|
|
1284
|
+
|
|
1285
|
+
```json
|
|
1286
|
+
{
|
|
1287
|
+
"plot": {
|
|
1288
|
+
"cost": "{1}{R}"
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1267
1293
|
## prevent_damage
|
|
1268
1294
|
|
|
1269
1295
|
While this permanent is on the battlefield, all damage from sources in `by` to recipients in `to` (damage scopes as in damage_replacement, both optional; `combat`: combat | noncombat) is prevented. "To and by this creature" is two of them. `then`: effects right after it prevents damage ("prevent that damage and put that many +1/+1 counters on him"), reading {"triggering": "amount"} (the damage prevented), "object" (the source) and "player" (its controller).
|
data/docs/reference/costs.md
CHANGED
|
@@ -133,6 +133,16 @@ Rules: CR 406.1, CR 601.2h, CR 602.2b, CR 118.3
|
|
|
133
133
|
}
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## exile_self
|
|
137
|
+
|
|
138
|
+
Exile the permanent the ability belongs to ("Exile this enchantment: ...").
|
|
139
|
+
|
|
140
|
+
Rules: CR 406.1, CR 602.2b, CR 118.3
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
"exile_self"
|
|
144
|
+
```
|
|
145
|
+
|
|
136
146
|
## exile_self_from_graveyard
|
|
137
147
|
|
|
138
148
|
Exile this card from your graveyard. An ability with this cost is activated from your graveyard.
|
|
@@ -256,7 +266,7 @@ Rules: CR 701.21a, CR 602.2b, CR 118.3
|
|
|
256
266
|
|
|
257
267
|
## sacrifice_permanent
|
|
258
268
|
|
|
259
|
-
Sacrifice `count` permanents you control matching `filter` (`other: true`: not the source). Chosen while casting or activating, sacrificed as it becomes cast; the sacrificed permanent's last known types and subtypes are remembered (condition `sacrificed`).
|
|
269
|
+
Sacrifice `count` permanents you control matching `filter` (`other: true`: not the source). Chosen while casting or activating, sacrificed as it becomes cast; the sacrificed permanent's last known types and subtypes are remembered (condition `sacrificed`), and its power, toughness and mana value (amount `sacrificed`).
|
|
260
270
|
|
|
261
271
|
Rules: CR 701.21a, CR 601.2h, CR 602.2b, CR 118.3, CR 608.2h
|
|
262
272
|
|
data/docs/reference/decisions.md
CHANGED
|
@@ -99,4 +99,4 @@ Untap step: `{type: "untap", object: id}` or `{type: "keep_tapped", object: id}`
|
|
|
99
99
|
- `cast` — `{type: "cast", object: id}` — start casting a card from your hand (CR 601.2). `{type: "cast", object: id, via: method}` casts a card from another zone (graveyard, exile, the top of your library) by the permission or alternative cost `via` names, e.g. beam me up (CR 601.3, 702.196). A card outside the game is named `"outside:<card id>"` (CR 400.11b). `{type: "cast", object: id, face: 1}` casts another face of a card in your hand: a modal double-faced card's back face, an Adventure or an Omen (CR 712.8b, 716.3, 721.3). Offered only when timing allows, targets exist and enough mana is available.
|
|
100
100
|
- `pass` — `{type: "pass"}` — pass priority. When all players pass in succession with an empty stack, the step ends (CR 117.4).
|
|
101
101
|
- `play_land` — `{type: "play_land", object: id}` — special action: your main phase, empty stack, one land per turn plus additional land plays (CR 305.1, 305.2). With `via`: a land from another zone that an effect lets you play. With `face: 1`: a modal double-faced card played with its land back face up (CR 712.9).
|
|
102
|
-
- `special_action` — `{type: "special_action", object: id, ability: ref}` — a special action of a card in your hand: foretell (during your turn, pay {2} and exile it face down, CR 116.2h) or suspend (when you could begin to cast it, pay its suspend cost and exile it with time counters, CR 116.2f).
|
|
102
|
+
- `special_action` — `{type: "special_action", object: id, ability: ref}` — a special action of a card in your hand: foretell (during your turn, pay {2} and exile it face down, CR 116.2h) or suspend (when you could begin to cast it, pay its suspend cost and exile it with time counters, CR 116.2f) or plot (in your main phase with an empty stack, pay its plot cost and exile it, CR 116.2k).
|
data/docs/reference/effects.md
CHANGED
|
@@ -224,7 +224,7 @@ Rules: CR 701.3a, CR 701.3b, CR 701.3c, CR 301.5c
|
|
|
224
224
|
|
|
225
225
|
## become
|
|
226
226
|
|
|
227
|
-
Each referenced permanent becomes what the parameters say, for `duration` (end_of_turn, default; indefinite: as long as it stays on the battlefield; while_source_on_battlefield: for as long as this effect's source remains on the battlefield; while_you_control_source: for as long as you control it): it has the card `types`, `supertypes` and `subtypes` in addition to its other types (layer 4, "becomes an artifact creature"), its creature types are exactly `creature_types` (layer 4), its colors are `colors` (layer 5), with `loses_abilities: true` it loses all the abilities it had (layer 6; abilities granted later, keyword counters put on it later and this effect's own `keywords` and `abilities` stay, CR 613.7), it has the `keywords` and `abilities` (layer 6: triggered, activated, mana, play_from or additional_land_plays abilities) and its base power and toughness are `base_power_toughness` (layer 7b). A permanent that becomes a creature has its printed power and toughness (a Vehicle's), or 0/0.
|
|
227
|
+
Each referenced permanent becomes what the parameters say, for `duration` (end_of_turn, default; until_your_next_turn: until your next turn begins; indefinite: as long as it stays on the battlefield; while_source_on_battlefield: for as long as this effect's source remains on the battlefield; while_you_control_source: for as long as you control it): it has the card `types`, `supertypes` and `subtypes` in addition to its other types (layer 4, "becomes an artifact creature"), its creature types are exactly `creature_types` (layer 4), its colors are `colors` (layer 5), with `loses_abilities: true` it loses all the abilities it had (layer 6; abilities granted later, keyword counters put on it later and this effect's own `keywords` and `abilities` stay, CR 613.7), it has the `keywords` and `abilities` (layer 6: triggered, activated, mana, play_from or additional_land_plays abilities) and its base power and toughness are `base_power_toughness` (layer 7b). A permanent that becomes a creature has its printed power and toughness (a Vehicle's), or 0/0.
|
|
228
228
|
|
|
229
229
|
Rules: CR 611.2a, CR 611.2b, CR 611.2c, CR 613.1d, CR 613.1e, CR 613.1f, CR 613.4b, CR 613.7b, CR 205.1a, CR 205.1b, CR 301.7b, CR 514.2
|
|
230
230
|
|
|
@@ -239,7 +239,7 @@ Rules: CR 611.2a, CR 611.2b, CR 611.2c, CR 613.1d, CR 613.1e, CR 613.1f, CR 613.
|
|
|
239
239
|
| `keywords` | list of keywords it gains |
|
|
240
240
|
| `base_power_toughness` | {"power": n, "toughness": n} |
|
|
241
241
|
| `abilities` | list of abilities it gains |
|
|
242
|
-
| `duration` | end_of_turn | indefinite | while_source_on_battlefield | while_you_control_source |
|
|
242
|
+
| `duration` | end_of_turn | until_your_next_turn | indefinite | while_source_on_battlefield | while_you_control_source |
|
|
243
243
|
| `to` | selector |
|
|
244
244
|
|
|
245
245
|
```json
|
|
@@ -461,9 +461,9 @@ Rules: CR 205.2a, CR 607.2d, CR 608.2c
|
|
|
461
461
|
|
|
462
462
|
## choose_cards
|
|
463
463
|
|
|
464
|
-
You choose `count` (default 1; "any": any number) cards matching `for` from `from`: a selector (e.g. {"result": 0}, the cards an earlier effect milled) or a player's zone, {"hand": selector} / {"graveyard": selector}. `optional`: you may choose fewer. Then they are put into `put` (`tapped` onto the battlefield, under your control; exiled cards `link`ed to the source). `look`: you look at the whole zone first; `reveal`: "chosen" reveals what you chose, "all" the whole set. `then` runs if a card was chosen, `otherwise` if none was ("if you can't"). `random: true`: the cards are picked at random (the game's random number generator) instead of chosen.
|
|
464
|
+
You choose `count` (default 1; "any": any number) cards matching `for` from `from`: a selector (e.g. {"result": 0}, the cards an earlier effect milled) or a player's zone, {"hand": selector} / {"graveyard": selector}. `optional`: you may choose fewer. Then they are put into `put` (`discard`: from a hand, its owner discards them; `tapped` onto the battlefield, under your control; exiled cards `link`ed to the source). `look`: you look at the whole zone first; `reveal`: "chosen" reveals what you chose, "all" the whole set. `then` runs if a card was chosen, `otherwise` if none was ("if you can't"). `random: true`: the cards are picked at random (the game's random number generator) instead of chosen.
|
|
465
465
|
|
|
466
|
-
Rules: CR 701.20a, CR 701.20e, CR 400.7j, CR 608.2c, CR 607.2a, CR 118.12, CR 701.9b
|
|
466
|
+
Rules: CR 701.20a, CR 701.20e, CR 400.7j, CR 608.2c, CR 607.2a, CR 118.12, CR 701.9a, CR 701.9b
|
|
467
467
|
|
|
468
468
|
| Parameter | Description |
|
|
469
469
|
| --- | --- |
|
|
@@ -471,7 +471,7 @@ Rules: CR 701.20a, CR 701.20e, CR 400.7j, CR 608.2c, CR 607.2a, CR 118.12, CR 70
|
|
|
471
471
|
| `for` | card spec |
|
|
472
472
|
| `count` | integer or "any" |
|
|
473
473
|
| `optional` | boolean |
|
|
474
|
-
| `put` | hand | graveyard | exile | battlefield | library_top | library_bottom |
|
|
474
|
+
| `put` | hand | graveyard | exile | battlefield | library_top | library_bottom | discard |
|
|
475
475
|
| `tapped` | boolean |
|
|
476
476
|
| `link` | boolean |
|
|
477
477
|
| `look` | boolean |
|
|
@@ -1361,14 +1361,15 @@ Rules: CR 119.3
|
|
|
1361
1361
|
|
|
1362
1362
|
## grant
|
|
1363
1363
|
|
|
1364
|
-
Each referenced permanent (or spell on the stack) gains the `keywords` until end of turn.
|
|
1364
|
+
Each referenced permanent (or spell on the stack) gains the `keywords` until end of turn (`duration: "until_your_next_turn"`: until its controller's next turn begins).
|
|
1365
1365
|
|
|
1366
|
-
Rules: CR 611.2a, CR 611.2c, CR 613.1f, CR 514.2
|
|
1366
|
+
Rules: CR 611.2a, CR 611.2c, CR 613.1f, CR 514.2, CR 500.1
|
|
1367
1367
|
|
|
1368
1368
|
| Parameter | Description |
|
|
1369
1369
|
| --- | --- |
|
|
1370
1370
|
| `keywords` | list of keywords |
|
|
1371
1371
|
| `to` | selector |
|
|
1372
|
+
| `duration` | end_of_turn | until_your_next_turn |
|
|
1372
1373
|
|
|
1373
1374
|
```json
|
|
1374
1375
|
{
|
|
@@ -1657,6 +1658,26 @@ Rules: CR 702.11c, CR 115.4, CR 508.1c, CR 402.2, CR 514.1, CR 615.1, CR 615.1a,
|
|
|
1657
1658
|
}
|
|
1658
1659
|
```
|
|
1659
1660
|
|
|
1661
|
+
## plot
|
|
1662
|
+
|
|
1663
|
+
Each referenced card or spell is exiled (if it isn't already) and becomes plotted: its owner may cast it from exile without paying its mana cost during their main phase while the stack is empty, on any later turn (cast option `via: "plotted"`).
|
|
1664
|
+
|
|
1665
|
+
Rules: CR 702.170c, CR 702.170d, CR 406.1
|
|
1666
|
+
|
|
1667
|
+
| Parameter | Description |
|
|
1668
|
+
| --- | --- |
|
|
1669
|
+
| `to` | selector (a spell, a card, or an exiled card such as {"result": 0}) |
|
|
1670
|
+
|
|
1671
|
+
```json
|
|
1672
|
+
{
|
|
1673
|
+
"plot": {
|
|
1674
|
+
"to": {
|
|
1675
|
+
"target": 0
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1660
1681
|
## prepare
|
|
1661
1682
|
|
|
1662
1683
|
Each referenced permanent (default: this one) becomes prepared, if it has a prepare spell and isn't prepared already: its controller creates a copy of the prepare spell in exile and may cast that copy while the permanent stays prepared. Casting it unprepares the permanent.
|
|
@@ -2054,7 +2075,7 @@ Rules: CR 701.22a
|
|
|
2054
2075
|
|
|
2055
2076
|
## search_library
|
|
2056
2077
|
|
|
2057
|
-
Search your library for up to `count` cards matching `for`, then put them into your hand (or onto the battlefield, tapped if `tapped`, with `counters
|
|
2078
|
+
Search your library for up to `count` cards matching `for`, then put them into your hand (or onto the battlefield, tapped if `tapped`, with `counters`; or into your graveyard) and shuffle. `put: library_top`: shuffle, then put the found card (one) on top. Computed bounds in `for` are worked out as the effect resolves. With a stated quality you may find fewer (or none); a search for just "a card" must find one if it can. `reveal` shows the found cards to every player. `searcher` names another player who searches their own library (e.g. {"controller_of": {"target": 0}}); `may: true` lets the searcher decline (then nothing is shuffled). `different_names: true`: the cards found must all have different names ("basic land cards with different names").
|
|
2058
2079
|
|
|
2059
2080
|
Rules: CR 701.23a, CR 701.23b, CR 701.23d, CR 701.23e, CR 701.20a, CR 701.24a, CR 701.24b, CR 118.12, CR 110.2a
|
|
2060
2081
|
|
|
@@ -2062,7 +2083,7 @@ Rules: CR 701.23a, CR 701.23b, CR 701.23d, CR 701.23e, CR 701.20a, CR 701.24a, C
|
|
|
2062
2083
|
| --- | --- |
|
|
2063
2084
|
| `for` | card spec: "card" (default), {"types"|"subtypes"|"supertypes": [...], "name": "..."} or {"any_of": [spec, ...]} |
|
|
2064
2085
|
| `count` | integer (default 1), "any" (any number), "x" or an amount ("up to that many") |
|
|
2065
|
-
| `put` | "hand" (default) or "
|
|
2086
|
+
| `put` | "hand" (default), "battlefield", "graveyard" or "library_top" (count 1) |
|
|
2066
2087
|
| `tapped` | true: the cards enter tapped (with put: battlefield) |
|
|
2067
2088
|
| `counters` | {counter name: n}: the cards enter with these counters (with put: battlefield) |
|
|
2068
2089
|
| `reveal` | true: reveal the found cards |
|
data/docs/reference/triggers.md
CHANGED
|
@@ -27,9 +27,9 @@ Rules: CR 603.2, CR 603.3b, CR 508.1, CR 707.10
|
|
|
27
27
|
|
|
28
28
|
## activates_ability
|
|
29
29
|
|
|
30
|
-
A player (`controller`: you (default) | opponent | any) activates an ability that isn't a mana ability. `source`: optional filter on the source's card; `name: "chosen"`: only a source with the name chosen as this permanent entered. `exhaust: true`: only an exhaust ability ("whenever you activate an exhaust ability", CR 702.177). `boast: true`: only a boast ability ("whenever a creature you control boasts", CR 702.142b).
|
|
30
|
+
A player (`controller`: you (default) | opponent | any) activates an ability that isn't a mana ability. `source`: optional filter on the source's card; `name: "chosen"`: only a source with the name chosen as this permanent entered. `exhaust: true`: only an exhaust ability ("whenever you activate an exhaust ability", CR 702.177). `boast: true`: only a boast ability ("whenever a creature you control boasts", CR 702.142b). `loyalty: true`: only a loyalty ability (CR 606).
|
|
31
31
|
|
|
32
|
-
Rules: CR 602.2, CR 605.1a, CR 603.2, CR 201.4, CR 702.177a, CR 702.142b
|
|
32
|
+
Rules: CR 602.2, CR 605.1a, CR 603.2, CR 201.4, CR 702.177a, CR 702.142b, CR 606.1
|
|
33
33
|
|
|
34
34
|
| Parameter | Description |
|
|
35
35
|
| --- | --- |
|
|
@@ -38,6 +38,7 @@ Rules: CR 602.2, CR 605.1a, CR 603.2, CR 201.4, CR 702.177a, CR 702.142b
|
|
|
38
38
|
| `name` | "chosen" |
|
|
39
39
|
| `exhaust` | true: only exhaust abilities |
|
|
40
40
|
| `boast` | true: only boast abilities |
|
|
41
|
+
| `loyalty` | true: only loyalty abilities |
|
|
41
42
|
|
|
42
43
|
```json
|
|
43
44
|
{
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Enjine
|
|
4
4
|
module Actions
|
|
5
|
-
# CR 116.2f, 116.2h: special actions that pay a cost and exile a card from your hand (foretell,
|
|
5
|
+
# CR 116.2f, 116.2h, 116.2k: special actions that pay a cost and exile a card from your hand (foretell,
|
|
6
|
+
# suspend, plot).
|
|
6
7
|
# The card's ability names the cost, when the action is allowed and what it does; the cost is paid in a
|
|
7
8
|
# "pay" frame (Engine::Payment#begin_payment!), which can be cancelled, and the player gets priority
|
|
8
9
|
# again afterwards (CR 116.3).
|
|
@@ -10,7 +11,8 @@ module Enjine
|
|
|
10
11
|
def self.doc
|
|
11
12
|
'`{type: "special_action", object: id, ability: ref}` — a special action of a card in your hand: foretell ' \
|
|
12
13
|
"(during your turn, pay {2} and exile it face down, CR 116.2h) or suspend (when you could begin to cast it, " \
|
|
13
|
-
"pay its suspend cost and exile it with time counters, CR 116.2f)
|
|
14
|
+
"pay its suspend cost and exile it with time counters, CR 116.2f) or plot (in your main phase with an empty " \
|
|
15
|
+
"stack, pay its plot cost and exile it, CR 116.2k)."
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def self.options(engine, index)
|
|
@@ -8,7 +8,8 @@ module Enjine
|
|
|
8
8
|
# {"via" => "alt:<ref>", "play" => "cast" | "play", "keyword" => "flashback", "instant" => true (sneak),
|
|
9
9
|
# "alt" => {"cost" => "{2}{R}", "ref" => ref, "exile" => true, "returns" => "tapped" | "unblocked",
|
|
10
10
|
# "harmonize" => true}}
|
|
11
|
-
# and a warped card in exile has the method {"via" => "warped", "play" => "cast"} (CR 702.185a)
|
|
11
|
+
# and a warped card in exile has the method {"via" => "warped", "play" => "cast"} (CR 702.185a); a plotted one
|
|
12
|
+
# {"via" => "plotted", "play" => "cast", "alt" => {"free" => true}, "sorcery" => true} (CR 702.170d).
|
|
12
13
|
#
|
|
13
14
|
# How a spell was cast is remembered on the spell (memory "cast_from": the zone, "cast_with": the keyword)
|
|
14
15
|
# and on the permanent it becomes (CR 400.7d), and each player's turn log keeps the keywords of the
|
|
@@ -32,7 +33,14 @@ module Enjine
|
|
|
32
33
|
ability = inst.ability
|
|
33
34
|
ability.method_for(inst.ref) if ability.kind == :alternative_cost && ability.functions?(self, obj, index)
|
|
34
35
|
end
|
|
35
|
-
own + warped_methods(index, obj)
|
|
36
|
+
own + warped_methods(index, obj) + plotted_methods(obj)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# CR 702.170d: a plotted card may be cast without paying its mana cost during its owner's main phase while
|
|
40
|
+
# the stack is empty, on any turn after the one it became plotted in (a sorcery's timing, whatever its type).
|
|
41
|
+
def plotted_methods(obj)
|
|
42
|
+
turn = obj.zone == :exile && obj.memory["plotted"]
|
|
43
|
+
turn && state.turn > turn ? [{ "via" => "plotted", "play" => "cast", "alt" => Permissions::FREE, "sorcery" => true }] : []
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# CR 702.185a: "Its owner may cast this card after the current turn has ended for as long as it remains
|
|
@@ -42,6 +50,14 @@ module Enjine
|
|
|
42
50
|
turn && state.turn > turn ? [{ "via" => "warped", "play" => "cast" }] : []
|
|
43
51
|
end
|
|
44
52
|
|
|
53
|
+
# CR 304.1, 307.1: an instant or flash spell (or a method cast "any time you could cast an instant") at
|
|
54
|
+
# instant timing; anything else, and a plotted card whatever its type (CR 702.170d), at sorcery timing.
|
|
55
|
+
def method_timing?(index, chars, method)
|
|
56
|
+
return true if sorcery_timing?(index)
|
|
57
|
+
|
|
58
|
+
!method["sorcery"] && (chars.type?(:instant) || chars.keyword?(:flash) || method["instant"])
|
|
59
|
+
end
|
|
60
|
+
|
|
45
61
|
# CR 702.187b: mayhem needs the card to have been discarded this turn (TurnHistory stamps it).
|
|
46
62
|
def discarded_this_turn?(obj) = !turned_memory(obj, "discarded").nil?
|
|
47
63
|
|
|
@@ -277,18 +277,18 @@ module Enjine
|
|
|
277
277
|
end
|
|
278
278
|
|
|
279
279
|
# The non-mana costs (CR 602.2b): its own ({T}, sacrifice it, exile it from the graveyard), then
|
|
280
|
-
# the chosen ones (sacrifice another creature, remove counters). A source sacrificed as a cost is
|
|
280
|
+
# the chosen ones (sacrifice another creature, remove counters). A source sacrificed or exiled as a cost is
|
|
281
281
|
# remembered as it last existed (its counters, CR 608.2h).
|
|
282
282
|
def pay_ability_costs!(ctx, obj)
|
|
283
283
|
source = object(ctx["source"])
|
|
284
284
|
ability = chars(source).ability(ctx["ability"])
|
|
285
|
-
obj.memory["source_counters"] = source.counters.dup if ability.costs.any?
|
|
285
|
+
obj.memory["source_counters"] = source.counters.dup if ability.costs.any? { Mechanics::Costs::LEAVING_SELF.include?(it.class) }
|
|
286
286
|
frame_costs(ctx).reject(&:mana?).each { it.pay!(self, source) } # the chosen option of a one_of (CR 601.2b)
|
|
287
287
|
pay_chosen_costs!(ctx)
|
|
288
288
|
return unless ability.kind == :loyalty
|
|
289
289
|
|
|
290
290
|
source.loyalty_turn = state.turn
|
|
291
|
-
cost = ability.loyalty_cost
|
|
291
|
+
cost = ability.x_loyalty? ? -ctx["x"].to_i : ability.loyalty_cost
|
|
292
292
|
cost.negative? ? remove_counters!(source, "loyalty", -cost) : add_counters!(source, "loyalty", cost)
|
|
293
293
|
end
|
|
294
294
|
|
|
@@ -75,7 +75,7 @@ module Enjine
|
|
|
75
75
|
# the mana cost or a cost such as "pay X life" or "exile X cards", X is 0.
|
|
76
76
|
def ask_x!(ctx)
|
|
77
77
|
return false unless ctx["x"].nil?
|
|
78
|
-
return (ctx["x"] = 0) && false unless base_cost(ctx)&.x_symbols.to_i.positive? || x_costs(ctx).any?
|
|
78
|
+
return (ctx["x"] = 0) && false unless base_cost(ctx)&.x_symbols.to_i.positive? || x_costs(ctx).any? || x_loyalty?(ctx)
|
|
79
79
|
|
|
80
80
|
ask!("kind" => "choose_x", "player" => ctx["player"], "max" => x_limit(ctx))
|
|
81
81
|
true
|
|
@@ -85,6 +85,9 @@ module Enjine
|
|
|
85
85
|
# graveyard" (Costs::CardsCost).
|
|
86
86
|
def x_costs(ctx) = casting_costs(ctx).select { it.respond_to?(:x?) && it.x? }
|
|
87
87
|
|
|
88
|
+
# A "-X" loyalty ability being activated (CR 606.4, 107.3).
|
|
89
|
+
def x_loyalty?(ctx) = ctx["kind"] == "ability" && casting_ability.respond_to?(:x_loyalty?) && casting_ability.x_loyalty?
|
|
90
|
+
|
|
88
91
|
# A {T} ability's own source can't pay for it.
|
|
89
92
|
def payment_excluded_source(ctx)
|
|
90
93
|
ctx["type"] == "cast" && ctx["kind"] == "ability" && needs_tap?(casting_ability) ? ctx["source"] : nil
|
|
@@ -94,6 +97,7 @@ module Enjine
|
|
|
94
97
|
# "pay X life" no more than their life total (CR 119.4), for "exile X cards" no more than there are (CR 118.3).
|
|
95
98
|
def x_limit(ctx)
|
|
96
99
|
limits = x_costs(ctx).map { it.x_limit(self, cost_payer(ctx)) }
|
|
100
|
+
limits << loyalty(object(ctx["source"])) if x_loyalty?(ctx) # CR 118.3: no more counters than it has
|
|
97
101
|
symbols = base_cost(ctx)&.x_symbols.to_i
|
|
98
102
|
return limits.min if symbols.zero?
|
|
99
103
|
|
|
@@ -162,7 +162,7 @@ module Enjine
|
|
|
162
162
|
|
|
163
163
|
c = chars(obj)
|
|
164
164
|
return false if c.land? || (method["alt"].nil? && c.mana_cost.nil?) # CR 305.9
|
|
165
|
-
return false unless
|
|
165
|
+
return false unless method_timing?(index, c, method)
|
|
166
166
|
return false if cast_forbidden?(index, obj) # CR 101.2
|
|
167
167
|
return false if returns_creature?(method["alt"]) && return_candidates(index, method["alt"]).empty?
|
|
168
168
|
return false unless additional_costs_payable?(obj) && alt_parts_payable?(obj, method) # CR 601.2f
|
|
@@ -134,12 +134,6 @@ module Enjine
|
|
|
134
134
|
granted.empty? ? base : base.with(abilities: (base.abilities + granted).freeze)
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
# CR 604.3, 707.9d: a characteristic-defining power/toughness, unless a copy exception sets them.
|
|
138
|
-
def defined_pt_copied?(obj)
|
|
139
|
-
except = copy_exceptions(obj)
|
|
140
|
-
definition(obj).defined_pt? && !(except&.key?("power") || except&.key?("toughness"))
|
|
141
|
-
end
|
|
142
|
-
|
|
143
137
|
# Layers 1-4: the object's own changes and resolved effects that add types (#own_layers),
|
|
144
138
|
# then static abilities that add card types ("this Vehicle is an artifact creature", CR 205.1b), then effects
|
|
145
139
|
# that set its creature types (CR 205.1a, 613.1d), statics before resolved effects, each in timestamp order.
|
|
@@ -239,13 +233,6 @@ module Enjine
|
|
|
239
233
|
sets.max_by(&:first)&.last
|
|
240
234
|
end
|
|
241
235
|
|
|
242
|
-
# CR 604.3, 613.4a: power/toughness from a characteristic-defining ability, in every zone.
|
|
243
|
-
def defined_power_toughness(obj, base, defn)
|
|
244
|
-
ctx = Mechanics::EffectContext.new(obj.controller, obj.id, obj.id, [], [])
|
|
245
|
-
base.with(power: defn.power_amount ? defn.power_amount.value(self, ctx) : base.power,
|
|
246
|
-
toughness: defn.toughness_amount ? defn.toughness_amount.value(self, ctx) : base.toughness)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
236
|
# [[source object, StaticAbility], ...] for permanents on the battlefield (printed abilities:
|
|
250
237
|
# an ability granting static abilities would need layer dependencies, CR 613.8).
|
|
251
238
|
def static_sources
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
class Engine
|
|
5
|
+
# Power and toughness defined by a characteristic-defining ability ("*", CR 604.3), used by the layer pass
|
|
6
|
+
# (Engine::Characteristics) on the battlefield and in every other zone.
|
|
7
|
+
module DefinedPowerToughness
|
|
8
|
+
# CR 604.3, 707.9d: a characteristic-defining power/toughness, unless a copy exception sets them.
|
|
9
|
+
def defined_pt_copied?(obj)
|
|
10
|
+
except = copy_exceptions(obj)
|
|
11
|
+
definition(obj).defined_pt? && !(except&.key?("power") || except&.key?("toughness"))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# CR 604.3, 613.4a: the defined power/toughness. Its amount may count the object itself ("card types among
|
|
15
|
+
# cards in all graveyards" while it's in one); it can't read power or toughness (CardDefinition#read_pt), so
|
|
16
|
+
# a nested look at the object while it's being computed skips the definition.
|
|
17
|
+
def defined_power_toughness(obj, base, defn)
|
|
18
|
+
return base if (defining = (@defining_pt ||= {})).key?(obj.id)
|
|
19
|
+
|
|
20
|
+
defining[obj.id] = ctx = Mechanics::EffectContext.new(obj.controller, obj.id, obj.id, [], [])
|
|
21
|
+
base.with(power: defn.power_amount ? defn.power_amount.value(self, ctx) : base.power,
|
|
22
|
+
toughness: defn.toughness_amount ? defn.toughness_amount.value(self, ctx) : base.toughness)
|
|
23
|
+
ensure
|
|
24
|
+
@defining_pt.delete(obj.id) if ctx
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
include DefinedPowerToughness
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -94,11 +94,17 @@ module Enjine
|
|
|
94
94
|
obj = object(id)
|
|
95
95
|
next unless obj.controller == index && !mana_abilities_forbidden?(obj) # Engine::Rules
|
|
96
96
|
|
|
97
|
-
chars(obj).mana_abilities.each { |inst| out << [id, inst.ref] if
|
|
97
|
+
chars(obj).mana_abilities.each { |inst| out << [id, inst.ref] if mana_ability_usable?(obj, inst) }
|
|
98
98
|
end
|
|
99
99
|
out
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
# CR 602.5b: "Activate only once each turn" counts on the source, as for other activated abilities.
|
|
103
|
+
def mana_ability_usable?(obj, inst)
|
|
104
|
+
per_turn = inst.ability.per_turn
|
|
105
|
+
(per_turn.nil? || activations_this_turn(obj, inst.ref) < per_turn) && inst.ability.activatable?(self, obj)
|
|
106
|
+
end
|
|
107
|
+
|
|
102
108
|
# Activation options for mana abilities: one per source, or one per mana it could add when the
|
|
103
109
|
# mana is chosen as it's activated (`mana`: e.g. "{G}{G}", CR 605.3b).
|
|
104
110
|
def mana_activation_options(sources)
|
|
@@ -118,6 +124,7 @@ module Enjine
|
|
|
118
124
|
ability = chars(obj).ability(ability_ref) or raise ArgumentError, "#{id} has no ability #{ability_ref}"
|
|
119
125
|
chosen = mana && Mana.parse_produce(mana, [], "mana")
|
|
120
126
|
from = mana_provenance(id) # before a cost sacrifices it (a Treasure)
|
|
127
|
+
record_activation!("source" => id, "ability" => ability_ref) if ability.per_turn
|
|
121
128
|
pay_from_pool!(index, ability.mana_cost, id) if ability.mana_cost
|
|
122
129
|
ability.costs.each { |cost| cost.pay!(self, obj) }
|
|
123
130
|
produced = chosen || ability.outcomes(self, obj).first # CR 605.3b: a fixed amount is counted as it resolves
|
|
@@ -67,6 +67,7 @@ module Enjine
|
|
|
67
67
|
"object" => obj.id, "targets" => targets, "results" => {} }
|
|
68
68
|
frame["ctx"]["linked"] = obj.memory["results"] if obj.memory.key?("results") # a delayed trigger's (CR 603.7c)
|
|
69
69
|
frame["ctx"]["linked_amounts"] = obj.memory["amounts"] if obj.memory.key?("amounts") # a reflexive trigger's
|
|
70
|
+
frame["ctx"].merge!(Snapshot.deep_copy(obj.memory["departed"])) if obj.memory.key?("departed") # and its LKI
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
# CR 400.7j / 608.2c: objects an effect moves or creates, so later effects can refer to them
|
|
@@ -17,6 +17,7 @@ module Enjine
|
|
|
17
17
|
current = chars(obj)
|
|
18
18
|
{ "controller" => obj.controller, "card" => obj.card, "types" => current.types, "subtypes" => current.subtypes,
|
|
19
19
|
"power" => current.power, "toughness" => current.toughness, "counters" => obj.counters.dup,
|
|
20
|
+
"mana_value" => Mechanics::Amount.mana_value(self, obj), # CR 202.3
|
|
20
21
|
"keywords" => current.keywords.map(&:to_s), # deathtouch, lifelink, wither (CR 702.2e, 702.15c, 702.80b)
|
|
21
22
|
"token" => token?(obj), "except" => copy_exceptions(obj), "abilities" => current.abilities }
|
|
22
23
|
.merge(obj.memory.slice(Mechanics::Linked::EXILED)) # the cards exiled with it (CR 607.2a)
|
|
@@ -109,7 +109,8 @@ module Enjine
|
|
|
109
109
|
# The part of last known information an ability keeps in its memory ("source_lki"): plain data.
|
|
110
110
|
def last_known_memory(lki)
|
|
111
111
|
info = { "controller" => lki["controller"], "power" => lki["power"], "toughness" => lki["toughness"],
|
|
112
|
-
"
|
|
112
|
+
"mana_value" => lki["mana_value"], "counters" => lki["counters"].dup, "card" => lki["card"],
|
|
113
|
+
"keywords" => lki["keywords"].dup }
|
|
113
114
|
info["except"] = lki["except"] if lki["except"] # what a copy of it copies (CR 707.2, Engine::Copies)
|
|
114
115
|
info.merge(lki.slice(Mechanics::Linked::EXILED).transform_values(&:dup)) # CR 607.2a (Linked.exiled_with)
|
|
115
116
|
end
|
|
@@ -117,6 +118,29 @@ module Enjine
|
|
|
117
118
|
# What the resolving spell or ability remembers about an object that left the battlefield during it.
|
|
118
119
|
def resolving_last_known(id) = state.frames.rfind { it["type"] == "resolve" && it["ctx"] }&.dig("ctx", "lki", id)
|
|
119
120
|
|
|
121
|
+
# A permanent that left the battlefield while a spell or ability resolves becomes a new object (CR 400.7);
|
|
122
|
+
# the resolving one links the new object to the permanent it was, so "the sacrificed creature's power"
|
|
123
|
+
# ({"power": {"result": n}}) is read as the permanent last existed (CR 608.2h).
|
|
124
|
+
def remember_departed!(new_id, old_id)
|
|
125
|
+
resolving = state.frames.rfind { it["type"] == "resolve" && it["ctx"] } or return
|
|
126
|
+
(resolving["ctx"]["departed"] ||= {})[new_id] = old_id
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# What the resolving spell or ability knows of permanents that left (their last known information and the
|
|
130
|
+
# new objects they became), for a reflexive trigger it creates ("when you do", CR 603.12, 608.2h).
|
|
131
|
+
def departed_snapshot
|
|
132
|
+
ctx = state.frames.rfind { it["type"] == "resolve" && it["ctx"] }&.fetch("ctx") or return nil
|
|
133
|
+
snapshot = ctx.slice("lki", "departed")
|
|
134
|
+
snapshot.empty? ? nil : Snapshot.deep_copy(snapshot)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The last known information of the permanent a new object was, if it left the battlefield during the
|
|
138
|
+
# resolving spell or ability.
|
|
139
|
+
def departed_last_known(new_id)
|
|
140
|
+
ctx = state.frames.rfind { it["type"] == "resolve" && it["ctx"] }&.fetch("ctx") or return nil
|
|
141
|
+
(old_id = ctx.dig("departed", new_id)) && ctx.dig("lki", old_id)
|
|
142
|
+
end
|
|
143
|
+
|
|
120
144
|
# CR 702.171b: a permanent becomes saddled until the end of the turn; leaving the battlefield ends it (the
|
|
121
145
|
# card is a new object, CR 400.7), and it isn't copiable (it lives on the object, not its characteristics).
|
|
122
146
|
def saddle!(id) = object_memory!(id, "saddled")
|
data/lib/enjine/engine/zones.rb
CHANGED
|
@@ -120,10 +120,7 @@ module Enjine
|
|
|
120
120
|
face_down = to == :exile ? data["face_down"] : nil
|
|
121
121
|
fresh = create_object(card_after_move(old, to), old.owner, to, hidden: !face_down.nil?) # front face up (Engine::Faces)
|
|
122
122
|
carry_copy_status!(fresh, old, to) # a token or copy stays one (Engine::Copies)
|
|
123
|
-
|
|
124
|
-
leave_combat!(id)
|
|
125
|
-
end_while_source_effects!(id) # CR 611.2b
|
|
126
|
-
end
|
|
123
|
+
left_battlefield!(fresh, old) if lki
|
|
127
124
|
arrive!(fresh, old, data)
|
|
128
125
|
fresh.memory.merge!("face_down" => true, "may_look" => face_down.uniq.sort) if face_down
|
|
129
126
|
list = state.zone_list(to, old.owner)
|
|
@@ -140,6 +137,14 @@ module Enjine
|
|
|
140
137
|
fresh
|
|
141
138
|
end
|
|
142
139
|
|
|
140
|
+
# A permanent that left the battlefield: the resolving spell or ability links the new object to it (CR 400.7,
|
|
141
|
+
# 608.2h; Engine::TurnHistory), it leaves combat, and effects lasting while it stays end (CR 611.2b).
|
|
142
|
+
def left_battlefield!(fresh, old)
|
|
143
|
+
remember_departed!(fresh.id, old.id)
|
|
144
|
+
leave_combat!(old.id)
|
|
145
|
+
end_while_source_effects!(old.id)
|
|
146
|
+
end
|
|
147
|
+
|
|
143
148
|
# Status of the new object in its new zone (CR 110.2, 614.12 entering status).
|
|
144
149
|
def arrive!(fresh, old, data)
|
|
145
150
|
case data["to"]
|
|
@@ -225,8 +230,13 @@ module Enjine
|
|
|
225
230
|
end
|
|
226
231
|
end
|
|
227
232
|
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
# `then_on_top`: cards set aside while the rest is shuffled, then put on top in that order ("then shuffle and
|
|
234
|
+
# put that card on top"; they stay in the library, CR 701.24a).
|
|
235
|
+
def shuffle_library!(index, then_on_top: [])
|
|
236
|
+
library = player(index).library
|
|
237
|
+
library.reject! { then_on_top.include?(it) }
|
|
238
|
+
state.rng.shuffle!(library)
|
|
239
|
+
library.unshift(*then_on_top)
|
|
230
240
|
emit Events::LibraryShuffled.new(player: name(index))
|
|
231
241
|
end
|
|
232
242
|
|