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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "crew"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Costs
|
|
8
|
+
# The cost of saddle N (CR 702.171a): "Tap any number of other untapped creatures you control with total
|
|
9
|
+
# power N or greater", chosen exactly like crew's. Each creature tapped for it saddles the permanent
|
|
10
|
+
# (CR 702.171c).
|
|
11
|
+
class Saddle < Crew
|
|
12
|
+
def choose!(engine, option) = super.merge("saddles" => true)
|
|
13
|
+
def action = :saddle
|
|
14
|
+
|
|
15
|
+
def pay_chosen!(engine, object, entries)
|
|
16
|
+
entries.each { engine.emit Events::SaddledMount.new(object: it["object"], mount: object.id) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module Effects
|
|
22
|
+
# "[It] becomes saddled until end of turn" (CR 702.171a-b): the saddled designation, which stays until the
|
|
23
|
+
# turn ends or the permanent leaves the battlefield and isn't copiable (it lives on the object, stamped
|
|
24
|
+
# with the turn, Engine#saddled?). The effect of a saddle ability, and of cards that saddle a Mount
|
|
25
|
+
# without its saddle ability.
|
|
26
|
+
class BecomeSaddled < Effect
|
|
27
|
+
primitive :effect, :become_saddled,
|
|
28
|
+
description: "Each referenced permanent (default: this one) becomes saddled until end of turn " \
|
|
29
|
+
"(filters: `\"saddled\": true`).",
|
|
30
|
+
rules: %w[702.171a 702.171b],
|
|
31
|
+
params: { "to" => "selector of permanents (default self)" },
|
|
32
|
+
example: { "become_saddled" => { "to" => { "target" => 0 } } }
|
|
33
|
+
|
|
34
|
+
def self.keys = %w[to if]
|
|
35
|
+
def self.default_to = "self"
|
|
36
|
+
def to_s = "becomes saddled until end of turn → #{to}"
|
|
37
|
+
|
|
38
|
+
def apply(engine, ctx)
|
|
39
|
+
riders = (engine.object(ctx.object).paid || []).select { it["saddles"] }.map { it["object"] }
|
|
40
|
+
objects(engine, ctx).each do |id|
|
|
41
|
+
engine.saddle!(id)
|
|
42
|
+
engine.emit Events::BecameSaddled.new(object: id, riders: riders)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module Abilities
|
|
49
|
+
# Saddle N (CR 702.171a): an activated ability of a Mount, at sorcery speed. Written {"saddle": {"power": N}}.
|
|
50
|
+
class Saddle < ActivatedAbility
|
|
51
|
+
primitive :ability, :saddle,
|
|
52
|
+
description: "Saddle N: tap any number of other untapped creatures you control with total power N " \
|
|
53
|
+
"or more (chosen one at a time; `done` once the total is enough): this permanent " \
|
|
54
|
+
"becomes saddled until end of turn. Activate only as a sorcery.",
|
|
55
|
+
rules: %w[702.171a 702.171b 702.171c],
|
|
56
|
+
params: { "power" => "integer >= 1 (the N of saddle N)" },
|
|
57
|
+
example: { "saddle" => { "power" => 2 } }
|
|
58
|
+
|
|
59
|
+
def self.build(params, errors, path, **)
|
|
60
|
+
Mechanics.check_keys(params, %w[power], errors, path)
|
|
61
|
+
n = Mechanics.integer_param(params, :power, errors, path, min: 1)
|
|
62
|
+
new(sorcery: true, targets: [].freeze, costs: [Costs::Saddle.new(n)].freeze,
|
|
63
|
+
effects: [Effects::BecomeSaddled.new(to: Selector.new(:self), params: {}, errors: [], path: path)].freeze)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def power = costs.first.power
|
|
67
|
+
def to_s = "Saddle #{power}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module Triggers
|
|
72
|
+
# "Whenever this creature becomes saddled" (CR 702.171b): a saddle ability (or another effect) saddled it.
|
|
73
|
+
class BecomesSaddled < BecomesCrewed
|
|
74
|
+
primitive :trigger, :becomes_saddled,
|
|
75
|
+
description: "The subject becomes saddled (its saddle ability resolved, or an effect saddled it).",
|
|
76
|
+
rules: %w[702.171b 603.2],
|
|
77
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
|
|
78
|
+
example: { "becomes_saddled" => {} }
|
|
79
|
+
|
|
80
|
+
def event_class = Events::BecameSaddled
|
|
81
|
+
|
|
82
|
+
def occurrences(engine, event, source)
|
|
83
|
+
return [] unless match?(engine, event, source)
|
|
84
|
+
|
|
85
|
+
obj = engine.object(event.object)
|
|
86
|
+
[{ "object" => obj.id, "player" => obj.controller, "card" => obj.card, "amount" => event.riders.size }]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_s = "Whenever #{@subject} becomes saddled"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# "Whenever this creature saddles a Mount" (CR 702.171c): it was tapped to pay a saddle cost. The event's
|
|
93
|
+
# object for the effects ("that Mount") is the Mount.
|
|
94
|
+
class SaddlesMount < CrewsVehicle
|
|
95
|
+
primitive :trigger, :saddles_mount,
|
|
96
|
+
description: "The subject (a creature) saddles a Mount: it's tapped to pay that Mount's saddle " \
|
|
97
|
+
"cost. {\"triggering\": \"object\"} is the Mount.",
|
|
98
|
+
rules: %w[702.171c 603.2],
|
|
99
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
|
|
100
|
+
example: { "saddles_mount" => { "subject" => "self" } }
|
|
101
|
+
|
|
102
|
+
def event_class = Events::SaddledMount
|
|
103
|
+
|
|
104
|
+
def occurrences(engine, event, source)
|
|
105
|
+
return [] unless match?(engine, event, source)
|
|
106
|
+
|
|
107
|
+
mount = engine.state.objects[event.mount]
|
|
108
|
+
[{ "object" => event.mount, "player" => engine.object(event.object).controller, "card" => mount&.card }.compact]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_s = "Whenever #{@subject} saddles a Mount"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# "Your speed increases by 1" (CR 702.179c-d): a player without speed gets that much; never above 4.
|
|
7
|
+
class IncreaseSpeed < Effect
|
|
8
|
+
def self.keys = %w[to]
|
|
9
|
+
def apply(engine, ctx) = engine.increase_speed!(ctx.controller, 1)
|
|
10
|
+
def to_s = "your speed increases by 1"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Keywords
|
|
15
|
+
# Start your engines! (CR 702.179a, 704.5aa): a player who controls a permanent with it and has no speed
|
|
16
|
+
# gets speed 1 (a state-based action). The inherent triggered ability of a player with speed (CR 702.179d)
|
|
17
|
+
# has no source; the engine (Engine::Speed) puts it on the stack as this keyword's ability
|
|
18
|
+
# "keyword:start_your_engines", which never triggers from the permanent itself (its zone is :rules).
|
|
19
|
+
class StartYourEngines < Primitive
|
|
20
|
+
primitive :keyword, :start_your_engines,
|
|
21
|
+
description: "If you control this permanent and have no speed, your speed becomes 1. Once you have " \
|
|
22
|
+
"speed, it increases by 1 (to at most 4) once on each of your turns when one or more " \
|
|
23
|
+
"opponents lose life. Pair with `max_speed` abilities and the `speed` amount.",
|
|
24
|
+
rules: %w[702.179a 702.179b 702.179c 702.179d 704.3],
|
|
25
|
+
example: { "keywords" => ["start_your_engines"] }
|
|
26
|
+
|
|
27
|
+
# "Whenever one or more opponents lose life during your turn" (matched by Engine::Speed#watch_speed!).
|
|
28
|
+
class OpponentsLoseLife
|
|
29
|
+
def watches?(_event) = false
|
|
30
|
+
def leaves_battlefield? = false
|
|
31
|
+
def to_s = "Whenever one or more opponents lose life during your turn"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.intrinsic_abilities
|
|
35
|
+
@intrinsic_abilities ||= [
|
|
36
|
+
Abilities::TriggeredAbility.new(
|
|
37
|
+
trigger: OpponentsLoseLife.new.freeze, targets: [].freeze, limit: "once_each_turn", zone: :rules,
|
|
38
|
+
condition: Condition.build({ "amount" => { "speed" => "you" }, "at_most" => 3 }, [], "speed"),
|
|
39
|
+
effects: [Effects::IncreaseSpeed.new(to: nil, params: {}, errors: [], path: "speed")].freeze
|
|
40
|
+
)
|
|
41
|
+
].freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module Abilities
|
|
47
|
+
# Max speed (CR 702.178a): "As long as your speed is 4, this object has [ability]." Like a station symbol,
|
|
48
|
+
# its keywords and abilities are part of the object only while it applies; a card that isn't on the
|
|
49
|
+
# battlefield uses its owner's speed (CR 702.178b, 113.6c: e.g. an ability activated from the graveyard).
|
|
50
|
+
class MaxSpeed < Primitive
|
|
51
|
+
primitive :ability, :max_speed,
|
|
52
|
+
description: "While your speed is 4 (max speed), this object has `keywords` and `abilities` (any " \
|
|
53
|
+
"kind: activated, triggered, static, mana; an ability with a graveyard cost works from " \
|
|
54
|
+
"your graveyard).",
|
|
55
|
+
rules: %w[702.178a 702.178b 702.179e 113.6c],
|
|
56
|
+
params: { "keywords" => "list of keywords", "abilities" => "list of abilities" },
|
|
57
|
+
example: { "max_speed" => { "abilities" => [{ "activated" => { "cost" => ["tap"],
|
|
58
|
+
"effects" => [{ "draw" => { "amount" => 1 } }] } }] } }
|
|
59
|
+
|
|
60
|
+
attr_reader :keywords, :nested_abilities
|
|
61
|
+
|
|
62
|
+
def self.build(params, errors, path, **)
|
|
63
|
+
Mechanics.check_keys(params, %w[keywords abilities], errors, path)
|
|
64
|
+
keywords = StationSymbol.build_keywords(params, errors, path)
|
|
65
|
+
abilities = StationSymbol.build_abilities(params, errors, path)
|
|
66
|
+
abilities.each_with_index do |ability, i|
|
|
67
|
+
errors << "#{path}.abilities[#{i}]: max speed abilities can't be nested" if ability.kind == :max_speed
|
|
68
|
+
end
|
|
69
|
+
errors << "#{path}: needs keywords or abilities" if keywords.empty? && abilities.empty?
|
|
70
|
+
new(keywords, abilities)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def initialize(keywords, abilities)
|
|
74
|
+
super()
|
|
75
|
+
@keywords = keywords.freeze
|
|
76
|
+
@nested_abilities = abilities.freeze
|
|
77
|
+
freeze
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def kind = :max_speed
|
|
81
|
+
def power = nil
|
|
82
|
+
def mana_ability? = false
|
|
83
|
+
def allowed_types = nil
|
|
84
|
+
def try_effects = []
|
|
85
|
+
def to_s = "Max speed — #{(keywords.map(&:to_s) + nested_abilities.map(&:to_s)).join("; ")}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Costs
|
|
6
|
+
# "Tap another untapped creature you control" as a cost (CR 702.184a, 602.2b). The player chooses
|
|
7
|
+
# the creature while activating (a `choose_cost` decision). Summoning sickness doesn't matter:
|
|
8
|
+
# CR 302.6 restricts only a creature's own {T} abilities.
|
|
9
|
+
class TapAnotherCreature < Primitive
|
|
10
|
+
primitive :cost, :tap_another_creature,
|
|
11
|
+
description: "Tap another untapped creature you control, chosen while activating. A creature " \
|
|
12
|
+
"that came under your control this turn can be tapped this way.",
|
|
13
|
+
rules: %w[702.184a 601.2h 602.2b 302.6],
|
|
14
|
+
example: "tap_another_creature"
|
|
15
|
+
|
|
16
|
+
INSTANCE = new.freeze
|
|
17
|
+
|
|
18
|
+
def self.build(params, errors, path, **)
|
|
19
|
+
Mechanics.check_keys(params, [], errors, path)
|
|
20
|
+
INSTANCE
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def mana? = false
|
|
24
|
+
def payable?(engine, object) = !choice_candidates(engine, object).empty?
|
|
25
|
+
# Paid when chosen (Engine#choose_cost!).
|
|
26
|
+
def pay!(_engine, _object) = nil
|
|
27
|
+
def to_s = "Tap another untapped creature you control"
|
|
28
|
+
|
|
29
|
+
def choices_needed = 1
|
|
30
|
+
def choice_options(engine, object, _chosen) = choice_candidates(engine, object).map { { object: it } }
|
|
31
|
+
|
|
32
|
+
def choose!(engine, option)
|
|
33
|
+
creature = engine.object(option[:object])
|
|
34
|
+
engine.tap!(creature)
|
|
35
|
+
{ "object" => creature.id, "power" => Abilities::TappingPower.of(engine, creature, :station), "tapped" => true }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def choice_candidates(engine, object)
|
|
39
|
+
engine.state.battlefield.select do |id|
|
|
40
|
+
other = engine.object(id)
|
|
41
|
+
id != object.id && other.controller == object.controller && !other.tapped && engine.chars(other).creature?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module Effects
|
|
48
|
+
# The effect of a station ability (CR 702.184a): put charge counters on its source equal to the
|
|
49
|
+
# power of the creature tapped to pay its cost. That's the creature's current power, or, if it has
|
|
50
|
+
# left the battlefield, its power when it was tapped; an ability may make it use its toughness (CR 702.184c,
|
|
51
|
+
# Abilities::TappingPower).
|
|
52
|
+
class StationCharge < Effect
|
|
53
|
+
def self.keys = %w[to]
|
|
54
|
+
|
|
55
|
+
def apply(engine, ctx)
|
|
56
|
+
paid = engine.object(ctx.object).paid&.first or return
|
|
57
|
+
creature = paid["object"]
|
|
58
|
+
on = engine.state.battlefield.include?(creature)
|
|
59
|
+
n = on ? Abilities::TappingPower.of(engine, engine.object(creature), :station) : paid["power"]
|
|
60
|
+
objects(engine, ctx).each { engine.add_counters!(engine.object(it), "charge", n) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_s = "put charge counters equal to the tapped creature's power on this"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
module Keywords
|
|
68
|
+
# Station (CR 702.184a): "Tap another untapped creature you control: Put a
|
|
69
|
+
# number of charge counters on this permanent equal to the tapped creature's power. Activate only as
|
|
70
|
+
# a sorcery." A station card has it at all times, whatever its charge counters (CR 722.4).
|
|
71
|
+
class Station < Primitive
|
|
72
|
+
primitive :keyword, :station,
|
|
73
|
+
description: "An activated ability: tap another untapped creature you control to put charge " \
|
|
74
|
+
"counters equal to its power on this permanent. Sorcery speed. Pair with " \
|
|
75
|
+
"`station_symbol` abilities for what the counters unlock.",
|
|
76
|
+
rules: %w[702.184a 722.4],
|
|
77
|
+
example: { "keywords" => ["station"] }
|
|
78
|
+
|
|
79
|
+
def self.intrinsic_abilities
|
|
80
|
+
@intrinsic_abilities ||= [
|
|
81
|
+
Abilities::ActivatedAbility.new(
|
|
82
|
+
sorcery: true, targets: [].freeze, costs: [Costs::TapAnotherCreature::INSTANCE].freeze,
|
|
83
|
+
effects: [Effects::StationCharge.new(to: Selector.new(:self), params: {}, errors: [], path: "station")].freeze
|
|
84
|
+
)
|
|
85
|
+
].freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
module Abilities
|
|
91
|
+
# A station symbol "{N+}" (CR 722.2): as long as this permanent has N or more charge counters, it
|
|
92
|
+
# has the listed keywords and abilities and, with a power/toughness box, is a creature with that base
|
|
93
|
+
# power and toughness in addition to its other types (CR 722.2a-b). A station card has no power or
|
|
94
|
+
# toughness off the battlefield (CR 722.2c), so P/T goes here, not on the card.
|
|
95
|
+
class StationSymbol < Primitive
|
|
96
|
+
primitive :ability, :station_symbol,
|
|
97
|
+
description: "While this permanent has at least `at` charge counters it has `keywords` and " \
|
|
98
|
+
"`abilities`; with `power`/`toughness` it is also a creature with that base power " \
|
|
99
|
+
"and toughness (layers 4 and 7b).",
|
|
100
|
+
rules: %w[722.2a 722.2b 722.2c 613.1d 613.4b],
|
|
101
|
+
params: { "at" => "integer >= 1 (the N of {N+})", "power" => "integer", "toughness" => "integer",
|
|
102
|
+
"keywords" => "list of keywords", "abilities" => "list of abilities" },
|
|
103
|
+
example: { "station_symbol" => { "at" => 6, "power" => 3, "toughness" => 4, "keywords" => ["flying"] } }
|
|
104
|
+
|
|
105
|
+
attr_reader :threshold, :power, :toughness, :keywords, :nested_abilities
|
|
106
|
+
|
|
107
|
+
def self.build(params, errors, path, **)
|
|
108
|
+
Mechanics.check_keys(params, %w[at power toughness keywords abilities], errors, path)
|
|
109
|
+
threshold = Mechanics.integer_param(params, :at, errors, path, min: 1)
|
|
110
|
+
stats = %w[power toughness].map { Mechanics.fetch_param(params, it) }
|
|
111
|
+
if stats.compact.size == 1 || !stats.compact.all?(Integer)
|
|
112
|
+
errors << "#{path}: power and toughness must both be integers, or both be left out"
|
|
113
|
+
stats = [nil, nil]
|
|
114
|
+
end
|
|
115
|
+
keywords = build_keywords(params, errors, path)
|
|
116
|
+
abilities = build_abilities(params, errors, path)
|
|
117
|
+
errors << "#{path}: needs power/toughness, keywords or abilities" if stats.compact.empty? && keywords.empty? && abilities.empty?
|
|
118
|
+
new(threshold, *stats, keywords, abilities)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.build_keywords(params, errors, path)
|
|
122
|
+
list = Mechanics.list_param(params, :keywords, errors, path).map { it.to_s.downcase.tr(" -", "__") }
|
|
123
|
+
list.each { errors << "#{path}.keywords: unknown keyword #{it.inspect}" unless Enjine.registry.fetch(:keyword, it) }
|
|
124
|
+
list.map(&:to_sym)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.build_abilities(params, errors, path)
|
|
128
|
+
Mechanics.list_param(params, :abilities, errors, path).each_with_index.filter_map do |entry, i|
|
|
129
|
+
ability = Mechanics.build_entry(:ability, entry, errors, "#{path}.abilities[#{i}]")
|
|
130
|
+
if ability&.kind == :station_symbol
|
|
131
|
+
errors << "#{path}.abilities[#{i}]: station symbols can't be nested"
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
ability
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def initialize(threshold, power, toughness, keywords, abilities)
|
|
139
|
+
super()
|
|
140
|
+
@threshold = threshold
|
|
141
|
+
@power = power
|
|
142
|
+
@toughness = toughness
|
|
143
|
+
@keywords = keywords.freeze
|
|
144
|
+
@nested_abilities = abilities.freeze
|
|
145
|
+
freeze
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def kind = :station_symbol
|
|
149
|
+
def mana_ability? = false
|
|
150
|
+
def allowed_types = nil
|
|
151
|
+
def try_effects = []
|
|
152
|
+
|
|
153
|
+
def to_s
|
|
154
|
+
bits = []
|
|
155
|
+
bits << "#{power}/#{toughness} creature" if power
|
|
156
|
+
bits.concat(keywords.map(&:to_s))
|
|
157
|
+
bits.concat(nested_abilities.map(&:to_s))
|
|
158
|
+
"#{threshold}+ | #{bits.join("; ")}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Keywords
|
|
6
|
+
# Storied (CR 702.195a): "Any time you control three or more permanents that are artifacts, Sagas, and/or
|
|
7
|
+
# legendary and you don't have an enduring story, you have an enduring story for the rest of the game."
|
|
8
|
+
# The engine checks it (Engine::Designations#tell_enduring_stories!); cards read the designation with
|
|
9
|
+
# the condition `{"designation": "enduring_story"}`.
|
|
10
|
+
class Storied < Primitive
|
|
11
|
+
primitive :keyword, :storied,
|
|
12
|
+
description: "While you control this permanent, once you control three or more permanents that are " \
|
|
13
|
+
"artifacts, Sagas and/or legendary, you have an enduring story for the rest of the game " \
|
|
14
|
+
"(condition `{\"designation\": \"enduring_story\"}`).",
|
|
15
|
+
rules: %w[702.195a 702.195b 702.195c], example: { "keywords" => ["storied"] }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Condition
|
|
20
|
+
# "If you have an enduring story" / "as long as you have an enduring story": the controller has a player
|
|
21
|
+
# designation (Engine::Designations; CR 702.195b).
|
|
22
|
+
class Designation
|
|
23
|
+
def self.build(data, errors, path)
|
|
24
|
+
Mechanics.check_keys(data, %w[designation], errors, path)
|
|
25
|
+
value = data["designation"]
|
|
26
|
+
known = Engine::Designations::NAMES
|
|
27
|
+
errors << "#{path}.designation: must be one of #{known.join(", ")}" unless known.include?(value)
|
|
28
|
+
new(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(designation)
|
|
32
|
+
@designation = designation
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def true?(engine, ctx) = engine.designation?(ctx.controller, @designation)
|
|
37
|
+
def to_s = "you have #{@designation == "enduring_story" ? "an enduring story" : @designation.tr("_", " ")}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../abilities/play_permissions"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Triggers
|
|
8
|
+
# The number of spells cast this turn before the one whose cast is being looked at (its SpellCast is
|
|
9
|
+
# already in the turn's history, Engine::TurnHistory): a cast trigger's "that many" (storm, CR 702.40a).
|
|
10
|
+
def self.spells_before(engine)
|
|
11
|
+
engine.state.players.each_index.sum { engine.turn_log(it).fetch("spells", []).size } - 1
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Effects
|
|
16
|
+
# Cascade's effect (CR 702.85a): exile cards from the top of your library until you exile a nonland card
|
|
17
|
+
# whose mana value is less than this spell's; you may cast it without paying its mana cost; then all
|
|
18
|
+
# exiled cards not cast go on the bottom of your library in a random order. Discover's steps with
|
|
19
|
+
# another limit and miss.
|
|
20
|
+
class Cascade < Discover
|
|
21
|
+
def self.build_for_keyword = new(to: Selector.new(:you), params: { "amount" => 0 }, errors: [], path: "cascade")
|
|
22
|
+
|
|
23
|
+
def to_s = "cascade"
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Less than this spell's mana value (X included on the stack, CR 202.3e); if the spell has left the
|
|
28
|
+
# stack, its card's mana value.
|
|
29
|
+
def limit(engine, ctx)
|
|
30
|
+
spell = engine.state.objects[ctx.source]
|
|
31
|
+
cost = spell ? engine.chars(spell).mana_cost&.with_x(spell.x.to_i) : triggering_cost(engine, ctx)
|
|
32
|
+
cost ? cost.mana_value - 1 : -1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def triggering_cost(engine, ctx)
|
|
36
|
+
card = Selector.memory(engine, ctx).dig("triggering", "card")
|
|
37
|
+
card && engine.catalog.fetch(card).mana_cost
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def miss_type = "decline"
|
|
41
|
+
def prompt = "Cascade: you may cast it without paying its mana cost"
|
|
42
|
+
def miss!(_engine, _hit) = nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module Keywords
|
|
47
|
+
# Storm (CR 702.40a): "When you cast this spell, copy it for each other spell that was cast before it this
|
|
48
|
+
# turn. If the spell has any targets, you may choose new targets for any of the copies." Each instance
|
|
49
|
+
# triggers separately (CR 702.40b).
|
|
50
|
+
class Storm < Primitive
|
|
51
|
+
primitive :keyword, :storm,
|
|
52
|
+
description: "When you cast this spell, copy it for each other spell cast before it this turn; you " \
|
|
53
|
+
"may choose new targets for the copies.",
|
|
54
|
+
rules: %w[702.40a 702.40b 707.10], example: { "keywords" => ["storm"] }
|
|
55
|
+
|
|
56
|
+
ABILITY = { "triggered" => { "when" => { "cast" => { "self" => true } },
|
|
57
|
+
"effects" => [{ "copy_spell" => { "of" => "self", "amount" => { "triggering" => "amount" },
|
|
58
|
+
"new_targets" => true } }] } }.freeze
|
|
59
|
+
|
|
60
|
+
def self.intrinsic_abilities = @intrinsic_abilities ||= [Keywords.ability_from_data(ABILITY, "storm")].freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Cascade (CR 702.85a): "When you cast this spell, exile cards from the top of your library until you exile
|
|
64
|
+
# a nonland card whose mana value is less than this spell's mana value. You may cast that card without
|
|
65
|
+
# paying its mana cost if the resulting spell's mana value is less than this spell's mana value. Then put
|
|
66
|
+
# all cards exiled this way that weren't cast on the bottom of your library in a random order." Each
|
|
67
|
+
# instance triggers separately (CR 702.85c).
|
|
68
|
+
class Cascade < Primitive
|
|
69
|
+
primitive :keyword, :cascade,
|
|
70
|
+
description: "When you cast this spell, exile cards from the top of your library until you exile a " \
|
|
71
|
+
"nonland card with lesser mana value. You may cast it without paying its mana cost. " \
|
|
72
|
+
"Then the exiled cards not cast go on the bottom of your library in a random order.",
|
|
73
|
+
rules: %w[702.85a 702.85c 608.2g], example: { "keywords" => ["cascade"] }
|
|
74
|
+
|
|
75
|
+
def self.intrinsic_abilities
|
|
76
|
+
@intrinsic_abilities ||= begin
|
|
77
|
+
trigger = Keywords.ability_from_data({ "triggered" => { "when" => { "cast" => { "self" => true } },
|
|
78
|
+
"effects" => [] } }, "cascade").trigger
|
|
79
|
+
[Abilities::TriggeredAbility.new(trigger: trigger, targets: [].freeze,
|
|
80
|
+
effects: [Effects::Cascade.build_for_keyword].freeze)].freeze
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Keywords
|
|
6
|
+
# Sunburst (CR 702.44): enters with a counter for each color of mana spent to cast it.
|
|
7
|
+
class Sunburst < Primitive
|
|
8
|
+
primitive :keyword, :sunburst,
|
|
9
|
+
description: "As this permanent enters from the stack as a resolving spell, it gets a +1/+1 counter " \
|
|
10
|
+
"(a creature) or a charge counter (otherwise) for each color of mana spent to cast it.",
|
|
11
|
+
rules: %w[702.44a 702.44b], example: { "keywords" => ["sunburst"] }
|
|
12
|
+
|
|
13
|
+
COLORS = Mana::COLORS.map(&:to_s).freeze
|
|
14
|
+
|
|
15
|
+
# The counters a resolving spell enters with (CR 702.44a); type-changing effects are ignored,
|
|
16
|
+
# so the printed card types decide the kind. CR 702.44b: only colored mana counts.
|
|
17
|
+
def self.entering_counters(engine, spell)
|
|
18
|
+
printed = engine.chars(spell)
|
|
19
|
+
return {} unless printed.keyword?(:sunburst)
|
|
20
|
+
|
|
21
|
+
colors = (spell.mana_spent || []).uniq & COLORS
|
|
22
|
+
return {} if colors.empty?
|
|
23
|
+
|
|
24
|
+
{ printed.creature? ? "+1/+1" : "charge" => colors.size }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|