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,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
# The context an effect runs in.
|
|
6
|
+
# `results` lists, per earlier effect of the same effect list, the objects that effect
|
|
7
|
+
# moved or created (CR 400.7j: "the card exiled this way"); see Selector {"result": n}.
|
|
8
|
+
# `amounts`: per earlier effect, how much it changed (life an earlier effect took); see Amount {"this_way": n}.
|
|
9
|
+
# `memory`: stands in for the resolving ability's memory while a trigger condition is checked (CR 603.4).
|
|
10
|
+
EffectContext = Struct.new(:controller, :source, :object, :targets, :results, :amounts, :memory)
|
|
11
|
+
|
|
12
|
+
# Data form of "who/what does this effect apply to":
|
|
13
|
+
# {"target": 0} the first chosen target
|
|
14
|
+
# "self" the source permanent, or the source card while it's still in
|
|
15
|
+
# the graveyard, hand or exile ("return this card from your
|
|
16
|
+
# graveyard", CR 113.6m; it's a new object once it moves, CR 400.7)
|
|
17
|
+
# "you" the controller
|
|
18
|
+
# "each_opponent" every opponent
|
|
19
|
+
# "attached" what the source is attached to ("enchanted creature", CR 303.4m)
|
|
20
|
+
# {"each": "creature", "controller": "you"} every matching object (you | opponent | any)
|
|
21
|
+
# {"result": 0} what effect 0 of this list moved or created (CR 400.7j)
|
|
22
|
+
# {"controller_of": selector} the controller of each referenced object ("its controller");
|
|
23
|
+
# one that has left the battlefield as it last existed (CR 608.2h)
|
|
24
|
+
# {"triggering": "object" | "player"} what the trigger event was about ("that creature",
|
|
25
|
+
# "that player", CR 603.2); see Triggers.event_facts
|
|
26
|
+
# {"triggering": "targets"} the targets of the spell a cast trigger saw that matched its
|
|
27
|
+
# `targets` filter ("those creatures", CR 601.2c)
|
|
28
|
+
# {"triggering": "ability"} the activated or triggered ability on the stack the trigger
|
|
29
|
+
# event was about ("copy that ability", CR 707.10)
|
|
30
|
+
# "watched" what a delayed triggered ability watches (CR 603.7c)
|
|
31
|
+
# "exiled_with" cards in exile exiled with the source (linked abilities, CR 607.2a)
|
|
32
|
+
# "gift_recipient" the opponent chosen for the spell's gift (CR 702.174a)
|
|
33
|
+
# "granter" the permanent whose static ability granted this ability ("Return
|
|
34
|
+
# Trusty Boomerang to its owner's hand", CR 607.1a), while it's still
|
|
35
|
+
# on the battlefield; the source itself for an ability it has printed
|
|
36
|
+
class Selector
|
|
37
|
+
CONTROLLERS = %w[you opponent any].freeze
|
|
38
|
+
TRIGGERING = %w[object player targets ability].freeze
|
|
39
|
+
# Where "self" finds its source (CR 113.6m); a source that changed zones is a new object (CR 400.7).
|
|
40
|
+
SELF_ZONES = %i[battlefield graveyard exile hand].freeze
|
|
41
|
+
|
|
42
|
+
attr_reader :kind, :value, :controller
|
|
43
|
+
|
|
44
|
+
def self.build(data, errors, path, targets_count:, results_count: 0)
|
|
45
|
+
case data
|
|
46
|
+
when "self", "you", "each_opponent", "attached", "watched", "exiled_with", "gift_recipient", "granter" then new(data.to_sym)
|
|
47
|
+
when Hash
|
|
48
|
+
data = data.transform_keys(&:to_s)
|
|
49
|
+
return build_result(data, errors, path, results_count) if data.key?("result")
|
|
50
|
+
return build_controller_of(data, errors, path, targets_count, results_count) if data.key?("controller_of")
|
|
51
|
+
return build_triggering(data, errors, path) if data.key?("triggering")
|
|
52
|
+
|
|
53
|
+
build_hash(data, errors, path, targets_count)
|
|
54
|
+
else
|
|
55
|
+
errors << "#{path}: must be {\"target\": n}, \"self\", \"you\", \"each_opponent\", \"attached\" or {\"each\": filter}"
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# {"result": n}: n must name an earlier effect of the same list (a nested list sees its parent's).
|
|
61
|
+
def self.build_result(data, errors, path, results_count)
|
|
62
|
+
Mechanics.check_keys(data, %w[result], errors, path)
|
|
63
|
+
index = data["result"]
|
|
64
|
+
unless index.is_a?(Integer) && index >= 0 && index < results_count
|
|
65
|
+
errors << "#{path}.result: must refer to one of the #{results_count} earlier effects"
|
|
66
|
+
end
|
|
67
|
+
new(:result, index)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.build_controller_of(data, errors, path, targets_count, results_count)
|
|
71
|
+
Mechanics.check_keys(data, %w[controller_of], errors, path)
|
|
72
|
+
inner = build(data["controller_of"], errors, "#{path}.controller_of", targets_count: targets_count,
|
|
73
|
+
results_count: results_count)
|
|
74
|
+
new(:controller_of, inner)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.build_triggering(data, errors, path)
|
|
78
|
+
Mechanics.check_keys(data, %w[triggering], errors, path)
|
|
79
|
+
errors << "#{path}.triggering: must be one of #{TRIGGERING.join(", ")}" unless TRIGGERING.include?(data["triggering"])
|
|
80
|
+
new(:triggering, data["triggering"])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.build_hash(data, errors, path, targets_count)
|
|
84
|
+
if data.key?("target")
|
|
85
|
+
index = data["target"]
|
|
86
|
+
unless index.is_a?(Integer) && index >= 0 && index < targets_count
|
|
87
|
+
errors << "#{path}.target: must refer to one of the #{targets_count} declared targets"
|
|
88
|
+
end
|
|
89
|
+
Mechanics.check_keys(data, %w[target], errors, path)
|
|
90
|
+
new(:target, index)
|
|
91
|
+
elsif data.key?("each")
|
|
92
|
+
Mechanics.check_keys(data, %w[each controller], errors, path)
|
|
93
|
+
filter = Mechanics.build_entry(:filter, data["each"], errors, "#{path}.each")
|
|
94
|
+
controller = data.fetch("controller", "any")
|
|
95
|
+
errors << "#{path}.controller: must be one of #{CONTROLLERS.join(", ")}" unless CONTROLLERS.include?(controller)
|
|
96
|
+
new(:each, filter, controller)
|
|
97
|
+
else
|
|
98
|
+
errors << "#{path}: needs \"target\" or \"each\""
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def initialize(kind, value = nil, controller = nil)
|
|
104
|
+
@kind = kind
|
|
105
|
+
@value = value
|
|
106
|
+
@controller = controller
|
|
107
|
+
freeze
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_s
|
|
111
|
+
case kind
|
|
112
|
+
when :target then "target ##{value + 1}"
|
|
113
|
+
when :result then "the object(s) from effect ##{value + 1}"
|
|
114
|
+
when :controller_of then "the controller of #{value}"
|
|
115
|
+
when :self then "itself"
|
|
116
|
+
when :attached then "the attached permanent"
|
|
117
|
+
when :triggering then "the triggering #{value}"
|
|
118
|
+
when :watched then "the watched object(s)"
|
|
119
|
+
when :you then "you"
|
|
120
|
+
when :each_opponent then "each opponent"
|
|
121
|
+
when :exiled_with then "the cards exiled with it"
|
|
122
|
+
when :gift_recipient then "the chosen opponent"
|
|
123
|
+
when :granter then "the permanent granting it"
|
|
124
|
+
when :each then "each #{value.primitive_name.tr("_",
|
|
125
|
+
" ")}#{{ "you" => " you control",
|
|
126
|
+
"opponent" => " an opponent controls" }[controller]}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Resolves to a list of references (object ids / "player:<name>") at resolution time.
|
|
131
|
+
def resolve(engine, ctx)
|
|
132
|
+
case kind
|
|
133
|
+
when :target then Array(ctx.targets[value]).compact # a counted target's slot holds a list (CR 601.2c)
|
|
134
|
+
when :result then Array(ctx.results&.[](value))
|
|
135
|
+
when :self then SELF_ZONES.include?(engine.state.objects[ctx.source]&.zone) ? [ctx.source] : []
|
|
136
|
+
when :attached then attached(engine, ctx)
|
|
137
|
+
when :triggering then triggering(engine, ctx)
|
|
138
|
+
when :watched, :gift_recipient then remembered(engine, ctx)
|
|
139
|
+
when :you then [Filters.player_ref(engine, ctx.controller)]
|
|
140
|
+
when :each_opponent
|
|
141
|
+
engine.state.players.each_index.reject { it == ctx.controller }.map { Filters.player_ref(engine, it) }
|
|
142
|
+
when :each then each(engine, ctx)
|
|
143
|
+
when :controller_of then controllers(engine, ctx)
|
|
144
|
+
when :exiled_with then Linked.exiled_with(engine, ctx.source, ctx)
|
|
145
|
+
when :granter then granter(engine, ctx)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# What the resolving triggered ability remembers (the event facts, watched objects).
|
|
150
|
+
def self.memory(engine, ctx) = ctx.memory || engine.state.objects[ctx.object]&.memory || {}
|
|
151
|
+
|
|
152
|
+
# What a delayed trigger watches; the opponent chosen for a gift, kept by the spell or the permanent it
|
|
153
|
+
# became (CR 702.174b).
|
|
154
|
+
def remembered(engine, ctx)
|
|
155
|
+
return Array(Selector.memory(engine, ctx)["watched"]) if kind == :watched
|
|
156
|
+
|
|
157
|
+
Array(Selector.memory(engine, ctx)["gift"] || engine.state.objects[ctx.source]&.memory&.[]("gift"))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def controllers(engine, ctx)
|
|
163
|
+
Array(value&.resolve(engine, ctx)).filter_map do |ref|
|
|
164
|
+
obj = engine.state.objects[ref]
|
|
165
|
+
controller = if obj && %i[battlefield
|
|
166
|
+
stack].include?(obj.zone)
|
|
167
|
+
obj.controller
|
|
168
|
+
else
|
|
169
|
+
engine.resolving_last_known(ref)&.fetch("controller") ||
|
|
170
|
+
engine.state.objects[ctx.object]&.memory&.dig(Linked::LKI, ref) || obj&.controller
|
|
171
|
+
end
|
|
172
|
+
controller && Filters.player_ref(engine, controller)
|
|
173
|
+
end.uniq
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def granter(engine, ctx)
|
|
177
|
+
id = engine.state.objects[ctx.object]&.memory&.[]("granter") || ctx.source
|
|
178
|
+
engine.state.battlefield.include?(id) ? [id] : []
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def triggering(engine, ctx)
|
|
182
|
+
found = Selector.memory(engine, ctx).fetch("triggering", {})[value]
|
|
183
|
+
return [] if found.nil?
|
|
184
|
+
|
|
185
|
+
case value
|
|
186
|
+
when "player" then [Filters.player_ref(engine, found)]
|
|
187
|
+
when "ability" then [engine.stack_object_for(found)].compact
|
|
188
|
+
else Array(found)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# CR 301.5f / 303.4m: "equipped/enchanted creature" is whatever the source is attached to now.
|
|
193
|
+
def attached(engine, ctx)
|
|
194
|
+
to = engine.state.objects[ctx.source]&.attached_to
|
|
195
|
+
to && engine.state.battlefield.include?(to) ? [to] : []
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def each(engine, ctx)
|
|
199
|
+
value.candidates_for(engine, ctx.controller, ctx.source, ctx: ctx).select do |ref|
|
|
200
|
+
index = Filters.player_index(engine, ref)
|
|
201
|
+
owner = index || engine.object(ref).controller
|
|
202
|
+
controller == "any" || (controller == "you") == (owner == ctx.controller)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Subtypes
|
|
6
|
+
# Auras and Equipment (CR 301.5, 303.4, 701.3, 702.5, 702.6). A permanent is attached to
|
|
7
|
+
# another through GameObject#attached_to; "equipped/enchanted creature" abilities refer to
|
|
8
|
+
# whatever the source is attached to (CR 301.5f, 303.4m). The engine side (legality,
|
|
9
|
+
# attach!/unattach!, state-based actions 704.5m/n/p) is in Engine::Attachments.
|
|
10
|
+
# Shared rules of the subtypes that make a permanent attachable.
|
|
11
|
+
class AttachmentSubtype < Primitive
|
|
12
|
+
def self.validate_definition(_defn, _errors) = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Aura (CR 303.4): an enchantment that enters attached to what its spell targeted.
|
|
16
|
+
class Aura < AttachmentSubtype
|
|
17
|
+
primitive :subtype, :aura,
|
|
18
|
+
description: "An enchantment that is cast targeting what its `enchant` ability allows and enters " \
|
|
19
|
+
"the battlefield attached to that target. If it's attached to something it can't " \
|
|
20
|
+
"enchant, or to nothing, it's put into its owner's graveyard (a state-based action).",
|
|
21
|
+
rules: %w[303.4 303.4a 303.4c 303.4d 608.3c 704.5m],
|
|
22
|
+
example: { "types" => ["enchantment"], "subtypes" => ["aura"], "abilities" => [{ "enchant" => "creature" }] }
|
|
23
|
+
|
|
24
|
+
def self.applies_to = [:enchantment]
|
|
25
|
+
|
|
26
|
+
# CR 303.4a: an Aura spell requires a target, defined by its enchant ability.
|
|
27
|
+
def self.validate_definition(defn, errors)
|
|
28
|
+
count = defn.abilities.count { it.kind == :enchant }
|
|
29
|
+
errors << "abilities: an Aura needs exactly one enchant ability (CR 303.4a, 702.5a)" unless count == 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Equipment (CR 301.5): an artifact that can be attached to a creature, usually by equip.
|
|
34
|
+
class Equipment < AttachmentSubtype
|
|
35
|
+
primitive :subtype, :equipment,
|
|
36
|
+
description: "An artifact that can be attached to a creature (it enters unattached). If the " \
|
|
37
|
+
"creature it equips stops being a creature or leaves, it becomes unattached and " \
|
|
38
|
+
"stays on the battlefield (a state-based action).",
|
|
39
|
+
rules: %w[301.5 301.5b 301.5c 704.5n],
|
|
40
|
+
example: { "types" => ["artifact"], "subtypes" => ["equipment"],
|
|
41
|
+
"abilities" => [{ "equip" => { "cost" => [{ "mana" => "{2}" }] } }] }
|
|
42
|
+
|
|
43
|
+
def self.applies_to = [:artifact]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module Abilities
|
|
48
|
+
# Enchant [object] (CR 702.5): what an Aura spell can target and what the Aura can enchant.
|
|
49
|
+
class Enchant < Primitive
|
|
50
|
+
primitive :ability, :enchant,
|
|
51
|
+
description: "The Aura's enchant ability: a target filter for what its spell targets and what the " \
|
|
52
|
+
"Aura may be attached to, e.g. \"creature\" or {\"permanent\": {\"any_of\": " \
|
|
53
|
+
"[\"artifact\", \"creature\"]}}. Only Auras have it.",
|
|
54
|
+
rules: %w[702.5a 115.1b 303.4a],
|
|
55
|
+
params: { "(value)" => "a target filter for a permanent" },
|
|
56
|
+
example: { "enchant" => "creature" }
|
|
57
|
+
|
|
58
|
+
PLAYER_FILTERS = %w[player opponent any_target].freeze
|
|
59
|
+
|
|
60
|
+
attr_reader :filter
|
|
61
|
+
|
|
62
|
+
def self.scalar_params? = true
|
|
63
|
+
|
|
64
|
+
def self.build(params, errors, path, **)
|
|
65
|
+
filter = Mechanics.build_entry(:filter, params, errors, path)
|
|
66
|
+
message = "must be a filter for permanents (Auras that enchant players are not supported yet)"
|
|
67
|
+
errors << "#{path}: #{message}" unless permanents?(filter)
|
|
68
|
+
new(filter)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether the filter matches only permanents ("Enchant creature or Vehicle" is a one_of of two).
|
|
72
|
+
def self.permanents?(filter)
|
|
73
|
+
return filter.filters.all? { permanents?(it) } if filter.respond_to?(:filters)
|
|
74
|
+
|
|
75
|
+
base = filter.respond_to?(:base) ? filter.base : filter
|
|
76
|
+
base.nil? || (base.zone == :battlefield && !PLAYER_FILTERS.include?(base.primitive_name))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def initialize(filter)
|
|
80
|
+
super()
|
|
81
|
+
@filter = filter
|
|
82
|
+
freeze
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def kind = :enchant
|
|
86
|
+
def mana_ability? = false
|
|
87
|
+
def allowed_types = [:enchantment]
|
|
88
|
+
def try_effects = []
|
|
89
|
+
|
|
90
|
+
def validate_definition(defn, errors, path)
|
|
91
|
+
errors << "#{path}: only Auras have enchant (CR 702.5a)" unless defn.subtypes.include?(:aura)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_s = "Enchant #{filter.is_a?(Class) ? filter.primitive_name.tr("_", " ") : filter}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The spell ability an Aura spell has implicitly (CR 115.1b, 303.4a): one target chosen
|
|
98
|
+
# by its enchant ability, no effects. Resolving attaches the Aura to it (CR 608.3c).
|
|
99
|
+
class AuraSpell < StackAbility
|
|
100
|
+
def self.for(enchant) = new(targets: [enchant.filter].freeze, effects: [].freeze)
|
|
101
|
+
|
|
102
|
+
def kind = :aura_spell
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Equip [cost] (CR 702.6a): "[Cost]: Attach this permanent to target creature you control.
|
|
106
|
+
# Activate only as a sorcery."
|
|
107
|
+
class Equip < ActivatedAbility
|
|
108
|
+
primitive :ability, :equip,
|
|
109
|
+
description: "Equip: pay `cost` to attach this Equipment to target creature you control (not " \
|
|
110
|
+
"itself). Sorcery speed. Only Equipment have it.",
|
|
111
|
+
rules: %w[702.6a 301.5b 301.5c 701.3a],
|
|
112
|
+
params: { "cost" => "list of costs, e.g. [{\"mana\": \"{2}\"}]" },
|
|
113
|
+
example: { "equip" => { "cost" => [{ "mana" => "{2}" }] } }
|
|
114
|
+
|
|
115
|
+
TARGET = { "creature" => { "controller" => "you", "other" => true } }.freeze
|
|
116
|
+
EFFECT = { "attach" => { "to" => { "target" => 0 } } }.freeze
|
|
117
|
+
|
|
118
|
+
def self.build(params, errors, path, **)
|
|
119
|
+
Mechanics.check_keys(params, %w[cost], errors, path)
|
|
120
|
+
costs = parse_costs(params, errors, path)
|
|
121
|
+
errors << "#{path}.cost: must be a non-empty list" if costs.empty? && errors.none? { it.start_with?("#{path}.cost") }
|
|
122
|
+
targets = [Mechanics.build_entry(:filter, TARGET, errors, path)].freeze
|
|
123
|
+
effects = [Mechanics.build_entry(:effect, EFFECT, errors, path, targets_count: 1)].freeze
|
|
124
|
+
new(targets: targets, effects: effects, costs: costs, sorcery: true)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def allowed_types = [:artifact]
|
|
128
|
+
|
|
129
|
+
def validate_definition(defn, errors, path)
|
|
130
|
+
errors << "#{path}: only Equipment have equip (CR 702.6a)" unless defn.subtypes.include?(:equipment)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_s = "Equip #{costs.join(", ")}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
module Effects
|
|
138
|
+
# Attach the source Aura or Equipment to a permanent (CR 701.3).
|
|
139
|
+
class Attach < Effect
|
|
140
|
+
primitive :effect, :attach,
|
|
141
|
+
description: "Attach this Aura or Equipment (the ability's source), or the `attachment` (e.g. a token " \
|
|
142
|
+
"an earlier effect created, {\"result\": n}), to the referenced permanent. Nothing " \
|
|
143
|
+
"happens if it can't legally be attached to it, if it's already attached to it, or if " \
|
|
144
|
+
"the attachment has left the battlefield.",
|
|
145
|
+
rules: %w[701.3a 701.3b 701.3c 301.5c],
|
|
146
|
+
params: { "to" => "selector naming one permanent: {\"target\": n}, \"self\" or {\"result\": n}",
|
|
147
|
+
"attachment" => "selector (default: the source)" },
|
|
148
|
+
example: { "attach" => { "to" => { "target" => 0 } } }
|
|
149
|
+
|
|
150
|
+
SINGLE = %i[target self result].freeze
|
|
151
|
+
|
|
152
|
+
def self.keys = %w[to attachment if]
|
|
153
|
+
|
|
154
|
+
def setup(params, errors, path)
|
|
155
|
+
if to && !SINGLE.include?(to.kind)
|
|
156
|
+
errors << "#{path}.to: must be {\"target\": n}, \"self\" or {\"result\": n} (an attachment equips one object, CR 301.5c)"
|
|
157
|
+
end
|
|
158
|
+
raw = Mechanics.fetch_param(params, :attachment)
|
|
159
|
+
@attachment = raw && Selector.build(raw, errors, "#{path}.attachment", targets_count: @targets_count,
|
|
160
|
+
results_count: @results_count)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def apply(engine, ctx)
|
|
164
|
+
target = objects(engine, ctx).first or return
|
|
165
|
+
ids = @attachment ? @attachment.resolve(engine, ctx) : [ctx.source]
|
|
166
|
+
ids.each do |id|
|
|
167
|
+
source = engine.state.objects[id]
|
|
168
|
+
engine.attach!(source, target) if source && engine.state.battlefield.include?(id)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def to_s = "attach to #{to}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Subtypes
|
|
6
|
+
# Shared behavior of the five basic land types (CR 305.6): each grants an
|
|
7
|
+
# intrinsic "{T}: Add <color>." ability.
|
|
8
|
+
class BasicLandType < Primitive
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :color
|
|
11
|
+
|
|
12
|
+
def define(name, color)
|
|
13
|
+
klass = Class.new(self)
|
|
14
|
+
klass.instance_variable_set(:@color, color)
|
|
15
|
+
Subtypes.const_set(name.capitalize, klass)
|
|
16
|
+
klass.primitive :subtype, name,
|
|
17
|
+
description: "Basic land type. A land with this subtype has \"{T}: Add {#{color}}.\"",
|
|
18
|
+
rules: %w[305.6 205.3i],
|
|
19
|
+
example: { "types" => ["land"], "subtypes" => [name.to_s] }
|
|
20
|
+
klass
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def applies_to = [:land]
|
|
24
|
+
|
|
25
|
+
def intrinsic_abilities
|
|
26
|
+
@intrinsic_abilities ||= [Abilities::ManaAbility.new([Costs::Tap::INSTANCE].freeze, [color].freeze)].freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
{ plains: :W, island: :U, swamp: :B, mountain: :R, forest: :G }.each do |name, color|
|
|
32
|
+
BasicLandType.define(name, color)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../triggers/base"
|
|
4
|
+
require_relative "../keywords/station"
|
|
5
|
+
|
|
6
|
+
module Enjine
|
|
7
|
+
module Mechanics
|
|
8
|
+
module Effects
|
|
9
|
+
# The effect of a class level bar's activated ability (CR 717.2a): "This Class's level becomes N."
|
|
10
|
+
class GainClassLevel < Effect
|
|
11
|
+
def self.keys = %w[to]
|
|
12
|
+
|
|
13
|
+
attr_reader :level
|
|
14
|
+
|
|
15
|
+
def setup(params, _errors, _path) = @level = params["level"]
|
|
16
|
+
def apply(engine, ctx) = objects(engine, ctx).each { engine.set_class_level!(engine.object(it), level) }
|
|
17
|
+
def to_s = "this Class's level becomes #{level}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module Abilities
|
|
22
|
+
# A class level bar "[Cost]: Level N — [Abilities]" (CR 717.2a): "[Cost]: This Class's level becomes N.
|
|
23
|
+
# Activate only if this Class is level N-1 and only as a sorcery" and "As long as this Class is level N or
|
|
24
|
+
# greater, it has [abilities]." Abilities printed in its text box section are its `abilities` and
|
|
25
|
+
# `keywords`; those above the first level bar are ordinary abilities (CR 717.3).
|
|
26
|
+
class ClassLevel < ActivatedAbility
|
|
27
|
+
primitive :ability, :class_level,
|
|
28
|
+
description: "A class level bar: `cost` (a mana cost string or a list of costs): this permanent's " \
|
|
29
|
+
"level becomes `level`; activate only if its level is `level` - 1, as a sorcery. While " \
|
|
30
|
+
"its level is `level` or greater it has `keywords` and `abilities`. A permanent " \
|
|
31
|
+
"without a level is level 1.",
|
|
32
|
+
rules: %w[717.2a 717.2b 717.2d 717.3 602.5d],
|
|
33
|
+
params: { "level" => "integer >= 2", "cost" => "mana cost string or list of costs",
|
|
34
|
+
"keywords" => "list of keywords", "abilities" => "list of abilities" },
|
|
35
|
+
example: { "class_level" => { "level" => 2, "cost" => "{1}{R}",
|
|
36
|
+
"abilities" => [{ "triggered" => {
|
|
37
|
+
"when" => { "discards" => {} },
|
|
38
|
+
"effects" => [{ "deal_damage" => { "amount" => 2, "to" => "each_opponent" } }]
|
|
39
|
+
} }] } }
|
|
40
|
+
|
|
41
|
+
attr_reader :level, :keywords, :nested_abilities
|
|
42
|
+
|
|
43
|
+
def self.build(params, errors, path, **)
|
|
44
|
+
Mechanics.check_keys(params, %w[level cost keywords abilities], errors, path)
|
|
45
|
+
level = Mechanics.integer_param(params, :level, errors, path, min: 2).to_i
|
|
46
|
+
raw = Mechanics.fetch_param(params, :cost)
|
|
47
|
+
costs = raw.is_a?(String) ? [Costs::ManaPayment.build(raw, errors, "#{path}.cost")] : parse_costs(params, errors, path)
|
|
48
|
+
errors << "#{path}.cost: required" if raw.nil?
|
|
49
|
+
keywords = StationSymbol.build_keywords(params, errors, path)
|
|
50
|
+
nested = StationSymbol.build_abilities(params, errors, path)
|
|
51
|
+
nested.each_with_index do |ability, i|
|
|
52
|
+
errors << "#{path}.abilities[#{i}]: class level bars can't be nested" if ability.is_a?(ClassLevel)
|
|
53
|
+
end
|
|
54
|
+
effect = Effects::GainClassLevel.new(to: Selector.new(:self), params: { "level" => level }, errors: errors, path: path)
|
|
55
|
+
new(sorcery: true, targets: [].freeze, costs: costs.compact.freeze, effects: [effect].freeze,
|
|
56
|
+
level: level, keywords: keywords.freeze, nested_abilities: nested.freeze)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(level:, keywords:, nested_abilities:, **)
|
|
60
|
+
@level = level
|
|
61
|
+
@keywords = keywords
|
|
62
|
+
@nested_abilities = nested_abilities
|
|
63
|
+
super(**)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# No power/toughness box (Engine::Characteristics#station_symbol_layers reads it).
|
|
67
|
+
def power = nil
|
|
68
|
+
|
|
69
|
+
# CR 717.2a: only if this Class is level N-1.
|
|
70
|
+
def restriction_met?(engine, index, source)
|
|
71
|
+
engine.class_level(engine.object(source)) == level - 1 && super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_s
|
|
75
|
+
bits = keywords.map(&:to_s) + nested_abilities.map(&:to_s)
|
|
76
|
+
"#{costs.join(", ")}: Level #{level}#{" — #{bits.join("; ")}" unless bits.empty?}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
module Triggers
|
|
82
|
+
# "When this Class becomes level N" (CR 717.2a): its level bar for N resolved.
|
|
83
|
+
class BecomesLevel < Trigger
|
|
84
|
+
primitive :trigger, :becomes_level,
|
|
85
|
+
description: "This permanent's level becomes `level` (a class level bar's ability resolves).",
|
|
86
|
+
rules: %w[717.2a 603.2],
|
|
87
|
+
params: { "level" => "integer >= 2" },
|
|
88
|
+
example: { "becomes_level" => { "level" => 3 } }
|
|
89
|
+
|
|
90
|
+
def self.keys = %w[level]
|
|
91
|
+
def event_class = Events::ClassLevelChanged
|
|
92
|
+
|
|
93
|
+
def setup(params, errors, path)
|
|
94
|
+
@level = Mechanics.integer_param(params, :level, errors, path, min: 2)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def match?(_engine, event, source) = event.object == source["id"] && event.level == @level
|
|
98
|
+
def to_s = "When this becomes level #{@level}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Subtypes
|
|
6
|
+
# The Saga enchantment type (CR 715).
|
|
7
|
+
class Saga < Primitive
|
|
8
|
+
primitive :subtype, :saga,
|
|
9
|
+
description: "Enchantment type. A Saga enters with a lore counter; as its controller's precombat " \
|
|
10
|
+
"main phase begins, they put another lore counter on it. Each lore counter added can " \
|
|
11
|
+
"trigger its chapter abilities. Once its lore counters reach its final chapter number " \
|
|
12
|
+
"and no chapter ability of it is waiting to resolve, its controller sacrifices it.",
|
|
13
|
+
rules: %w[715.3a 715.3c 505.4 715.4 704.5s],
|
|
14
|
+
example: { "types" => ["enchantment"], "subtypes" => ["saga"],
|
|
15
|
+
"abilities" => [{ "chapter" => { "chapter" => 1, "effects" => [{ "draw" => { "amount" => 1 } }] } }] }
|
|
16
|
+
|
|
17
|
+
def self.applies_to = [:enchantment]
|
|
18
|
+
|
|
19
|
+
# CR 715.3a: "This Saga enters with a lore counter on it" (a replacement effect, CR 614.1c).
|
|
20
|
+
def self.entering(_definition, enters)
|
|
21
|
+
enters["counters"]["lore"] = enters["counters"].fetch("lore", 0) + 1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Triggers
|
|
27
|
+
# The trigger condition of a chapter ability (CR 715.2b): one or more lore counters are put on
|
|
28
|
+
# this Saga and their number goes from less than N to at least N. Entering with lore counters
|
|
29
|
+
# counts as having them put on it (CR 122.6).
|
|
30
|
+
class ChapterTrigger
|
|
31
|
+
attr_reader :chapter
|
|
32
|
+
|
|
33
|
+
def initialize(chapter)
|
|
34
|
+
@chapter = chapter
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def watches?(event) = event.is_a?(Events::CountersChanged) || event.is_a?(Events::ZoneChanged)
|
|
39
|
+
def leaves_battlefield? = false
|
|
40
|
+
|
|
41
|
+
def match?(engine, event, source)
|
|
42
|
+
if event.is_a?(Events::CountersChanged)
|
|
43
|
+
event.object == source["id"] && event.counter == "lore" && event.from < chapter && event.to >= chapter
|
|
44
|
+
else
|
|
45
|
+
entered = event.to == "battlefield" && event.new_id == source["id"]
|
|
46
|
+
entered && engine.state.objects[event.new_id].counters.fetch("lore", 0) >= chapter
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s = "Chapter #{chapter}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
module Abilities
|
|
55
|
+
# A chapter ability "{rN} — [effect]" (CR 715.2): a triggered ability of a Saga. Write
|
|
56
|
+
# "{rN1}, {rN2} — [effect]" as one chapter ability per number (CR 715.2c).
|
|
57
|
+
class ChapterAbility < StackAbility
|
|
58
|
+
primitive :ability, :chapter,
|
|
59
|
+
description: "A Saga's chapter ability: triggers when lore counters put on it bring their number " \
|
|
60
|
+
"from below `chapter` to at least `chapter`. Its targets are chosen as it's put on the " \
|
|
61
|
+
"stack. A Saga's final chapter number is its greatest `chapter`.",
|
|
62
|
+
rules: %w[715.2 715.2b 715.2c 715.2d 122.6],
|
|
63
|
+
params: { "chapter" => "integer >= 1 (the chapter symbol's number)", "targets" => "list of target filters",
|
|
64
|
+
"effects" => "list of effects", "modes" => "optional modes, as for a spell" },
|
|
65
|
+
example: { "chapter" => { "chapter" => 2, "effects" => [{ "gain_life" => { "amount" => 3 } }] } }
|
|
66
|
+
|
|
67
|
+
attr_reader :chapter, :trigger
|
|
68
|
+
|
|
69
|
+
def self.build(params, errors, path, **)
|
|
70
|
+
Mechanics.check_keys(params, %w[chapter] + BODY_KEYS, errors, path)
|
|
71
|
+
chapter = Mechanics.integer_param(params, :chapter, errors, path, min: 1)
|
|
72
|
+
new(**parse_body(params, errors, path), chapter: chapter)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initialize(chapter:, **)
|
|
76
|
+
@chapter = chapter
|
|
77
|
+
@trigger = Triggers::ChapterTrigger.new(chapter)
|
|
78
|
+
super(**)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def kind = :triggered
|
|
82
|
+
def allowed_types = [:enchantment]
|
|
83
|
+
def condition = nil
|
|
84
|
+
def condition_met?(*, **) = true
|
|
85
|
+
def to_s = "Chapter #{chapter} — #{effects_text}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Supertypes
|
|
6
|
+
# Basic (CR 205.4c). Exempt from the four-copy deck limit.
|
|
7
|
+
class Basic < Primitive
|
|
8
|
+
primitive :supertype, :basic,
|
|
9
|
+
description: "Basic cards are exempt from the four-copies deck construction limit.",
|
|
10
|
+
rules: %w[205.4c 100.2a],
|
|
11
|
+
example: { "supertypes" => ["basic"] }
|
|
12
|
+
|
|
13
|
+
def self.deck_limit_exempt? = true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Supertypes
|
|
6
|
+
# Legendary (CR 205.4d). Subject to the legend rule (CR 704.5j).
|
|
7
|
+
class Legendary < Primitive
|
|
8
|
+
primitive :supertype, :legendary,
|
|
9
|
+
description: "If a player controls two or more legendary permanents with the same name, they choose " \
|
|
10
|
+
"one and put the rest into their owners' graveyards (the legend rule).",
|
|
11
|
+
rules: %w[205.4d 704.5j],
|
|
12
|
+
example: { "supertypes" => ["legendary"] }
|
|
13
|
+
|
|
14
|
+
def self.deck_limit_exempt? = false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|