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,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Abilities
|
|
6
|
+
# Base for restriction abilities a permanent has on itself (CR 509.1b, 115.4): no stack, no effects.
|
|
7
|
+
class Restriction < Primitive
|
|
8
|
+
def mana_ability? = false
|
|
9
|
+
def allowed_types = nil
|
|
10
|
+
def try_effects = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# "This creature can't be blocked by creatures with power 2 or less" / "... by artifact creatures".
|
|
14
|
+
class CantBeBlockedBy < Restriction
|
|
15
|
+
primitive :ability, :cant_be_blocked_by,
|
|
16
|
+
description: "This creature can't be blocked by creatures matching `filter` (checked as blockers " \
|
|
17
|
+
"are declared, with their current characteristics).",
|
|
18
|
+
rules: %w[509.1b 509.1a],
|
|
19
|
+
params: { "filter" => "a creature filter, e.g. {\"creature\": {\"power\": {\"at_most\": 2}}}" },
|
|
20
|
+
example: { "cant_be_blocked_by" => { "filter" => { "creature" => { "power" => { "at_most" => 2 } } } } }
|
|
21
|
+
|
|
22
|
+
attr_reader :filter
|
|
23
|
+
|
|
24
|
+
def self.build(params, errors, path, **)
|
|
25
|
+
Mechanics.check_keys(params, %w[filter], errors, path)
|
|
26
|
+
raw = Mechanics.fetch_param(params, :filter)
|
|
27
|
+
errors << "#{path}.filter: required" if raw.nil?
|
|
28
|
+
filter = raw && Mechanics.build_entry(:filter, raw, errors, "#{path}.filter")
|
|
29
|
+
new(filter)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(filter)
|
|
33
|
+
super()
|
|
34
|
+
@filter = filter
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def kind = :cant_be_blocked_by
|
|
39
|
+
def allowed_types = [:creature]
|
|
40
|
+
def to_s = "can't be blocked by #{filter.is_a?(Class) ? filter.primitive_name : filter}"
|
|
41
|
+
|
|
42
|
+
# Whether `blocker` may not block this creature (controller-relative filter keys use the blocker's).
|
|
43
|
+
def forbids?(engine, blocker) = filter.match?(engine, blocker, engine.object(blocker).controller)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# "This token can block only creatures with flying" / "This creature can't block non-Spirit creatures".
|
|
47
|
+
class CanBlockOnly < Restriction
|
|
48
|
+
primitive :ability, :can_block_only,
|
|
49
|
+
description: "This creature can block only attacking creatures matching `filter` (checked as " \
|
|
50
|
+
"blockers are declared, with their current characteristics; controller-relative " \
|
|
51
|
+
"filter keys are seen from this creature's controller).",
|
|
52
|
+
rules: %w[509.1b 509.1a],
|
|
53
|
+
params: { "filter" => "a creature filter, e.g. {\"creature\": {\"keywords\": [\"flying\"]}}" },
|
|
54
|
+
example: { "can_block_only" => { "filter" => { "creature" => { "keywords" => ["flying"] } } } }
|
|
55
|
+
|
|
56
|
+
attr_reader :filter
|
|
57
|
+
|
|
58
|
+
def self.build(params, errors, path, **)
|
|
59
|
+
Mechanics.check_keys(params, %w[filter], errors, path)
|
|
60
|
+
raw = Mechanics.fetch_param(params, :filter)
|
|
61
|
+
errors << "#{path}.filter: required" if raw.nil?
|
|
62
|
+
new(raw && Mechanics.build_entry(:filter, raw, errors, "#{path}.filter"))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def initialize(filter)
|
|
66
|
+
super()
|
|
67
|
+
@filter = filter
|
|
68
|
+
freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def kind = :can_block_only
|
|
72
|
+
def allowed_types = [:creature]
|
|
73
|
+
def to_s = "can block only #{filter.is_a?(Class) ? filter.primitive_name : filter}"
|
|
74
|
+
|
|
75
|
+
# Whether this creature (`blocker`) may block `attacker`.
|
|
76
|
+
def allows?(engine, blocker, attacker) = filter.match?(engine, attacker, engine.object(blocker).controller)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# "This creature can't be the target of abilities from artifact sources your opponents control."
|
|
80
|
+
class CantBeTargetedBy < Restriction
|
|
81
|
+
primitive :ability, :cant_be_targeted_by,
|
|
82
|
+
description: "This permanent can't be the target of abilities its controller's opponents control " \
|
|
83
|
+
"whose source matches `abilities_from` (a card spec, e.g. {\"types\": [\"artifact\"]}).",
|
|
84
|
+
rules: %w[115.4 113.7 702.11d],
|
|
85
|
+
params: { "abilities_from" => "card spec of the ability's source" },
|
|
86
|
+
example: { "cant_be_targeted_by" => { "abilities_from" => { "types" => ["artifact"] } } }
|
|
87
|
+
|
|
88
|
+
attr_reader :spec
|
|
89
|
+
|
|
90
|
+
def self.build(params, errors, path, **)
|
|
91
|
+
Mechanics.check_keys(params, %w[abilities_from], errors, path)
|
|
92
|
+
raw = Mechanics.fetch_param(params, :abilities_from)
|
|
93
|
+
errors << "#{path}.abilities_from: required" if raw.nil?
|
|
94
|
+
new(CardSpec.build(raw, errors, "#{path}.abilities_from"))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def initialize(spec)
|
|
98
|
+
super()
|
|
99
|
+
@spec = spec
|
|
100
|
+
freeze
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def kind = :cant_be_targeted_by
|
|
104
|
+
def to_s = "can't be the target of abilities from #{spec.to_s.delete_suffix(" card")} sources opponents control"
|
|
105
|
+
|
|
106
|
+
# Whether an ability whose source is `source` (a card object) may not target this permanent.
|
|
107
|
+
def forbids?(engine, source) = spec.match?(engine, source)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# "You can't cast this spell unless there are seven or more cards in your graveyard" / "You can't cast this
|
|
111
|
+
# spell during your first, second, or third turns of the game" (CR 601.3): the card can be cast, from any
|
|
112
|
+
# zone and by any permission, only while the condition holds for the player who would cast it.
|
|
113
|
+
class CastRestriction < Restriction
|
|
114
|
+
primitive :ability, :cast_restriction,
|
|
115
|
+
description: "This card can be cast only while `only_if` (a condition, checked for the player who " \
|
|
116
|
+
"would cast it) holds, whatever lets them cast it.",
|
|
117
|
+
rules: %w[601.3 101.2],
|
|
118
|
+
params: { "only_if" => "condition" },
|
|
119
|
+
example: { "cast_restriction" => { "only_if" => { "amount" => { "graveyard_size" => "you" }, "at_least" => 7 } } }
|
|
120
|
+
|
|
121
|
+
attr_reader :condition
|
|
122
|
+
|
|
123
|
+
def self.build(params, errors, path, **)
|
|
124
|
+
Mechanics.check_keys(params, %w[only_if], errors, path)
|
|
125
|
+
raw = Mechanics.fetch_param(params, :only_if)
|
|
126
|
+
errors << "#{path}.only_if: required" if raw.nil?
|
|
127
|
+
new(raw && Condition.build(raw, errors, "#{path}.only_if"))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def initialize(condition)
|
|
131
|
+
super()
|
|
132
|
+
@condition = condition
|
|
133
|
+
freeze
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def kind = :cast_restriction
|
|
137
|
+
def to_s = "You can't cast this spell unless #{condition}."
|
|
138
|
+
|
|
139
|
+
# Whether player `index` may begin to cast `obj` now.
|
|
140
|
+
def allows?(engine, index, obj) = condition.nil? || condition.true?(engine, EffectContext.new(index, obj.id, obj.id, nil, []))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Abilities
|
|
6
|
+
# "Instant and sorcery spells you cast from your hand have cascade" (CR 604.1, 611.3a): a static ability of a
|
|
7
|
+
# permanent giving keywords to the spells its controller controls on the stack that match `spells` (and were
|
|
8
|
+
# cast `from` / `not_from` a zone). See Engine::SpellGrants.
|
|
9
|
+
class SpellKeywords < Primitive
|
|
10
|
+
primitive :ability, :spell_keywords,
|
|
11
|
+
description: "Spells you control on the stack matching `spells` (a spell filter; default every " \
|
|
12
|
+
"spell), cast `from` / `not_from` a zone if given, have the `keywords` (e.g. cascade, " \
|
|
13
|
+
"which then triggers as such a spell is cast).",
|
|
14
|
+
rules: %w[604.1 611.3a 702.85a],
|
|
15
|
+
params: { "spells" => "spell filter", "from" => "zone", "not_from" => "zone",
|
|
16
|
+
"keywords" => "list of keywords" },
|
|
17
|
+
example: { "spell_keywords" => { "spells" => { "spell" => { "any_of" => %w[instant sorcery] } },
|
|
18
|
+
"from" => "hand", "keywords" => ["cascade"] } }
|
|
19
|
+
|
|
20
|
+
attr_reader :keywords
|
|
21
|
+
|
|
22
|
+
def self.build(params, errors, path, **)
|
|
23
|
+
Mechanics.check_keys(params, %w[spells from not_from keywords], errors, path)
|
|
24
|
+
filter = params["spells"] && Mechanics.build_entry(:filter, params["spells"], errors, "#{path}.spells")
|
|
25
|
+
keywords = Array(params["keywords"]).map(&:to_s)
|
|
26
|
+
errors << "#{path}.keywords: needs at least one keyword" if keywords.empty?
|
|
27
|
+
keywords.each { errors << "#{path}.keywords: unknown keyword #{it.inspect}" unless Enjine.registry.fetch(:keyword, it) }
|
|
28
|
+
new(filter, CastFrom.build(params, errors, path), keywords.map(&:to_sym).freeze)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(filter, zones, keywords)
|
|
32
|
+
super()
|
|
33
|
+
@filter = filter
|
|
34
|
+
@zones = zones
|
|
35
|
+
@keywords = keywords
|
|
36
|
+
freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def kind = :spell_keywords
|
|
40
|
+
def mana_ability? = false
|
|
41
|
+
def allowed_types = nil
|
|
42
|
+
def try_effects = []
|
|
43
|
+
|
|
44
|
+
# `spell`: a spell on the stack controlled by the controller of `source`.
|
|
45
|
+
def applies_to?(engine, spell, source)
|
|
46
|
+
return false if spell.ability? || !@zones.match?(engine.cast_origin(spell))
|
|
47
|
+
|
|
48
|
+
@filter.nil? || @filter.match_for?(engine, spell.id, source.controller, source.id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_s = "#{@filter || "Spells"} you cast#{@zones} have #{keywords.join(", ")}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|