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,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Creates a delayed triggered ability (CR 603.7): "at the beginning of the next end step,
|
|
7
|
+
# return it". It is defined here, in the card data, and addressed as "delayed:<path>".
|
|
8
|
+
class DelayedTrigger < Effect
|
|
9
|
+
primitive :effect, :delayed_trigger,
|
|
10
|
+
description: "Creates a delayed triggered ability with the controller and source of the resolving " \
|
|
11
|
+
"spell or ability. It triggers only once, the next time `when` happens (e.g. " \
|
|
12
|
+
"{\"step_begins\": {\"step\": \"end\", \"whose\": \"each\"}} for the next end step). Its " \
|
|
13
|
+
"`effects` may use {\"result\": n} for what earlier effects of the creating list moved or " \
|
|
14
|
+
"created; those objects are affected only if they are still where they were put.",
|
|
15
|
+
rules: %w[603.7a 603.7b 603.7c 603.7d 603.7e],
|
|
16
|
+
params: { "when" => "a trigger condition", "effects" => "list of effects",
|
|
17
|
+
"watch" => "selector resolved now (e.g. {\"target\": 0}): the objects its trigger subject " \
|
|
18
|
+
"\"watched\" and its effects' selector \"watched\" mean (\"when that creature dies\")",
|
|
19
|
+
"this_turn" => "true: it lasts until the end of this turn and triggers each time " \
|
|
20
|
+
"(\"whenever ... this turn\", CR 603.7b); \"once\": only the next time " \
|
|
21
|
+
"this turn (\"at the beginning of the next combat this turn\")" },
|
|
22
|
+
example: { "delayed_trigger" => { "when" => { "step_begins" => { "step" => "end", "whose" => "each" } },
|
|
23
|
+
"effects" => [{ "sacrifice" => { "to" => { "result" => 0 } } }] } }
|
|
24
|
+
|
|
25
|
+
attr_reader :ref, :delayed_ability
|
|
26
|
+
|
|
27
|
+
def self.keys = %w[when effects watch this_turn]
|
|
28
|
+
def self.default_to = "you"
|
|
29
|
+
|
|
30
|
+
def setup(params, errors, path)
|
|
31
|
+
@ref = "delayed:#{path}"
|
|
32
|
+
raw = Mechanics.fetch_param(params, :when)
|
|
33
|
+
trigger = raw.nil? ? ((errors << "#{path}.when: required") && nil) : Mechanics.build_entry(:trigger, raw, errors, "#{path}.when")
|
|
34
|
+
effects = Abilities::StackAbility.parse_effects(params, errors, path, 0, results_count: @results_count)
|
|
35
|
+
errors << "#{path}.effects: needs at least one effect" if effects.empty?
|
|
36
|
+
@delayed_ability = Abilities::TriggeredAbility.new(targets: [].freeze, effects: effects, trigger: trigger)
|
|
37
|
+
raw_watch = Mechanics.fetch_param(params, :watch)
|
|
38
|
+
@watch = raw_watch && Selector.build(raw_watch, errors, "#{path}.watch", targets_count: @targets_count,
|
|
39
|
+
results_count: @results_count)
|
|
40
|
+
@this_turn = Mechanics.fetch_param(params, :this_turn)
|
|
41
|
+
errors << "#{path}.this_turn: must be true, false or \"once\"" unless [nil, true, false, "once"].include?(@this_turn)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def nested_effects = delayed_ability.effects
|
|
45
|
+
def to_s = "delayed#{" this turn" if @this_turn}: #{delayed_ability}#{" (watching #{@watch})" if @watch}"
|
|
46
|
+
|
|
47
|
+
# CR 603.7d-e: controlled by the resolving object's controller, from the same source. It
|
|
48
|
+
# remembers the results it may name and the objects it watches (CR 603.7c).
|
|
49
|
+
def apply(engine, ctx)
|
|
50
|
+
entry = { "controller" => ctx.controller, "source" => ctx.source, "card" => engine.object(ctx.object).card,
|
|
51
|
+
"ability" => ref, "results" => Snapshot.deep_copy(ctx.results || []) }
|
|
52
|
+
entry["watched"] = @watch.resolve(engine, ctx) if @watch
|
|
53
|
+
entry["turn"] = engine.state.turn if @this_turn
|
|
54
|
+
entry["once"] = true if @this_turn == "once" # CR 603.7b: the next time only, and only this turn
|
|
55
|
+
engine.create_delayed_trigger!(entry)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A reflexive triggered ability (CR 603.12): "When you do, [effect]". Put it where the action it
|
|
60
|
+
# depends on is known to have happened, usually a "may"'s `then`. It triggers at once and goes on the
|
|
61
|
+
# stack the next time a player would receive priority, with its own targets chosen then (CR 603.3d).
|
|
62
|
+
class ReflexiveTrigger < Effect
|
|
63
|
+
primitive :effect, :reflexive_trigger,
|
|
64
|
+
description: "Creates a reflexive triggered ability (\"when you do, ...\") that triggers immediately, " \
|
|
65
|
+
"with the controller and source of the resolving spell or ability. Its `targets` are " \
|
|
66
|
+
"chosen as it's put on the stack; its `effects` may name the results of earlier effects " \
|
|
67
|
+
"of the creating list. With `if`, it triggers only when the condition holds " \
|
|
68
|
+
"(\"when excess damage is dealt this way\").",
|
|
69
|
+
rules: %w[603.12 603.12a 603.7d 603.7e 603.3d],
|
|
70
|
+
params: { "targets" => "list of target filters", "effects" => "list of effects", "if" => "condition",
|
|
71
|
+
"divide" => "amount divided among the targets as it's put on the stack (CR 601.2d)" },
|
|
72
|
+
example: { "reflexive_trigger" => { "targets" => ["any_target"],
|
|
73
|
+
"effects" => [{ "deal_damage" => { "amount" => 2,
|
|
74
|
+
"to" => { "target" => 0 } } }] } }
|
|
75
|
+
|
|
76
|
+
# Stands in for the trigger condition on the stack ability ("When you do").
|
|
77
|
+
WhenYouDo = Data.define do
|
|
78
|
+
def to_s = "When you do"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
attr_reader :ref, :delayed_ability
|
|
82
|
+
|
|
83
|
+
def self.keys = %w[targets effects divide if]
|
|
84
|
+
def self.default_to = "you"
|
|
85
|
+
|
|
86
|
+
def setup(params, errors, path)
|
|
87
|
+
@ref = "reflexive:#{path}"
|
|
88
|
+
targets = Abilities::StackAbility.parse_targets(params, errors, path)
|
|
89
|
+
effects = Abilities::StackAbility.parse_effects(params, errors, path, targets.size, results_count: @results_count)
|
|
90
|
+
errors << "#{path}.effects: needs at least one effect" if effects.empty?
|
|
91
|
+
extras = Abilities::StackAbility.parse_extras(params.slice("divide"), errors, path)
|
|
92
|
+
Abilities::StackAbility.check_divide(extras, targets, errors, path)
|
|
93
|
+
@delayed_ability = Abilities::TriggeredAbility.new(targets: targets, effects: effects, trigger: WhenYouDo.new,
|
|
94
|
+
extras: extras)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def nested_effects = delayed_ability.effects
|
|
98
|
+
def to_s = "reflexive: #{delayed_ability}"
|
|
99
|
+
|
|
100
|
+
def apply(engine, ctx)
|
|
101
|
+
# Created by a replacement's follow-up (Engine::FollowUps), its spell may be long gone: its card is remembered.
|
|
102
|
+
memory = Selector.memory(engine, ctx)
|
|
103
|
+
card = engine.state.objects[ctx.object]&.card || memory["source_card"]
|
|
104
|
+
source = { "id" => ctx.source, "card" => card, "controller" => ctx.controller, "ability" => ref }
|
|
105
|
+
engine.queue_pending_trigger!(source, nil, { "results" => Snapshot.deep_copy(ctx.results || []),
|
|
106
|
+
"amounts" => (ctx.amounts || []).dup,
|
|
107
|
+
"triggering" => memory["triggering"]&.dup }.compact)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Destroy permanents (CR 701.8).
|
|
7
|
+
class Destroy < Effect
|
|
8
|
+
primitive :effect, :destroy, description: "Destroy each referenced permanent (it goes to its owner's graveyard).",
|
|
9
|
+
rules: %w[701.8a], params: { "to" => "selector" },
|
|
10
|
+
example: { "destroy" => { "to" => { "target" => 0 } } }
|
|
11
|
+
def self.keys = %w[to if]
|
|
12
|
+
|
|
13
|
+
# CR 702.12b: indestructible permanents aren't destroyed.
|
|
14
|
+
def apply(engine, ctx)
|
|
15
|
+
objects(engine, ctx).each do |id|
|
|
16
|
+
engine.move!(id, :graveyard, reason: "destroy") unless engine.chars(id).keyword?(:indestructible)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Discard N cards of the player's choice (CR 701.9), at random (CR 701.9b), or the whole hand.
|
|
7
|
+
class Discard < ChoiceEffect
|
|
8
|
+
primitive :effect, :discard, description: "Each referenced player discards `amount` cards of their choice " \
|
|
9
|
+
"(default: you). `random: true`: the cards are chosen at random " \
|
|
10
|
+
"(the game's random number generator), with no decision. " \
|
|
11
|
+
"`hand: true`: each discards their whole hand at once (\"discard " \
|
|
12
|
+
"your hand\"; how many is its amount {\"this_way\": n}).",
|
|
13
|
+
rules: %w[701.9a 701.9b], params: { "amount" => "integer", "to" => "selector",
|
|
14
|
+
"random" => "boolean",
|
|
15
|
+
"hand" => "true: the whole hand" },
|
|
16
|
+
example: { "discard" => { "amount" => 1, "to" => "each_opponent" } }
|
|
17
|
+
def self.keys = %w[amount to if random hand]
|
|
18
|
+
def self.amount_optional?(params) = Mechanics.fetch_param(params, :hand) == true
|
|
19
|
+
def self.default_to = "you"
|
|
20
|
+
|
|
21
|
+
def setup(params, errors, path)
|
|
22
|
+
@random = Mechanics.flag_param(params, :random, errors, path)
|
|
23
|
+
@hand = Mechanics.flag_param(params, :hand, errors, path)
|
|
24
|
+
errors << "#{path}: hand and random can't be combined" if @hand && @random
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s = @hand ? "discard the whole hand → #{to}" : "#{super}#{" at random" if @random}"
|
|
28
|
+
|
|
29
|
+
# CR 118.3: "you may discard a card" needs a card to discard; a hand can always be discarded.
|
|
30
|
+
def possible?(engine, ctx) = @hand || players(engine, ctx).all? { engine.player(it).hand.size >= amount_for(engine, ctx) }
|
|
31
|
+
|
|
32
|
+
def run(engine, ctx, locals)
|
|
33
|
+
return discard_at_random(engine, ctx) if @random
|
|
34
|
+
return discard_hands(engine, ctx) if @hand
|
|
35
|
+
|
|
36
|
+
queue = (locals["players"] ||= players(engine, ctx).flat_map { |i| [i] * amount_for(engine, ctx) })
|
|
37
|
+
chosen = answers(locals)
|
|
38
|
+
apply_answers(engine, locals, chosen)
|
|
39
|
+
while chosen.size < queue.size
|
|
40
|
+
index = queue[chosen.size]
|
|
41
|
+
hand = engine.player(index).hand
|
|
42
|
+
return ask(engine, locals, index, "Discard a card", hand.map { { "type" => "discard", "object" => it } }) unless hand.empty?
|
|
43
|
+
|
|
44
|
+
chosen << { "type" => "none" } # nothing left to discard
|
|
45
|
+
locals["applied"] = chosen.size
|
|
46
|
+
end
|
|
47
|
+
:done
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# CR 701.9b: no one chooses; each card is picked by the game's RNG (so a replay picks the same ones).
|
|
51
|
+
def discard_at_random(engine, ctx)
|
|
52
|
+
return :done unless condition.nil? || condition.true?(engine, ctx)
|
|
53
|
+
|
|
54
|
+
n = amount_for(engine, ctx)
|
|
55
|
+
players(engine, ctx).each do |index|
|
|
56
|
+
hand = engine.player(index).hand
|
|
57
|
+
picked = engine.state.rng.shuffle!(hand.dup).first(n)
|
|
58
|
+
picked.each { engine.move!(it, :graveyard, reason: "discard") }
|
|
59
|
+
end
|
|
60
|
+
:done
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# "Discard your hand" (CR 701.9a): all at once, no choice; how many were discarded is its amount.
|
|
64
|
+
def discard_hands(engine, ctx)
|
|
65
|
+
return :done unless condition.nil? || condition.true?(engine, ctx)
|
|
66
|
+
|
|
67
|
+
players(engine, ctx).each do |index|
|
|
68
|
+
cards = engine.player(index).hand.dup
|
|
69
|
+
cards.each { engine.move!(it, :graveyard, reason: "discard") }
|
|
70
|
+
engine.record_amount!(cards.size)
|
|
71
|
+
end
|
|
72
|
+
:done
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Performs the discards chosen since the last step.
|
|
76
|
+
def apply_answers(engine, locals, chosen)
|
|
77
|
+
(locals["applied"] ||= 0).upto(chosen.size - 1) do |i|
|
|
78
|
+
answer = chosen[i]
|
|
79
|
+
engine.move!(answer["object"], :graveyard, reason: "discard") if answer["type"] == "discard"
|
|
80
|
+
end
|
|
81
|
+
locals["applied"] = chosen.size
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Draw cards (CR 121).
|
|
7
|
+
class Draw < Effect
|
|
8
|
+
primitive :effect, :draw, description: "Each referenced player draws `amount` cards (default: you).",
|
|
9
|
+
rules: %w[121.1 121.2], params: { "amount" => "integer", "to" => "selector" },
|
|
10
|
+
example: { "draw" => { "amount" => 2 } }
|
|
11
|
+
def self.default_to = "you"
|
|
12
|
+
def apply(engine, ctx) = players(engine, ctx).each { |i| amount_for(engine, ctx).times { engine.draw!(i) } }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Earthbend N (CR 701.66a): "Target land you control becomes a 0/0 land creature with haste in addition to
|
|
7
|
+
# its other types. Put N +1/+1 counters on it. When that land dies or is put into exile, return it to the
|
|
8
|
+
# battlefield tapped under your control." The target is declared on the ability like any other target
|
|
9
|
+
# (usually `{"land": {"controller": "you"}}`) and named by `to`. The land becomes a creature for as long as
|
|
10
|
+
# it stays on the battlefield (no duration is stated, CR 611.2a), is the effect's result ("that land"), and
|
|
11
|
+
# a delayed triggered ability watches it (CR 603.7c). Creating that ability is earthbending (CR 701.66b).
|
|
12
|
+
class Earthbend < Effect
|
|
13
|
+
primitive :effect, :earthbend,
|
|
14
|
+
description: "Earthbend `amount`: each referenced land (the target, `to`) becomes a 0/0 creature " \
|
|
15
|
+
"with haste that's still a land, gets `amount` +1/+1 counters, and when it dies or is " \
|
|
16
|
+
"put into exile it returns to the battlefield tapped under your control. The land is " \
|
|
17
|
+
"the effect's result.",
|
|
18
|
+
rules: %w[701.66a 701.66b 603.7c 611.2a 613.1d 613.4b],
|
|
19
|
+
params: { "amount" => "amount", "to" => "selector (the target land, e.g. {\"target\": 0})" },
|
|
20
|
+
example: { "earthbend" => { "amount" => 2, "to" => { "target" => 0 } } }
|
|
21
|
+
|
|
22
|
+
# The delayed triggered ability: "When that land dies or is put into exile, return it to the battlefield
|
|
23
|
+
# tapped under your control" ("it" is the card in its new zone, CR 400.7).
|
|
24
|
+
RETURN = { "when" => { "any_of" => [{ "dies" => { "subject" => "watched" } },
|
|
25
|
+
{ "leaves_battlefield" => { "subject" => "watched", "to" => "exile" } }] },
|
|
26
|
+
"effects" => [{ "return_to_battlefield" => { "to" => { "triggering" => "object" }, "tapped" => true,
|
|
27
|
+
"under" => "you" } }] }.freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :ref, :delayed_ability
|
|
30
|
+
|
|
31
|
+
def setup(_params, errors, path)
|
|
32
|
+
@ref = "earthbend:#{path}"
|
|
33
|
+
trigger = Mechanics.build_entry(:trigger, RETURN["when"], errors, "#{path}.when")
|
|
34
|
+
effects = Abilities::StackAbility.parse_effects(RETURN, errors, path, 0)
|
|
35
|
+
@delayed_ability = Abilities::TriggeredAbility.new(targets: [].freeze, effects: effects, trigger: trigger)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def nested_effects = delayed_ability.effects
|
|
39
|
+
def to_s = "earthbend #{amount} → #{to}"
|
|
40
|
+
|
|
41
|
+
def apply(engine, ctx)
|
|
42
|
+
lands = objects(engine, ctx).select { engine.chars(it).land? }
|
|
43
|
+
return if lands.empty?
|
|
44
|
+
|
|
45
|
+
n = amount_for(engine, ctx)
|
|
46
|
+
become_creature!(engine, ctx, lands)
|
|
47
|
+
lands.each do |id|
|
|
48
|
+
engine.record_result!(id)
|
|
49
|
+
engine.add_counters!(engine.object(id), "+1/+1", n, by_player: ctx.controller)
|
|
50
|
+
engine.create_delayed_trigger!("controller" => ctx.controller, "source" => ctx.source,
|
|
51
|
+
"card" => engine.object(ctx.object).card, "ability" => ref, "watched" => [id])
|
|
52
|
+
engine.emit Events::KeywordAction.new(player: engine.name(ctx.controller), action: "earthbend", amount: n)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Layer 4 (a creature in addition to its other types), layer 6 (haste) and layer 7b (base 0/0), for as
|
|
59
|
+
# long as it stays on the battlefield; the same continuous effects `become` creates.
|
|
60
|
+
def become_creature!(engine, ctx, lands)
|
|
61
|
+
lasting = { "duration" => "permanent" }
|
|
62
|
+
engine.add_continuous_effect!("add_types", ctx, lands,
|
|
63
|
+
{ "types" => ["creature"], "supertypes" => [], "subtypes" => [] }.merge(lasting))
|
|
64
|
+
engine.add_continuous_effect!("grant", ctx, lands, { "keywords" => ["haste"] }.merge(lasting))
|
|
65
|
+
engine.add_continuous_effect!("base_power_toughness", ctx, lands, { "power" => 0, "toughness" => 0 }.merge(lasting))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# "You get an emblem with '...'" (CR 114.1-114.4): each referenced player gets an emblem in the command
|
|
7
|
+
# zone with the `abilities`. They are part of this card's data, referenced "<this effect's ref>:<j>"
|
|
8
|
+
# (CardDefinition#effect_granted), and work from the command zone for the rest of the game (Engine::Emblems).
|
|
9
|
+
class CreateEmblem < Effect
|
|
10
|
+
KINDS = %i[triggered static player_rule].freeze
|
|
11
|
+
|
|
12
|
+
primitive :effect, :create_emblem,
|
|
13
|
+
description: "Each referenced player (default you) gets an emblem with the `abilities` (triggered, " \
|
|
14
|
+
"static or player_rule abilities). An emblem is in the command zone for the rest of the " \
|
|
15
|
+
"game; its abilities work there, and \"this emblem\" is the source of what they do.",
|
|
16
|
+
rules: %w[114.1 114.2 114.3 114.4 113.6p],
|
|
17
|
+
params: { "abilities" => "list of triggered, static or player_rule abilities", "to" => "selector" },
|
|
18
|
+
example: { "create_emblem" => { "abilities" => [
|
|
19
|
+
{ "static" => { "affects" => { "each" => "creature", "controller" => "you" },
|
|
20
|
+
"pump" => { "power" => 2, "toughness" => 2 } } }
|
|
21
|
+
] } }
|
|
22
|
+
|
|
23
|
+
attr_reader :ref, :granted_abilities
|
|
24
|
+
|
|
25
|
+
def self.keys = %w[abilities to if]
|
|
26
|
+
def self.default_to = "you"
|
|
27
|
+
|
|
28
|
+
def setup(params, errors, path)
|
|
29
|
+
@ref = "emblem:#{path}"
|
|
30
|
+
list = Mechanics.list_param(params, :abilities, errors, path)
|
|
31
|
+
errors << "#{path}.abilities: needs at least one ability" if list.empty?
|
|
32
|
+
@granted_abilities = list.each_with_index.filter_map do |entry, j|
|
|
33
|
+
ability = Mechanics.build_entry(:ability, entry, errors, "#{path}.abilities[#{j}]")
|
|
34
|
+
next ability if ability.nil? || KINDS.include?(ability.kind)
|
|
35
|
+
|
|
36
|
+
errors << "#{path}.abilities[#{j}]: an emblem's abilities are #{KINDS.join(", ")} abilities"
|
|
37
|
+
nil
|
|
38
|
+
end.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def nested_effects = granted_abilities.flat_map(&:try_effects)
|
|
42
|
+
|
|
43
|
+
def apply(engine, ctx)
|
|
44
|
+
card = engine.object(ctx.object).card
|
|
45
|
+
players(engine, ctx).each { engine.create_emblem!(it, card, ref) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_s = "gets an emblem with \"#{granted_abilities.join("; ")}\" → #{to}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Endure N (CR 701.63a): the permanent's controller creates an N/N white Spirit creature token unless
|
|
7
|
+
# they put N +1/+1 counters on that permanent. The choice is an `effect_choice` ("counters" is offered
|
|
8
|
+
# only while the permanent is on the battlefield). Endure 0 does nothing (CR 701.63b). N is any amount
|
|
9
|
+
# ("endures X", "where X is the number of counters on this creature"), fixed as the effect starts; the
|
|
10
|
+
# token's power and toughness are copiable values the effect gives it (CR 707.2, 111.4).
|
|
11
|
+
class Endure < ChoiceEffect
|
|
12
|
+
primitive :effect, :endure,
|
|
13
|
+
description: "Each referenced permanent (default: this one) endures `amount`: its controller puts " \
|
|
14
|
+
"that many +1/+1 counters on it, or creates an N/N white Spirit creature token.",
|
|
15
|
+
rules: %w[701.63a 701.63b 707.2 111.4],
|
|
16
|
+
params: { "amount" => "amount (2, \"x\", {\"counters\": ...})",
|
|
17
|
+
"to" => "selector of permanents (default self)" },
|
|
18
|
+
example: { "endure" => { "amount" => 2 } }
|
|
19
|
+
|
|
20
|
+
SPIRIT = { "name" => "Spirit", "types" => ["creature"], "subtypes" => ["spirit"], "colors" => ["W"],
|
|
21
|
+
"power" => 0, "toughness" => 0 }.freeze
|
|
22
|
+
|
|
23
|
+
def self.keys = %w[amount to if]
|
|
24
|
+
def self.default_to = "self"
|
|
25
|
+
def self.spirit = @spirit ||= CardDefinition.token(SPIRIT)
|
|
26
|
+
|
|
27
|
+
def created_tokens = [Endure.spirit]
|
|
28
|
+
|
|
29
|
+
def run(engine, ctx, locals)
|
|
30
|
+
unless locals.key?("queue")
|
|
31
|
+
return :done unless condition.nil? || condition.true?(engine, ctx)
|
|
32
|
+
|
|
33
|
+
locals["n"] = [amount_for(engine, ctx), 0].max
|
|
34
|
+
locals["queue"] = locals["n"].zero? ? [] : refs(engine, ctx).select { engine.state.objects.key?(it) }
|
|
35
|
+
end
|
|
36
|
+
n = locals["n"]
|
|
37
|
+
while (id = locals["queue"].first)
|
|
38
|
+
perm = engine.object(id)
|
|
39
|
+
index = perm.zone == :battlefield ? perm.controller : ctx.controller
|
|
40
|
+
answer = answers(locals).first
|
|
41
|
+
if answer.nil? && perm.zone == :battlefield
|
|
42
|
+
return ask(engine, locals, index, "Endure #{n}: put #{n} +1/+1 counters on it, or create a #{n}/#{n} Spirit?",
|
|
43
|
+
[{ "type" => "counters", "object" => id }, { "type" => "spirit" }])
|
|
44
|
+
end
|
|
45
|
+
answer&.[]("type") == "counters" ? engine.add_counters!(perm, "+1/+1", n, by_player: index) : spirit!(engine, index, n)
|
|
46
|
+
locals["queue"].shift
|
|
47
|
+
locals.delete("answers")
|
|
48
|
+
end
|
|
49
|
+
:done
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s = "endure #{amount} → #{to}"
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# An N/N Spirit: N is part of the token's copiable values (Engine::Copies, "copy_except").
|
|
57
|
+
def spirit!(engine, index, n)
|
|
58
|
+
engine.create_token!(Endure.spirit, index, memory: { "copy_except" => { "power" => n, "toughness" => n } })
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Exile (CR 406, 701.13). `link`: the card is "exiled with" the source, for its linked abilities
|
|
7
|
+
# (CR 607.2a). `until_source_leaves`: it returns once the source leaves the battlefield (CR 610.3).
|
|
8
|
+
class Exile < ZoneEffect
|
|
9
|
+
primitive :effect, :exile,
|
|
10
|
+
description: "Exile each referenced permanent, spell, or card in a graveyard or hand. `link: true`: the cards " \
|
|
11
|
+
"are exiled with the source, for its \"exiled with\" abilities (the `exiled_with` selector). " \
|
|
12
|
+
"`until_source_leaves: true`: they return to the battlefield under their owners' control " \
|
|
13
|
+
"when the source leaves the battlefield; if it already has, nothing is exiled.",
|
|
14
|
+
rules: %w[406.1 701.13a 607.2a 406.6 610.3 610.3a 610.3b 610.3c],
|
|
15
|
+
params: { "to" => "selector", "link" => "boolean", "until_source_leaves" => "boolean" },
|
|
16
|
+
example: { "exile" => { "to" => { "target" => 0 } } }
|
|
17
|
+
|
|
18
|
+
attr_reader :link, :until_leaves
|
|
19
|
+
|
|
20
|
+
def self.keys = %w[to link until_source_leaves if]
|
|
21
|
+
def self.zone = :exile
|
|
22
|
+
# A spell too ("exile target spell", CR 701.13a).
|
|
23
|
+
def self.from_zones = %i[battlefield graveyard hand stack]
|
|
24
|
+
|
|
25
|
+
def setup(params, errors, path)
|
|
26
|
+
@link = Mechanics.flag_param(params, "link", errors, path)
|
|
27
|
+
@until_leaves = Mechanics.flag_param(params, "until_source_leaves", errors, path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s = "exile#{" until the source leaves" if until_leaves}#{" (linked)" if link} → #{to}"
|
|
31
|
+
|
|
32
|
+
# CR 610.3a-b: if the source has already left the battlefield, the object doesn't move.
|
|
33
|
+
def apply(engine, ctx)
|
|
34
|
+
return if until_leaves && !engine.state.battlefield.include?(ctx.source)
|
|
35
|
+
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def move(engine, ctx, id)
|
|
40
|
+
fresh = super or return
|
|
41
|
+
engine.link_exiled!(ctx.source, fresh.id) if link || until_leaves
|
|
42
|
+
return unless until_leaves
|
|
43
|
+
|
|
44
|
+
(engine.object(ctx.source).memory[Linked::UNTIL_LEAVES] ||= []) << fresh.id
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# "Take an extra turn after this one." (CR 500.7)
|
|
7
|
+
class ExtraTurn < Effect
|
|
8
|
+
primitive :effect, :extra_turn,
|
|
9
|
+
description: "Each referenced player (default you) takes an extra turn after this one. Extra " \
|
|
10
|
+
"turns are taken most recently created first.",
|
|
11
|
+
rules: %w[500.7],
|
|
12
|
+
params: { "to" => "selector" },
|
|
13
|
+
example: { "extra_turn" => { "if" => { "began_extra_turn" => false } } }
|
|
14
|
+
|
|
15
|
+
def self.keys = %w[to if]
|
|
16
|
+
def self.default_to = "you"
|
|
17
|
+
|
|
18
|
+
def apply(engine, ctx) = players(engine, ctx).each { engine.add_extra_turn!(it) }
|
|
19
|
+
def to_s = "take an extra turn after this one → #{to}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# "Target opponent skips their next X turns." (CR 614.10)
|
|
23
|
+
class SkipTurns < Effect
|
|
24
|
+
primitive :effect, :skip_turns,
|
|
25
|
+
description: "Each referenced player skips their next `amount` turns (extra turns included): " \
|
|
26
|
+
"each of those turns doesn't happen, and the next player in turn order takes theirs. " \
|
|
27
|
+
"Several skips add up.",
|
|
28
|
+
rules: %w[614.1b 614.10 614.10a],
|
|
29
|
+
params: { "amount" => "amount", "to" => "selector" },
|
|
30
|
+
example: { "skip_turns" => { "amount" => 1, "to" => { "target" => 0 } } }
|
|
31
|
+
|
|
32
|
+
def apply(engine, ctx)
|
|
33
|
+
n = amount_for(engine, ctx)
|
|
34
|
+
players(engine, ctx).each { engine.skip_turns!(it, n) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s = "skip the next #{amount} turn(s) → #{to}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# "After this phase, there is an additional combat phase", "... followed by an additional main phase",
|
|
41
|
+
# "After this main phase, there are two additional combat phases." (CR 500.8)
|
|
42
|
+
class AdditionalPhases < Effect
|
|
43
|
+
primitive :effect, :additional_phases,
|
|
44
|
+
description: "Adds `phases` (in order; each \"combat\" or \"main\") directly after the current " \
|
|
45
|
+
"phase of this turn. Phases added after the same phase come most recently created " \
|
|
46
|
+
"first. An added main phase is a postcombat main phase. `attackers`: only creatures " \
|
|
47
|
+
"matching this filter can attack during the (first) added combat phase.",
|
|
48
|
+
rules: %w[500.8 505.1 508.1c],
|
|
49
|
+
params: { "phases" => "list of \"combat\" | \"main\"", "attackers" => "creature filter" },
|
|
50
|
+
example: { "additional_phases" => { "phases" => %w[combat main] } }
|
|
51
|
+
|
|
52
|
+
attr_reader :phases, :attackers
|
|
53
|
+
|
|
54
|
+
def self.keys = %w[phases attackers if]
|
|
55
|
+
def self.default_to = "you"
|
|
56
|
+
|
|
57
|
+
def setup(params, errors, path)
|
|
58
|
+
@attackers = Mechanics.fetch_param(params, :attackers)
|
|
59
|
+
Mechanics.build_entry(:filter, @attackers, errors, "#{path}.attackers") unless @attackers.nil?
|
|
60
|
+
@phases = Mechanics.list_param(params, :phases, errors, path)
|
|
61
|
+
return if !@phases.empty? && @phases.all? { Engine::ADDED_PHASES.key?(it) }
|
|
62
|
+
|
|
63
|
+
errors << "#{path}.phases: must be a non-empty list of \"combat\" | \"main\""
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply(engine, _ctx) = engine.add_phases!(phases.dup, attackers: attackers)
|
|
67
|
+
|
|
68
|
+
def to_s
|
|
69
|
+
only = " (only #{Mechanics.build_entry(:filter, attackers, [], "")} can attack)" if attackers
|
|
70
|
+
"after this phase, additional #{phases.join(" and ")} phase#{"s" if phases.size > 1}#{only}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# "End the turn." (CR 725.1)
|
|
75
|
+
class EndTheTurn < Effect
|
|
76
|
+
primitive :effect, :end_the_turn,
|
|
77
|
+
description: "Ends the turn: triggered abilities not yet on the stack cease to exist, every object on " \
|
|
78
|
+
"the stack is exiled (this spell too, so its later effects don't happen), state-based " \
|
|
79
|
+
"actions are checked, combat ends and the game skips to the cleanup step. Abilities " \
|
|
80
|
+
"that trigger meanwhile go on the stack during cleanup, followed by another cleanup step.",
|
|
81
|
+
rules: %w[725.1 725.1a 725.1b 725.1c 725.1d 725.1e 725.1f 514.3a],
|
|
82
|
+
example: "end_the_turn"
|
|
83
|
+
|
|
84
|
+
def self.keys = %w[if]
|
|
85
|
+
def self.default_to = "you"
|
|
86
|
+
|
|
87
|
+
def apply(engine, _ctx) = engine.end_the_turn!
|
|
88
|
+
def to_s = "end the turn"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Condition
|
|
93
|
+
# "If you haven't begun an extra turn this game" (CR 500.7): {"began_extra_turn": false}.
|
|
94
|
+
class BeganExtraTurn
|
|
95
|
+
def initialize(wanted)
|
|
96
|
+
@wanted = wanted
|
|
97
|
+
freeze
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def true?(engine, ctx) = engine.began_extra_turn?(ctx.controller) == @wanted
|
|
101
|
+
def to_s = @wanted ? "you've begun an extra turn this game" : "you haven't begun an extra turn this game"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# "If it's the first combat phase of the turn" (CR 500.8): {"first_combat_phase": true}.
|
|
105
|
+
class FirstCombatPhase
|
|
106
|
+
def self.build(data, errors, path)
|
|
107
|
+
Mechanics.check_keys(data, %w[first_combat_phase], errors, path)
|
|
108
|
+
errors << "#{path}.first_combat_phase: must be true or false" unless [true, false].include?(data["first_combat_phase"])
|
|
109
|
+
new(data["first_combat_phase"] != false)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def initialize(wanted)
|
|
113
|
+
@wanted = wanted
|
|
114
|
+
freeze
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def true?(engine, _ctx) = engine.first_combat_phase? == @wanted
|
|
118
|
+
def to_s = @wanted ? "it's the first combat phase of the turn" : "it isn't the first combat phase of the turn"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|