enjine 0.1.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 +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/SUPPORTED.md +387 -0
- data/docs/ARCHITECTURE.md +141 -0
- data/docs/DESIGN.md +139 -0
- data/docs/MECHANICS_COVERAGE.md +670 -0
- data/docs/guides/adding-a-mechanic.md +60 -0
- data/docs/guides/api.md +614 -0
- data/docs/guides/card-data-format.md +1472 -0
- data/docs/guides/deck-builder.md +157 -0
- data/docs/plans/standard.md +371 -0
- data/docs/reference/abilities.md +1680 -0
- data/docs/reference/costs.md +339 -0
- data/docs/reference/counters.md +121 -0
- data/docs/reference/decisions.md +102 -0
- data/docs/reference/effects.md +2340 -0
- data/docs/reference/filters.md +172 -0
- data/docs/reference/keywords.md +647 -0
- data/docs/reference/subtypes.md +169 -0
- data/docs/reference/supertypes.md +31 -0
- data/docs/reference/triggers.md +797 -0
- data/docs/reference/types.md +216 -0
- data/exe/enjine +7 -0
- data/lib/enjine/actions/activate_mana_ability.rb +24 -0
- data/lib/enjine/actions/cast.rb +57 -0
- data/lib/enjine/actions/pass.rb +18 -0
- data/lib/enjine/actions/play_land.rb +52 -0
- data/lib/enjine/actions/special_action.rb +37 -0
- data/lib/enjine/actions.rb +20 -0
- data/lib/enjine/agents/busy.rb +27 -0
- data/lib/enjine/agents/random.rb +28 -0
- data/lib/enjine/card_definition.rb +433 -0
- data/lib/enjine/card_faces.rb +116 -0
- data/lib/enjine/catalog.rb +56 -0
- data/lib/enjine/characteristics.rb +39 -0
- data/lib/enjine/cli/text.rb +295 -0
- data/lib/enjine/cli.rb +312 -0
- data/lib/enjine/decisions/bottom.rb +29 -0
- data/lib/enjine/decisions/casting.rb +188 -0
- data/lib/enjine/decisions/combat.rb +75 -0
- data/lib/enjine/decisions/discard.rb +29 -0
- data/lib/enjine/decisions/effect_choice.rb +21 -0
- data/lib/enjine/decisions/enter_choice.rb +26 -0
- data/lib/enjine/decisions/legend_rule.rb +21 -0
- data/lib/enjine/decisions/mulligan.rb +26 -0
- data/lib/enjine/decisions/order_triggers.rb +21 -0
- data/lib/enjine/decisions/play_or_draw.rb +19 -0
- data/lib/enjine/decisions/priority.rb +22 -0
- data/lib/enjine/decisions/untap_choice.rb +35 -0
- data/lib/enjine/decisions.rb +22 -0
- data/lib/enjine/deck.rb +151 -0
- data/lib/enjine/deck_builder.rb +454 -0
- data/lib/enjine/docs.rb +90 -0
- data/lib/enjine/engine/ability_layers.rb +118 -0
- data/lib/enjine/engine/attachments.rb +86 -0
- data/lib/enjine/engine/casting/alt_casting.rb +254 -0
- data/lib/enjine/engine/casting/cast_choices.rb +292 -0
- data/lib/enjine/engine/casting/casting.rb +326 -0
- data/lib/enjine/engine/casting/cost_options.rb +99 -0
- data/lib/enjine/engine/casting/costs.rb +117 -0
- data/lib/enjine/engine/casting/payment.rb +353 -0
- data/lib/enjine/engine/casting/permissions.rb +412 -0
- data/lib/enjine/engine/casting/target_counts.rb +113 -0
- data/lib/enjine/engine/casting/waterbend.rb +90 -0
- data/lib/enjine/engine/characteristics.rb +379 -0
- data/lib/enjine/engine/class_levels.rb +25 -0
- data/lib/enjine/engine/combat.rb +315 -0
- data/lib/enjine/engine/copies.rb +208 -0
- data/lib/enjine/engine/damage.rb +196 -0
- data/lib/enjine/engine/designations.rb +46 -0
- data/lib/enjine/engine/emblems.rb +37 -0
- data/lib/enjine/engine/entering.rb +156 -0
- data/lib/enjine/engine/face_down.rb +30 -0
- data/lib/enjine/engine/faces.rb +222 -0
- data/lib/enjine/engine/follow_ups.rb +27 -0
- data/lib/enjine/engine/mana_sources.rb +230 -0
- data/lib/enjine/engine/phasing.rb +74 -0
- data/lib/enjine/engine/pregame.rb +95 -0
- data/lib/enjine/engine/priority.rb +47 -0
- data/lib/enjine/engine/resolution.rb +168 -0
- data/lib/enjine/engine/rules.rb +287 -0
- data/lib/enjine/engine/sagas.rb +41 -0
- data/lib/enjine/engine/speed.rb +104 -0
- data/lib/enjine/engine/spell_grants.rb +46 -0
- data/lib/enjine/engine/state_based_actions.rb +157 -0
- data/lib/enjine/engine/triggers.rb +364 -0
- data/lib/enjine/engine/turn_history.rb +240 -0
- data/lib/enjine/engine/turn_structure.rb +224 -0
- data/lib/enjine/engine/zones.rb +278 -0
- data/lib/enjine/engine.rb +55 -0
- data/lib/enjine/errors.rb +62 -0
- data/lib/enjine/events.rb +161 -0
- data/lib/enjine/frames.rb +84 -0
- data/lib/enjine/fuzz.rb +112 -0
- data/lib/enjine/game.rb +204 -0
- data/lib/enjine/history.rb +26 -0
- data/lib/enjine/mana.rb +142 -0
- data/lib/enjine/mechanics/abilities/additional_triggers.rb +86 -0
- data/lib/enjine/mechanics/abilities/chosen_values.rb +213 -0
- data/lib/enjine/mechanics/abilities/cost_changes.rb +154 -0
- data/lib/enjine/mechanics/abilities/damage_replacements.rb +569 -0
- data/lib/enjine/mechanics/abilities/double_counters.rb +71 -0
- data/lib/enjine/mechanics/abilities/entering_choices.rb +91 -0
- data/lib/enjine/mechanics/abilities/entering_copy.rb +69 -0
- data/lib/enjine/mechanics/abilities/enters_tapped.rb +159 -0
- data/lib/enjine/mechanics/abilities/enters_with_counters.rb +116 -0
- data/lib/enjine/mechanics/abilities/mana_ability.rb +209 -0
- data/lib/enjine/mechanics/abilities/play_permissions.rb +527 -0
- data/lib/enjine/mechanics/abilities/player_rules.rb +169 -0
- data/lib/enjine/mechanics/abilities/replacement_effects.rb +368 -0
- data/lib/enjine/mechanics/abilities/restrictions.rb +144 -0
- data/lib/enjine/mechanics/abilities/spell_keywords.rb +55 -0
- data/lib/enjine/mechanics/abilities/stack_abilities.rb +542 -0
- data/lib/enjine/mechanics/abilities/static.rb +365 -0
- data/lib/enjine/mechanics/abilities/tapping_power.rb +71 -0
- data/lib/enjine/mechanics/abilities/turn_rules.rb +49 -0
- data/lib/enjine/mechanics/card_spec.rb +177 -0
- data/lib/enjine/mechanics/copy_except.rb +73 -0
- data/lib/enjine/mechanics/costs/base.rb +115 -0
- data/lib/enjine/mechanics/costs/behold.rb +76 -0
- data/lib/enjine/mechanics/costs/discard.rb +65 -0
- data/lib/enjine/mechanics/costs/exert.rb +29 -0
- data/lib/enjine/mechanics/costs/exile_cards.rb +39 -0
- data/lib/enjine/mechanics/costs/from_graveyard.rb +45 -0
- data/lib/enjine/mechanics/costs/get_counters.rb +39 -0
- data/lib/enjine/mechanics/costs/mana.rb +37 -0
- data/lib/enjine/mechanics/costs/one_of.rb +49 -0
- data/lib/enjine/mechanics/costs/pay_life.rb +50 -0
- data/lib/enjine/mechanics/costs/remove_counters.rb +51 -0
- data/lib/enjine/mechanics/costs/sacrifice.rb +59 -0
- data/lib/enjine/mechanics/costs/tap.rb +56 -0
- data/lib/enjine/mechanics/costs/waterbend.rb +29 -0
- data/lib/enjine/mechanics/counters/kinds.rb +190 -0
- data/lib/enjine/mechanics/effects/add_subtypes.rb +30 -0
- data/lib/enjine/mechanics/effects/airbend.rb +41 -0
- data/lib/enjine/mechanics/effects/amass.rb +124 -0
- data/lib/enjine/mechanics/effects/assimilate.rb +72 -0
- data/lib/enjine/mechanics/effects/base.rb +148 -0
- data/lib/enjine/mechanics/effects/become.rb +182 -0
- data/lib/enjine/mechanics/effects/behold.rb +48 -0
- data/lib/enjine/mechanics/effects/blight.rb +94 -0
- data/lib/enjine/mechanics/effects/choose_cards.rb +168 -0
- data/lib/enjine/mechanics/effects/copy.rb +370 -0
- data/lib/enjine/mechanics/effects/counter_spell.rb +21 -0
- data/lib/enjine/mechanics/effects/counters.rb +161 -0
- data/lib/enjine/mechanics/effects/damage.rb +72 -0
- data/lib/enjine/mechanics/effects/delayed_triggers.rb +112 -0
- data/lib/enjine/mechanics/effects/destroy.rb +22 -0
- data/lib/enjine/mechanics/effects/discard.rb +86 -0
- data/lib/enjine/mechanics/effects/draw.rb +16 -0
- data/lib/enjine/mechanics/effects/earthbend.rb +70 -0
- data/lib/enjine/mechanics/effects/emblem.rb +52 -0
- data/lib/enjine/mechanics/effects/endure.rb +63 -0
- data/lib/enjine/mechanics/effects/exile.rb +49 -0
- data/lib/enjine/mechanics/effects/extra_turn.rb +122 -0
- data/lib/enjine/mechanics/effects/faces.rb +115 -0
- data/lib/enjine/mechanics/effects/fight.rb +76 -0
- data/lib/enjine/mechanics/effects/flip_coins.rb +97 -0
- data/lib/enjine/mechanics/effects/gain_control.rb +94 -0
- data/lib/enjine/mechanics/effects/game_end.rb +42 -0
- data/lib/enjine/mechanics/effects/library.rb +327 -0
- data/lib/enjine/mechanics/effects/life.rb +83 -0
- data/lib/enjine/mechanics/effects/look_at_hand.rb +17 -0
- data/lib/enjine/mechanics/effects/loot.rb +118 -0
- data/lib/enjine/mechanics/effects/mana.rb +96 -0
- data/lib/enjine/mechanics/effects/may.rb +82 -0
- data/lib/enjine/mechanics/effects/meld.rb +77 -0
- data/lib/enjine/mechanics/effects/mill.rb +22 -0
- data/lib/enjine/mechanics/effects/pay.rb +231 -0
- data/lib/enjine/mechanics/effects/phasing.rb +51 -0
- data/lib/enjine/mechanics/effects/proliferate.rb +72 -0
- data/lib/enjine/mechanics/effects/pump.rb +60 -0
- data/lib/enjine/mechanics/effects/put_in_library.rb +98 -0
- data/lib/enjine/mechanics/effects/return_choice.rb +29 -0
- data/lib/enjine/mechanics/effects/return_to_battlefield.rb +80 -0
- data/lib/enjine/mechanics/effects/return_to_hand.rb +20 -0
- data/lib/enjine/mechanics/effects/roll_dice.rb +45 -0
- data/lib/enjine/mechanics/effects/sacrifice.rb +118 -0
- data/lib/enjine/mechanics/effects/scry_and_surveil.rb +74 -0
- data/lib/enjine/mechanics/effects/search.rb +156 -0
- data/lib/enjine/mechanics/effects/tap_untap.rb +23 -0
- data/lib/enjine/mechanics/effects/tokens.rb +76 -0
- data/lib/enjine/mechanics/filters/any_target.rb +15 -0
- data/lib/enjine/mechanics/filters/base.rb +386 -0
- data/lib/enjine/mechanics/filters/graveyard.rb +23 -0
- data/lib/enjine/mechanics/filters/one_of.rb +55 -0
- data/lib/enjine/mechanics/filters/permanents.rb +52 -0
- data/lib/enjine/mechanics/filters/players.rb +20 -0
- data/lib/enjine/mechanics/filters/stack.rb +97 -0
- data/lib/enjine/mechanics/follow_up.rb +46 -0
- data/lib/enjine/mechanics/keywords/alternative_costs.rb +361 -0
- data/lib/enjine/mechanics/keywords/beam_me_up.rb +78 -0
- data/lib/enjine/mechanics/keywords/changeling.rb +58 -0
- data/lib/enjine/mechanics/keywords/convoke.rb +77 -0
- data/lib/enjine/mechanics/keywords/crew.rb +155 -0
- data/lib/enjine/mechanics/keywords/cycling.rb +58 -0
- data/lib/enjine/mechanics/keywords/decayed.rb +25 -0
- data/lib/enjine/mechanics/keywords/devour.rb +61 -0
- data/lib/enjine/mechanics/keywords/evergreen.rb +54 -0
- data/lib/enjine/mechanics/keywords/extort.rb +26 -0
- data/lib/enjine/mechanics/keywords/flags.rb +46 -0
- data/lib/enjine/mechanics/keywords/foretell.rb +45 -0
- data/lib/enjine/mechanics/keywords/gift.rb +50 -0
- data/lib/enjine/mechanics/keywords/hideaway.rb +28 -0
- data/lib/enjine/mechanics/keywords/increment.rb +31 -0
- data/lib/enjine/mechanics/keywords/job_select.rb +29 -0
- data/lib/enjine/mechanics/keywords/kicker.rb +92 -0
- data/lib/enjine/mechanics/keywords/mobilize_and_firebending.rb +84 -0
- data/lib/enjine/mechanics/keywords/paradigm.rb +91 -0
- data/lib/enjine/mechanics/keywords/power_up.rb +48 -0
- data/lib/enjine/mechanics/keywords/protection.rb +111 -0
- data/lib/enjine/mechanics/keywords/prowess.rb +30 -0
- data/lib/enjine/mechanics/keywords/riot.rb +35 -0
- data/lib/enjine/mechanics/keywords/saddle.rb +115 -0
- data/lib/enjine/mechanics/keywords/speed.rb +89 -0
- data/lib/enjine/mechanics/keywords/station.rb +163 -0
- data/lib/enjine/mechanics/keywords/storied.rb +41 -0
- data/lib/enjine/mechanics/keywords/storm_and_cascade.rb +86 -0
- data/lib/enjine/mechanics/keywords/sunburst.rb +29 -0
- data/lib/enjine/mechanics/keywords/suspend.rb +191 -0
- data/lib/enjine/mechanics/keywords/teamwork.rb +45 -0
- data/lib/enjine/mechanics/keywords/ward.rb +67 -0
- data/lib/enjine/mechanics/linked.rb +67 -0
- data/lib/enjine/mechanics/mana_spent.rb +99 -0
- data/lib/enjine/mechanics/selector.rb +207 -0
- data/lib/enjine/mechanics/subtypes/auras_and_equipment.rb +176 -0
- data/lib/enjine/mechanics/subtypes/basic_land_types.rb +36 -0
- data/lib/enjine/mechanics/subtypes/class_levels.rb +102 -0
- data/lib/enjine/mechanics/subtypes/saga.rb +89 -0
- data/lib/enjine/mechanics/supertypes/basic.rb +17 -0
- data/lib/enjine/mechanics/supertypes/legendary.rb +18 -0
- data/lib/enjine/mechanics/triggers/ability_triggers.rb +51 -0
- data/lib/enjine/mechanics/triggers/any_of.rb +43 -0
- data/lib/enjine/mechanics/triggers/base.rb +228 -0
- data/lib/enjine/mechanics/triggers/casting.rb +219 -0
- data/lib/enjine/mechanics/triggers/combat.rb +123 -0
- data/lib/enjine/mechanics/triggers/damage.rb +130 -0
- data/lib/enjine/mechanics/triggers/dilemma.rb +37 -0
- data/lib/enjine/mechanics/triggers/draw_and_discard.rb +85 -0
- data/lib/enjine/mechanics/triggers/keyword_actions.rb +55 -0
- data/lib/enjine/mechanics/triggers/life.rb +35 -0
- data/lib/enjine/mechanics/triggers/steps.rb +39 -0
- data/lib/enjine/mechanics/triggers/tap_and_counters.rb +111 -0
- data/lib/enjine/mechanics/triggers/zone_changes.rb +249 -0
- data/lib/enjine/mechanics/types/kindred.rb +30 -0
- data/lib/enjine/mechanics/types/land.rb +24 -0
- data/lib/enjine/mechanics/types/permanent_types.rb +67 -0
- data/lib/enjine/mechanics/types/spell_types.rb +38 -0
- data/lib/enjine/mechanics/values.rb +675 -0
- data/lib/enjine/mechanics.rb +74 -0
- data/lib/enjine/player.rb +30 -0
- data/lib/enjine/primitive.rb +61 -0
- data/lib/enjine/replacements.rb +191 -0
- data/lib/enjine/rng.rb +57 -0
- data/lib/enjine/rules_monitor.rb +404 -0
- data/lib/enjine/setup.rb +45 -0
- data/lib/enjine/snapshot/player_checks.rb +86 -0
- data/lib/enjine/snapshot/turn_checks.rb +91 -0
- data/lib/enjine/snapshot.rb +307 -0
- data/lib/enjine/state.rb +173 -0
- data/lib/enjine/steps.rb +117 -0
- data/lib/enjine/tokens.rb +25 -0
- data/lib/enjine/verify.rb +233 -0
- data/lib/enjine/version.rb +7 -0
- data/lib/enjine/view.rb +162 -0
- data/lib/enjine.rb +46 -0
- metadata +315 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
# Rules that apply to a player rather than to an object (see Engine::Rules#player_rule?):
|
|
6
|
+
# hexproof the player can't be the target of spells or abilities opponents control (702.11c)
|
|
7
|
+
# cant_attack the player can't declare attackers (508.1c)
|
|
8
|
+
# no_max_hand_size the player has no maximum hand size (402.2, 514.1)
|
|
9
|
+
# prevent_all_damage all damage that would be dealt to the player is prevented (615.1)
|
|
10
|
+
# cant_lose_for_life the player doesn't lose the game for having 0 or less life (704.5a, 104.3b)
|
|
11
|
+
# cant_sacrifice the player can't sacrifice permanents matching `filter` (701.21a, 614.17b)
|
|
12
|
+
# cant_cast the player can't cast spells (matching `spells`) (101.2, 601.3)
|
|
13
|
+
# cant_cast_except_from_hand the player can't cast spells from anywhere other than their hand (601.3)
|
|
14
|
+
# spells_cant_be_countered spells the player controls (matching `spells`) can't be countered (101.2, 701.6a)
|
|
15
|
+
# abilities_cant_be_countered activated and triggered abilities the player controls can't be countered (101.2)
|
|
16
|
+
# cant_gain_life the player can't gain life (119.7)
|
|
17
|
+
# cant_activate the player can't activate abilities of sources matching `filter`, except mana
|
|
18
|
+
# abilities (602.5)
|
|
19
|
+
# protection_from_opponents the player has protection from each of their opponents: opponents' spells
|
|
20
|
+
# and abilities can't target them and damage from opponents' sources is prevented
|
|
21
|
+
# (702.16b, 702.16e, 702.16k)
|
|
22
|
+
# damage_cant_be_prevented / combat_damage_cant_be_prevented game rules: while any player has one, (combat)
|
|
23
|
+
# damage can't be prevented (615.12)
|
|
24
|
+
module PlayerRules
|
|
25
|
+
RULES = {
|
|
26
|
+
"hexproof" => %w[702.11c 115.4], "cant_attack" => %w[508.1c], "no_max_hand_size" => %w[402.2 514.1],
|
|
27
|
+
"prevent_all_damage" => %w[615.1 615.1a], "cant_lose_for_life" => %w[704.5a 104.3b],
|
|
28
|
+
"cant_sacrifice" => %w[701.21a 614.17b], "cant_cast" => %w[101.2 601.3],
|
|
29
|
+
"cant_cast_except_from_hand" => %w[101.2 601.3], "spells_cant_be_countered" => %w[101.2 701.6a],
|
|
30
|
+
"abilities_cant_be_countered" => %w[101.2 701.6a],
|
|
31
|
+
"cant_gain_life" => %w[119.7], "cant_activate" => %w[101.2 602.5],
|
|
32
|
+
"damage_cant_be_prevented" => %w[615.12], "combat_damage_cant_be_prevented" => %w[615.12],
|
|
33
|
+
"protection_from_opponents" => %w[702.16b 702.16e 702.16k]
|
|
34
|
+
}.freeze
|
|
35
|
+
# Rules that take a `filter` of permanents (required for cant_sacrifice) or a card spec of `spells`.
|
|
36
|
+
FILTERED = %w[cant_sacrifice cant_activate].freeze
|
|
37
|
+
REQUIRED_FILTER = %w[cant_sacrifice].freeze
|
|
38
|
+
SPELLS = %w[cant_cast spells_cant_be_countered].freeze
|
|
39
|
+
|
|
40
|
+
def self.rule(params, errors, path)
|
|
41
|
+
rule = Mechanics.fetch_param(params, :rule)
|
|
42
|
+
errors << "#{path}.rule: must be one of #{RULES.keys.join(", ")}" unless RULES.key?(rule)
|
|
43
|
+
rule
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# [filter, spells] of a rule's data, validated for the rule.
|
|
47
|
+
def self.qualifiers(params, rule, errors, path)
|
|
48
|
+
raw = Mechanics.fetch_param(params, :filter)
|
|
49
|
+
if raw.nil? && REQUIRED_FILTER.include?(rule)
|
|
50
|
+
errors << "#{path}.filter: required"
|
|
51
|
+
elsif raw && !FILTERED.include?(rule)
|
|
52
|
+
errors << "#{path}.filter: only for #{FILTERED.join(", ")}"
|
|
53
|
+
end
|
|
54
|
+
spells = Mechanics.fetch_param(params, :spells)
|
|
55
|
+
errors << "#{path}.spells: only for #{SPELLS.join(", ")}" if spells && !SPELLS.include?(rule)
|
|
56
|
+
[raw && Mechanics.build_entry(:filter, raw, errors, "#{path}.filter"), spells && CardSpec.build(spells, errors, "#{path}.spells")]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module Abilities
|
|
61
|
+
# A static ability giving players a rule: "You don't lose the game for having 0 or less life",
|
|
62
|
+
# "You can't sacrifice Treasure tokens", "Your opponents can't attack", "Your opponents can't cast
|
|
63
|
+
# spells during your turn", "Creature spells you control can't be countered", "Players can't gain life".
|
|
64
|
+
class PlayerRule < Primitive
|
|
65
|
+
AFFECTS = %w[you opponents each].freeze
|
|
66
|
+
|
|
67
|
+
primitive :ability, :player_rule,
|
|
68
|
+
description: "While this permanent is on the battlefield (and its `if` condition holds), `rule` " \
|
|
69
|
+
"applies to the players in `affects` (you | opponents | each; default you). Rules: " \
|
|
70
|
+
"#{PlayerRules::RULES.keys.join(", ")}. `cant_sacrifice` takes a `filter` of the " \
|
|
71
|
+
"permanents that can't be sacrificed, `cant_activate` may take a `filter` of the " \
|
|
72
|
+
"sources whose abilities can't be activated (mana abilities still can), and " \
|
|
73
|
+
"`cant_cast` and `spells_cant_be_countered` may take a card spec of the `spells`. " \
|
|
74
|
+
"The damage rules apply to all damage whichever players they name.",
|
|
75
|
+
rules: PlayerRules::RULES.values.flatten.uniq + %w[604.1 604.2 611.3a],
|
|
76
|
+
params: { "rule" => PlayerRules::RULES.keys.join(" | "), "affects" => AFFECTS.join(" | "),
|
|
77
|
+
"filter" => "permanent filter (#{PlayerRules::FILTERED.join(", ")})",
|
|
78
|
+
"spells" => "card spec of the spells (#{PlayerRules::SPELLS.join(", ")})",
|
|
79
|
+
"if" => "condition (\"during your turn\": {\"your_turn\": true})" },
|
|
80
|
+
example: { "player_rule" => { "rule" => "cant_cast", "affects" => "opponents", "if" => { "your_turn" => true } } }
|
|
81
|
+
|
|
82
|
+
attr_reader :rule, :affects, :filter, :spells, :condition
|
|
83
|
+
|
|
84
|
+
def self.build(params, errors, path, **)
|
|
85
|
+
Mechanics.check_keys(params, %w[rule affects filter spells if], errors, path)
|
|
86
|
+
rule = PlayerRules.rule(params, errors, path)
|
|
87
|
+
affects = Mechanics.fetch_param(params, :affects) || "you"
|
|
88
|
+
errors << "#{path}.affects: must be one of #{AFFECTS.join(", ")}" unless AFFECTS.include?(affects)
|
|
89
|
+
filter, spells = PlayerRules.qualifiers(params, rule, errors, path)
|
|
90
|
+
raw_if = Mechanics.fetch_param(params, :if)
|
|
91
|
+
new(rule, affects, filter, spells: spells, condition: raw_if && Condition.build(raw_if, errors, "#{path}.if"))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def initialize(rule, affects, filter, spells: nil, condition: nil)
|
|
95
|
+
super()
|
|
96
|
+
@rule = rule
|
|
97
|
+
@affects = affects
|
|
98
|
+
@filter = filter
|
|
99
|
+
@spells = spells
|
|
100
|
+
@condition = condition
|
|
101
|
+
freeze
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def kind = :player_rule
|
|
105
|
+
def mana_ability? = false
|
|
106
|
+
def allowed_types = nil
|
|
107
|
+
def try_effects = []
|
|
108
|
+
|
|
109
|
+
# Whether the rule applies to player `index` for a source controlled by `controller`.
|
|
110
|
+
def applies_to?(index, controller)
|
|
111
|
+
affects == "each" || (affects == "you") == (index == controller)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Its "as long as" / "during your turn" condition (CR 604.2), from its source's point of view.
|
|
115
|
+
def active?(engine, source)
|
|
116
|
+
condition.nil? || condition.true?(engine, EffectContext.new(source.controller, source.id, source.id, [], []))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whether the object asked about (a permanent for `filter`, a card for `spells`) is one the rule is about.
|
|
120
|
+
def matches?(engine, object, source)
|
|
121
|
+
(filter.nil? || filter.match?(engine, object.id, source.controller)) && (spells.nil? || spells.match?(engine, object))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def to_s
|
|
125
|
+
who = { "you" => "you", "opponents" => "your opponents", "each" => "each player" }.fetch(affects)
|
|
126
|
+
about = filter || spells
|
|
127
|
+
"#{who}: #{rule.tr("_", " ")}#{" (#{about.is_a?(Class) ? about.primitive_name : about})" if about}" \
|
|
128
|
+
"#{" as long as #{condition}" if condition}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
module Effects
|
|
134
|
+
# "You gain hexproof until end of turn", "Prevent all damage that would be dealt to you this turn",
|
|
135
|
+
# "You have no maximum hand size until your next turn", "They can't attack this turn".
|
|
136
|
+
class PlayerRuleEffect < Effect
|
|
137
|
+
DURATIONS = %w[end_of_turn until_your_next_turn].freeze
|
|
138
|
+
|
|
139
|
+
primitive :effect, :player_rule,
|
|
140
|
+
description: "Each referenced player (default you) gets `rule` for the `duration`: end_of_turn " \
|
|
141
|
+
"(default) or until_your_next_turn (until the controller's next turn begins). Rules: " \
|
|
142
|
+
"#{(PlayerRules::RULES.keys - PlayerRules::REQUIRED_FILTER).join(", ")}.",
|
|
143
|
+
rules: PlayerRules::RULES.values.flatten.uniq + %w[611.2a 514.2],
|
|
144
|
+
params: { "rule" => (PlayerRules::RULES.keys - PlayerRules::REQUIRED_FILTER).join(" | "), "to" => "selector",
|
|
145
|
+
"duration" => DURATIONS.join(" | ") },
|
|
146
|
+
example: { "player_rule" => { "rule" => "no_max_hand_size", "duration" => "until_your_next_turn" } }
|
|
147
|
+
|
|
148
|
+
attr_reader :rule, :duration
|
|
149
|
+
|
|
150
|
+
def self.keys = %w[rule duration to if]
|
|
151
|
+
def self.default_to = "you"
|
|
152
|
+
|
|
153
|
+
def setup(params, errors, path)
|
|
154
|
+
@rule = PlayerRules.rule(params, errors, path)
|
|
155
|
+
errors << "#{path}.rule: #{@rule} needs a static ability (it takes a filter)" if PlayerRules::REQUIRED_FILTER.include?(@rule)
|
|
156
|
+
@duration = Mechanics.fetch_param(params, :duration) || "end_of_turn"
|
|
157
|
+
errors << "#{path}.duration: must be one of #{DURATIONS.join(", ")}" unless DURATIONS.include?(@duration)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def apply(engine, ctx)
|
|
161
|
+
affected = players(engine, ctx).map { Filters.player_ref(engine, it) }
|
|
162
|
+
engine.add_continuous_effect!("player_rule", ctx, affected, "rule" => rule, "duration" => duration)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def to_s = "#{rule.tr("_", " ")} (#{duration.tr("_", " ")}) → #{to}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
# Which permanents a static ability of a permanent applies to, relative to its controller:
|
|
6
|
+
# {"each": filter, "controller": "you" | "opponent" | "any", "other": true}
|
|
7
|
+
class Affects
|
|
8
|
+
CONTROLLERS = %w[you opponent any].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :filter, :controller, :other
|
|
11
|
+
|
|
12
|
+
def self.build(data, errors, path)
|
|
13
|
+
unless data.is_a?(Hash) && data["each"]
|
|
14
|
+
errors << "#{path}: must be {\"each\": filter, \"controller\": you|opponent|any, \"other\": bool}"
|
|
15
|
+
return new(nil, "any", false)
|
|
16
|
+
end
|
|
17
|
+
Mechanics.check_keys(data, %w[each controller other], errors, path)
|
|
18
|
+
controller = data.fetch("controller", "any")
|
|
19
|
+
errors << "#{path}.controller: must be one of #{CONTROLLERS.join(", ")}" unless CONTROLLERS.include?(controller)
|
|
20
|
+
new(Mechanics.build_entry(:filter, data["each"], errors, "#{path}.each"), controller, data["other"] == true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(filter, controller, other)
|
|
24
|
+
@filter = filter
|
|
25
|
+
@controller = controller
|
|
26
|
+
@other = other
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def controller_ok?(object_controller, source)
|
|
31
|
+
controller == "any" || (controller == "you") == (object_controller == source.controller)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A permanent on the battlefield, by its current characteristics and status.
|
|
35
|
+
def match?(engine, obj, source)
|
|
36
|
+
return false if other && obj.id == source.id
|
|
37
|
+
return false unless controller_ok?(obj.controller, source)
|
|
38
|
+
|
|
39
|
+
filter.match_for?(engine, obj.id, source.controller, source.id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A permanent about to enter under `entering_controller` (CR 614.12: as it would exist there).
|
|
43
|
+
def match_facts?(facts, entering_controller, source)
|
|
44
|
+
!filter.nil? && controller_ok?(entering_controller, source) && filter.matches_facts?(facts)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
name = filter.is_a?(Class) ? filter.primitive_name.tr("_", " ") : filter.to_s
|
|
49
|
+
"#{"other " if other}#{name}s#{{ "you" => " you control", "opponent" => " opponents control" }[controller]}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module Abilities
|
|
54
|
+
# Base for replacement effects of a permanent's static abilities (CR 614.1, 614.12): no stack.
|
|
55
|
+
class StaticReplacement < Primitive
|
|
56
|
+
def mana_ability? = false
|
|
57
|
+
def allowed_types = nil
|
|
58
|
+
def try_effects = []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# "If you would draw a card, draw two cards instead." (CR 614.11)
|
|
62
|
+
class DrawReplacement < StaticReplacement
|
|
63
|
+
primitive :ability, :draw_replacement,
|
|
64
|
+
description: "While this permanent is on the battlefield, each time its controller would draw a " \
|
|
65
|
+
"card, they draw `amount` cards instead (even with an empty library). The draws it " \
|
|
66
|
+
"makes aren't replaced by it again, but another such effect applies to each of them.",
|
|
67
|
+
rules: %w[614.11 614.11a 614.5 121.2 616.1],
|
|
68
|
+
params: { "amount" => "integer >= 2" },
|
|
69
|
+
example: { "draw_replacement" => { "amount" => 2 } }
|
|
70
|
+
|
|
71
|
+
attr_reader :amount
|
|
72
|
+
|
|
73
|
+
def self.build(params, errors, path, **)
|
|
74
|
+
Mechanics.check_keys(params, %w[amount], errors, path)
|
|
75
|
+
new(Mechanics.integer_param(params, :amount, errors, path, min: 2))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def initialize(amount)
|
|
79
|
+
super()
|
|
80
|
+
@amount = amount
|
|
81
|
+
freeze
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def kind = :draw_replacement
|
|
85
|
+
def replace_draw = true
|
|
86
|
+
def to_s = "if you would draw a card, draw #{amount} instead"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# "If another creature you control with a +1/+1 counter on it would be destroyed, instead remove all
|
|
90
|
+
# +1/+1 counters from it and remove all damage from it." (CR 614.1a, regeneration-like 614.8)
|
|
91
|
+
class DestroyReplacement < StaticReplacement
|
|
92
|
+
primitive :ability, :destroy_replacement,
|
|
93
|
+
description: "If a permanent matching `affects` would be destroyed (by an effect, lethal damage or " \
|
|
94
|
+
"deathtouch), instead: all its `remove_counters` counters are removed (if given) and, " \
|
|
95
|
+
"with `remove_damage: true`, all damage marked on it is removed. Sacrificing isn't " \
|
|
96
|
+
"destroying, and 0 toughness isn't either.",
|
|
97
|
+
rules: %w[614.1a 614.8 701.8a 701.21a 704.5f 704.5g 704.5h],
|
|
98
|
+
params: { "affects" => "{\"each\": filter, \"controller\": ..., \"other\": bool}",
|
|
99
|
+
"remove_counters" => "counter name", "remove_damage" => "boolean" },
|
|
100
|
+
example: { "destroy_replacement" => {
|
|
101
|
+
"affects" => { "each" => { "creature" => { "counters" => "+1/+1" } }, "controller" => "you", "other" => true },
|
|
102
|
+
"remove_counters" => "+1/+1", "remove_damage" => true
|
|
103
|
+
} }
|
|
104
|
+
|
|
105
|
+
attr_reader :affects, :counter, :damage
|
|
106
|
+
|
|
107
|
+
def self.build(params, errors, path, **)
|
|
108
|
+
Mechanics.check_keys(params, %w[affects remove_counters remove_damage], errors, path)
|
|
109
|
+
affects = Affects.build(Mechanics.fetch_param(params, :affects), errors, "#{path}.affects")
|
|
110
|
+
counter = Mechanics.fetch_param(params, :remove_counters)
|
|
111
|
+
counter &&= Counters.parse(counter, errors, "#{path}.remove_counters")
|
|
112
|
+
damage = Mechanics.fetch_param(params, :remove_damage) == true
|
|
113
|
+
errors << "#{path}: needs remove_counters and/or remove_damage" unless counter || damage
|
|
114
|
+
new(affects, counter, damage)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def initialize(affects, counter, damage)
|
|
118
|
+
super()
|
|
119
|
+
@affects = affects
|
|
120
|
+
@counter = counter
|
|
121
|
+
@damage = damage
|
|
122
|
+
freeze
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def kind = :destroy_replacement
|
|
126
|
+
def replaces?(engine, obj, source) = affects.match?(engine, obj, source)
|
|
127
|
+
|
|
128
|
+
def replace_destroy(engine, obj)
|
|
129
|
+
engine.remove_all_counters!(obj, counter) if counter
|
|
130
|
+
engine.remove_damage!(obj) if damage
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_s
|
|
134
|
+
instead = [counter && "remove all #{counter} counters", damage && "remove all damage"].compact.join(" and ")
|
|
135
|
+
"if #{affects} would be destroyed, instead #{instead}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# "If one or more tokens would be created under your control, that many 3/3 Illusion tokens are
|
|
140
|
+
# created instead." (CR 614.1a, 111.1)
|
|
141
|
+
class TokenReplacement < StaticReplacement
|
|
142
|
+
primitive :ability, :token_replacement,
|
|
143
|
+
description: "If tokens would be created under this permanent's controller's control, that many " \
|
|
144
|
+
"`token` tokens are created instead (tapped or attacking if the originals would be). " \
|
|
145
|
+
"`token` is a predefined token name or inline card data.",
|
|
146
|
+
rules: %w[614.1a 614.16 111.1 616.1],
|
|
147
|
+
params: { "token" => "\"Treasure\" | \"Clue\" | \"Food\" | {card data}" },
|
|
148
|
+
example: { "token_replacement" => { "token" => { "name" => "Illusion", "types" => %w[artifact creature],
|
|
149
|
+
"subtypes" => ["illusion"], "power" => 3,
|
|
150
|
+
"toughness" => 3 } } }
|
|
151
|
+
|
|
152
|
+
attr_reader :token_definition
|
|
153
|
+
|
|
154
|
+
def self.build(params, errors, path, **)
|
|
155
|
+
Mechanics.check_keys(params, %w[token], errors, path)
|
|
156
|
+
raw = Mechanics.fetch_param(params, :token)
|
|
157
|
+
data = raw.is_a?(String) ? Enjine::Tokens.fetch(raw) : raw
|
|
158
|
+
return new(CardDefinition.token(data)) if data.is_a?(Hash)
|
|
159
|
+
|
|
160
|
+
errors << "#{path}.token: must be a predefined token (#{Enjine::Tokens::LIBRARY.keys.join(", ")}) or card data"
|
|
161
|
+
new(nil)
|
|
162
|
+
rescue InvalidCardDefinition => e
|
|
163
|
+
e.errors.each { errors << "#{path}.token.#{it}" }
|
|
164
|
+
new(nil)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def initialize(definition)
|
|
168
|
+
super()
|
|
169
|
+
@token_definition = definition
|
|
170
|
+
freeze
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def kind = :token_replacement
|
|
174
|
+
def to_s = "if tokens would be created under your control, that many #{token_definition.name} tokens are created instead"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# "If one or more tokens would be created under your control, twice that many of those tokens are
|
|
178
|
+
# created instead." (CR 614.1a, 111.1)
|
|
179
|
+
class DoubleTokens < StaticReplacement
|
|
180
|
+
primitive :ability, :double_tokens,
|
|
181
|
+
description: "While this permanent is on the battlefield, if tokens would be created under its " \
|
|
182
|
+
"controller's control, twice that many of those tokens are created instead. Several " \
|
|
183
|
+
"apply one after another (two make four times as many).",
|
|
184
|
+
rules: %w[614.1a 614.5 111.1 616.1],
|
|
185
|
+
example: "double_tokens"
|
|
186
|
+
|
|
187
|
+
def self.build(params, errors, path, **)
|
|
188
|
+
Mechanics.check_keys(params, [], errors, path)
|
|
189
|
+
new
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def initialize
|
|
193
|
+
super
|
|
194
|
+
freeze
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def kind = :double_tokens
|
|
198
|
+
def doubles_tokens = true
|
|
199
|
+
def to_s = "if tokens would be created under your control, twice that many are created instead"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# "If a creature an opponent controls would die, exile it instead." (CR 614.1a, 700.4)
|
|
203
|
+
class ExileIfDiesStatic < StaticReplacement
|
|
204
|
+
primitive :ability, :exile_if_dies,
|
|
205
|
+
description: "While this permanent is on the battlefield, a permanent matching `affects` that would " \
|
|
206
|
+
"be put into a graveyard from the battlefield is exiled instead; the card is \"exiled " \
|
|
207
|
+
"with\" this permanent (exiled_with). `then`: effects right after (\"instead exile it " \
|
|
208
|
+
"and you gain 2 life\"; a reflexive_trigger for \"When you do, ...\"), reading " \
|
|
209
|
+
"{\"triggering\": \"object\"} (the exiled card) and \"player\" (who controlled it).",
|
|
210
|
+
rules: %w[614.1a 700.4 603.12 607.2a],
|
|
211
|
+
params: { "affects" => "{\"each\": filter, \"controller\": you|opponent|any, \"other\": bool}",
|
|
212
|
+
"then" => "list of effects" },
|
|
213
|
+
example: { "exile_if_dies" => { "affects" => { "each" => "creature", "controller" => "opponent" } } }
|
|
214
|
+
|
|
215
|
+
attr_reader :affects, :follow_up
|
|
216
|
+
|
|
217
|
+
def self.build(params, errors, path, **)
|
|
218
|
+
Mechanics.check_keys(params, %w[affects then], errors, path)
|
|
219
|
+
new(Affects.build(Mechanics.fetch_param(params, :affects), errors, "#{path}.affects"), FollowUp.build(params, errors, path))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def initialize(affects, follow_up = nil)
|
|
223
|
+
super()
|
|
224
|
+
@affects = affects
|
|
225
|
+
@follow_up = follow_up
|
|
226
|
+
freeze
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def kind = :exile_if_dies
|
|
230
|
+
def try_effects = [follow_up].compact
|
|
231
|
+
def exiles_dying?(engine, obj, source) = affects.match?(engine, obj, source)
|
|
232
|
+
|
|
233
|
+
# What happens once the card is exiled: it is linked to `source` (CR 607.2a), then the follow-up.
|
|
234
|
+
def follow_up_entry(source, obj)
|
|
235
|
+
facts = { "player" => obj.controller }
|
|
236
|
+
(follow_up&.entry(source.card, source.controller, source.id, facts) || { "source" => source.id, "facts" => facts })
|
|
237
|
+
.merge("link" => true)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def to_s = "if #{affects} would die, exile it instead#{", then #{follow_up}" if follow_up}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# "Lands you control enter untapped." (CR 614.1c, 614.12; applied after every "enters tapped", 616.1)
|
|
244
|
+
class EntersUntapped < StaticReplacement
|
|
245
|
+
primitive :ability, :enters_untapped,
|
|
246
|
+
description: "Permanents matching `affects` enter the battlefield untapped, even if another effect " \
|
|
247
|
+
"or their own ability would have them enter tapped.",
|
|
248
|
+
rules: %w[614.1c 614.12 616.1],
|
|
249
|
+
params: { "affects" => "{\"each\": filter, \"controller\": you|opponent|any}" },
|
|
250
|
+
example: { "enters_untapped" => { "affects" => { "each" => "land", "controller" => "you" } } }
|
|
251
|
+
|
|
252
|
+
attr_reader :affects
|
|
253
|
+
|
|
254
|
+
def self.build(params, errors, path, **)
|
|
255
|
+
Mechanics.check_keys(params, %w[affects], errors, path)
|
|
256
|
+
new(Affects.build(Mechanics.fetch_param(params, :affects), errors, "#{path}.affects"))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def initialize(affects)
|
|
260
|
+
super()
|
|
261
|
+
@affects = affects
|
|
262
|
+
freeze
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def kind = :enters_untapped
|
|
266
|
+
def enters_untapped?(source, controller, facts) = affects.match_facts?(facts, controller, source)
|
|
267
|
+
def to_s = "#{affects} enter untapped"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# "If a creature you control would enter while you control an attacking creature, instead it enters
|
|
271
|
+
# tapped and attacking." (CR 614.1c, 508.4)
|
|
272
|
+
class EntersAttacking < StaticReplacement
|
|
273
|
+
primitive :ability, :enters_attacking,
|
|
274
|
+
description: "While this permanent's controller controls an attacking creature, permanents matching " \
|
|
275
|
+
"`affects` enter the battlefield tapped and attacking (the defending player). They " \
|
|
276
|
+
"never \"attacked\", so attack triggers don't see them.",
|
|
277
|
+
rules: %w[614.1c 508.4 506.4],
|
|
278
|
+
params: { "affects" => "{\"each\": filter, \"controller\": you|opponent|any}" },
|
|
279
|
+
example: { "enters_attacking" => { "affects" => { "each" => "creature", "controller" => "you" } } }
|
|
280
|
+
|
|
281
|
+
attr_reader :affects
|
|
282
|
+
|
|
283
|
+
def self.build(params, errors, path, **)
|
|
284
|
+
Mechanics.check_keys(params, %w[affects], errors, path)
|
|
285
|
+
new(Affects.build(Mechanics.fetch_param(params, :affects), errors, "#{path}.affects"))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def initialize(affects)
|
|
289
|
+
super()
|
|
290
|
+
@affects = affects
|
|
291
|
+
freeze
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def kind = :enters_attacking
|
|
295
|
+
|
|
296
|
+
def enters_attacking?(engine, source, controller, facts)
|
|
297
|
+
engine.attackers.keys.any? { engine.object(it).controller == source.controller } &&
|
|
298
|
+
affects.match_facts?(facts, controller, source)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def to_s = "#{affects} enter tapped and attacking while you control an attacking creature"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
module Effects
|
|
306
|
+
# "Until end of turn, if one or more counters would be put on a permanent you control, that many plus
|
|
307
|
+
# one of each of those kinds are put on it instead." (CR 614.1a, 611.2a)
|
|
308
|
+
class CounterBonus < Effect
|
|
309
|
+
primitive :effect, :counter_bonus,
|
|
310
|
+
description: "Until end of turn, whenever one or more counters would be put on a permanent the " \
|
|
311
|
+
"controller controls (including counters it enters with), that many plus `amount` " \
|
|
312
|
+
"of each of those kinds are put on it instead.",
|
|
313
|
+
rules: %w[614.1a 614.16 611.2a 122.6 616.1],
|
|
314
|
+
params: { "amount" => "integer (default 1)" },
|
|
315
|
+
example: { "counter_bonus" => { "amount" => 1 } }
|
|
316
|
+
|
|
317
|
+
def self.keys = %w[amount if]
|
|
318
|
+
def self.default_to = "you"
|
|
319
|
+
|
|
320
|
+
def initialize(to:, params:, errors:, path:, targets_count: 0, results_count: 0)
|
|
321
|
+
params = params.transform_keys(&:to_s)
|
|
322
|
+
params["amount"] = 1 unless params.key?("amount")
|
|
323
|
+
super
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def apply(engine, ctx)
|
|
327
|
+
engine.add_continuous_effect!("counter_bonus", ctx, [], "amount" => amount_for(engine, ctx))
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def to_s = "until end of turn, counters put on your permanents: that many plus #{amount}"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# "If that creature would die this turn, exile it instead." (CR 614.1a, 700.4)
|
|
334
|
+
class ExileIfDies < Effect
|
|
335
|
+
primitive :effect, :exile_if_dies,
|
|
336
|
+
description: "Until end of turn, if a referenced permanent would be put into a graveyard from the " \
|
|
337
|
+
"battlefield, it is exiled instead.",
|
|
338
|
+
rules: %w[614.1a 700.4 611.2a 611.2c],
|
|
339
|
+
params: { "to" => "selector" },
|
|
340
|
+
example: { "exile_if_dies" => { "to" => { "target" => 0 } } }
|
|
341
|
+
|
|
342
|
+
def self.keys = %w[to if]
|
|
343
|
+
|
|
344
|
+
def apply(engine, ctx) = engine.add_continuous_effect!("exile_if_dies", ctx, objects(engine, ctx), {})
|
|
345
|
+
def to_s = "if it would die this turn, exile it instead → #{to}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# "Exile [this spell]" as part of its own resolution, so it doesn't go to the graveyard (CR 608.2n).
|
|
349
|
+
class ExileThisSpell < Effect
|
|
350
|
+
primitive :effect, :exile_this_spell,
|
|
351
|
+
description: "The resolving spell is exiled instead of being put into its owner's graveyard as it " \
|
|
352
|
+
"finishes resolving.",
|
|
353
|
+
rules: %w[608.2n 406.1],
|
|
354
|
+
example: "exile_this_spell"
|
|
355
|
+
|
|
356
|
+
def self.keys = %w[if]
|
|
357
|
+
def self.default_to = "you"
|
|
358
|
+
|
|
359
|
+
# The resolving object is on the stack; an ability (not a spell) has no card to exile.
|
|
360
|
+
def apply(engine, ctx)
|
|
361
|
+
engine.move!(ctx.object, :exile, reason: "exile") if engine.state.objects[ctx.object]&.kind == "card"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def to_s = "exile this spell"
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|