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,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Keywords
|
|
6
|
+
# CR 702.51a-d: convoke. While paying the spell's total cost, each untapped creature you control may be
|
|
7
|
+
# tapped to pay {1} or one mana of one of its colors (a `pay_cost` option `{type: "convoke", object: id}`).
|
|
8
|
+
class Convoke < Primitive
|
|
9
|
+
primitive :keyword, :convoke,
|
|
10
|
+
description: "Convoke: while paying this spell's total cost you may tap untapped creatures you " \
|
|
11
|
+
"control, each paying for {1} or one mana of one of its colors (pay_cost option " \
|
|
12
|
+
"`{type: \"convoke\", object: id}`). It isn't an alternative or additional cost.",
|
|
13
|
+
rules: %w[702.51a 702.51b 702.51c 702.51d],
|
|
14
|
+
example: { "keywords" => ["convoke"] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# CR 702.126a-c: improvise. For each generic mana in the spell's total cost, an untapped artifact you control
|
|
18
|
+
# may be tapped instead of paying it (a `pay_cost` option `{type: "improvise", object: id}`).
|
|
19
|
+
class Improvise < Primitive
|
|
20
|
+
primitive :keyword, :improvise,
|
|
21
|
+
description: "Improvise: while paying this spell's total cost you may tap untapped artifacts you " \
|
|
22
|
+
"control, each paying for {1} of its generic part (pay_cost option `{type: " \
|
|
23
|
+
"\"improvise\", object: id}`). It isn't an alternative or additional cost.",
|
|
24
|
+
rules: %w[702.126a 702.126b 702.126c],
|
|
25
|
+
example: { "keywords" => ["improvise"] }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module Abilities
|
|
30
|
+
# "Noncreature spells you cast have improvise." (CR 702.126a, 611.3a): while this permanent is on the
|
|
31
|
+
# battlefield, spells its controller casts matching the card spec `spells` have the listed helper keywords
|
|
32
|
+
# (convoke, improvise) as their costs are paid (Engine::AltCasting#helper_keywords).
|
|
33
|
+
class SpellsHave < Primitive
|
|
34
|
+
KEYWORDS = %w[convoke improvise].freeze
|
|
35
|
+
|
|
36
|
+
primitive :ability, :spells_have,
|
|
37
|
+
description: "Spells you cast matching the card spec `spells` (default: every spell) have `keywords` " \
|
|
38
|
+
"(convoke and/or improvise) while this permanent is on the battlefield.",
|
|
39
|
+
rules: %w[702.126a 702.51a 611.3a 604.1],
|
|
40
|
+
params: { "spells" => "card spec, e.g. {\"not_types\": [\"creature\"]}",
|
|
41
|
+
"keywords" => "list of convoke, improvise" },
|
|
42
|
+
example: { "spells_have" => { "spells" => { "not_types" => ["creature"] }, "keywords" => ["improvise"] } }
|
|
43
|
+
|
|
44
|
+
attr_reader :spells, :keywords
|
|
45
|
+
|
|
46
|
+
def self.build(params, errors, path, **)
|
|
47
|
+
Mechanics.check_keys(params, %w[spells keywords], errors, path)
|
|
48
|
+
keywords = Mechanics.fetch_param(params, :keywords)
|
|
49
|
+
unless keywords.is_a?(Array) && !keywords.empty? && (keywords - KEYWORDS).empty?
|
|
50
|
+
errors << "#{path}.keywords: must be a non-empty list of #{KEYWORDS.join(", ")}"
|
|
51
|
+
end
|
|
52
|
+
raw = Mechanics.fetch_param(params, :spells)
|
|
53
|
+
new(raw && CardSpec.build(raw, errors, "#{path}.spells"), Array(keywords).freeze)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(spells, keywords)
|
|
57
|
+
super()
|
|
58
|
+
@spells = spells
|
|
59
|
+
@keywords = keywords
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def kind = :spells_have
|
|
64
|
+
def mana_ability? = false
|
|
65
|
+
def allowed_types = nil
|
|
66
|
+
def try_effects = []
|
|
67
|
+
|
|
68
|
+
# The keywords `spell` gets from this ability of a permanent controlled by `controller`.
|
|
69
|
+
def keywords_for(engine, spell, controller)
|
|
70
|
+
spell.controller == controller && (spells.nil? || spells.match?(engine, spell)) ? keywords : []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_s = "#{spells&.to_s&.sub(/ card\z/, "")} spells you cast have #{keywords.join(", ")}".strip.capitalize
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Costs
|
|
6
|
+
# The cost of crew N (CR 702.122a): "Tap any number of other untapped creatures you control with total
|
|
7
|
+
# power N or greater." Each creature is a `choose_cost` decision and is tapped as it is chosen (cancelling
|
|
8
|
+
# untaps it); once their total power is N or more the player may stop (`done`) or tap more. Summoning
|
|
9
|
+
# sickness doesn't matter (CR 302.6). As the ability is activated each one crews the Vehicle (CR 702.122b).
|
|
10
|
+
class Crew < Primitive
|
|
11
|
+
attr_reader :power
|
|
12
|
+
|
|
13
|
+
def initialize(power)
|
|
14
|
+
super()
|
|
15
|
+
@power = power
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mana? = false
|
|
20
|
+
def pay!(_engine, _object) = nil
|
|
21
|
+
def choices_needed = power
|
|
22
|
+
def to_s = "Tap any number of other untapped creatures you control with total power #{power} or greater"
|
|
23
|
+
|
|
24
|
+
# The power a creature crews with: its power, unless it "crews Vehicles as though its power were 2 greater"
|
|
25
|
+
# or "using its toughness" (Abilities::TappingPower).
|
|
26
|
+
def crew_power(engine, creature) = Abilities::TappingPower.of(engine, creature, action)
|
|
27
|
+
def action = :crew
|
|
28
|
+
|
|
29
|
+
def candidates(engine, object)
|
|
30
|
+
engine.state.battlefield.select do |id|
|
|
31
|
+
other = engine.object(id)
|
|
32
|
+
id != object.id && other.controller == object.controller && !other.tapped && engine.chars(other).creature?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def payable?(engine, object) = candidates(engine, object).sum { crew_power(engine, engine.object(it)) } >= power
|
|
37
|
+
def choice_options(engine, object, _chosen) = candidates(engine, object).map { { object: it } }
|
|
38
|
+
|
|
39
|
+
def choose!(engine, option)
|
|
40
|
+
creature = engine.object(option[:object])
|
|
41
|
+
engine.tap!(creature)
|
|
42
|
+
{ "object" => creature.id, "power" => crew_power(engine, creature), "tapped" => true }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def finishable?(entries) = entries.sum { it["power"] } >= power
|
|
46
|
+
|
|
47
|
+
# Chosen once the total power is enough and the player is done (or nothing is left to tap).
|
|
48
|
+
def chosen?(engine, object, entries, done)
|
|
49
|
+
finishable?(entries) && (done || candidates(engine, object).empty?)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# CR 702.122b: each creature tapped to pay the cost crews the Vehicle.
|
|
53
|
+
def pay_chosen!(engine, object, entries)
|
|
54
|
+
entries.each { engine.emit Events::CrewedVehicle.new(object: it["object"], vehicle: object.id) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module Effects
|
|
60
|
+
# The effect of a crew ability (CR 702.122a, 702.122e): this permanent becomes an artifact creature until
|
|
61
|
+
# end of turn, and it "becomes crewed" by the creatures tapped to pay the cost.
|
|
62
|
+
class Crewed < Effect
|
|
63
|
+
def self.keys = %w[to]
|
|
64
|
+
|
|
65
|
+
def apply(engine, ctx)
|
|
66
|
+
vehicle = objects(engine, ctx).first or return
|
|
67
|
+
engine.add_continuous_effect!("add_types", ctx, [vehicle], "types" => %w[artifact creature], "supertypes" => [],
|
|
68
|
+
"subtypes" => [])
|
|
69
|
+
crew = (engine.object(ctx.object).paid || []).map { it["object"] }
|
|
70
|
+
engine.emit Events::BecameCrewed.new(object: vehicle, crew: crew)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_s = "this becomes an artifact creature until end of turn"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
module Abilities
|
|
78
|
+
# Crew N (CR 702.122a): an activated ability of a Vehicle. Written {"crew": {"power": N}}.
|
|
79
|
+
class Crew < ActivatedAbility
|
|
80
|
+
primitive :ability, :crew,
|
|
81
|
+
description: "Crew N: tap any number of other untapped creatures you control with total power N or " \
|
|
82
|
+
"more (chosen one at a time; `done` once the total is enough): this permanent becomes " \
|
|
83
|
+
"an artifact creature until end of turn. A Vehicle has its printed power and toughness " \
|
|
84
|
+
"only while it's a creature.",
|
|
85
|
+
rules: %w[702.122a 702.122b 702.122e 301.7 301.7a 301.7b 208.3 302.6],
|
|
86
|
+
params: { "power" => "integer >= 1 (the N of crew N)" },
|
|
87
|
+
example: { "crew" => { "power" => 3 } }
|
|
88
|
+
|
|
89
|
+
def self.build(params, errors, path, **)
|
|
90
|
+
Mechanics.check_keys(params, %w[power], errors, path)
|
|
91
|
+
n = Mechanics.integer_param(params, :power, errors, path, min: 1)
|
|
92
|
+
new(sorcery: false, targets: [].freeze, costs: [Costs::Crew.new(n)].freeze,
|
|
93
|
+
effects: [Effects::Crewed.new(to: Selector.new(:self), params: {}, errors: [], path: path)].freeze)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def power = costs.first.power
|
|
97
|
+
def to_s = "Crew #{power}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
module Triggers
|
|
102
|
+
# "Whenever this Vehicle becomes crewed" (CR 702.122e): whenever its crew ability resolves.
|
|
103
|
+
class BecomesCrewed < Trigger
|
|
104
|
+
primitive :trigger, :becomes_crewed,
|
|
105
|
+
description: "A crew ability of the subject resolves (it becomes crewed).",
|
|
106
|
+
rules: %w[702.122e 603.2],
|
|
107
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
|
|
108
|
+
example: { "becomes_crewed" => {} }
|
|
109
|
+
|
|
110
|
+
def event_class = Events::BecameCrewed
|
|
111
|
+
|
|
112
|
+
def match?(engine, event, source)
|
|
113
|
+
obj = engine.state.objects[event.object] or return false
|
|
114
|
+
@subject.match?(engine, event.object, obj.controller, obj.card, source, types: engine.chars(obj).types)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def occurrences(engine, event, source)
|
|
118
|
+
return [] unless match?(engine, event, source)
|
|
119
|
+
|
|
120
|
+
obj = engine.object(event.object)
|
|
121
|
+
[{ "object" => obj.id, "player" => obj.controller, "card" => obj.card, "amount" => event.crew.size }]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def to_s = "Whenever #{@subject} becomes crewed"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# "Whenever this creature crews a Vehicle" (CR 702.122b): it was tapped to pay a crew cost. The event's
|
|
128
|
+
# object for the effects ("that Vehicle") is the Vehicle.
|
|
129
|
+
class CrewsVehicle < Trigger
|
|
130
|
+
primitive :trigger, :crews_vehicle,
|
|
131
|
+
description: "The subject (a creature) crews a Vehicle: it's tapped to pay that Vehicle's crew cost. " \
|
|
132
|
+
"{\"triggering\": \"object\"} is the Vehicle.",
|
|
133
|
+
rules: %w[702.122b 603.2],
|
|
134
|
+
params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
|
|
135
|
+
example: { "crews_vehicle" => { "subject" => "self" } }
|
|
136
|
+
|
|
137
|
+
def event_class = Events::CrewedVehicle
|
|
138
|
+
|
|
139
|
+
def match?(engine, event, source)
|
|
140
|
+
obj = engine.state.objects[event.object] or return false
|
|
141
|
+
@subject.match?(engine, event.object, obj.controller, obj.card, source, types: engine.chars(obj).types)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def occurrences(engine, event, source)
|
|
145
|
+
return [] unless match?(engine, event, source)
|
|
146
|
+
|
|
147
|
+
vehicle = engine.state.objects[event.vehicle]
|
|
148
|
+
[{ "object" => event.vehicle, "player" => engine.object(event.object).controller, "card" => vehicle&.card }.compact]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def to_s = "Whenever #{@subject} crews a Vehicle"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Abilities
|
|
6
|
+
# Cycling and typecycling (CR 702.29): activated abilities that function only while the card
|
|
7
|
+
# is in its owner's hand.
|
|
8
|
+
# {"cycling": {"cost": "{2}"}} [cost], discard this card: draw a card
|
|
9
|
+
# {"cycling": {"cost": "{2}", "for": {"subtypes": ["swamp"]}}} ...: search for a Swamp card (typecycling)
|
|
10
|
+
# {"cycling": {"cost": "{2}", "for": {"supertypes": ["basic"], "types": ["land"]}}} basic landcycling
|
|
11
|
+
class Cycling < ActivatedAbility
|
|
12
|
+
primitive :ability, :cycling,
|
|
13
|
+
description: "Cycling: pay `cost` and discard this card from your hand to draw a card. With `for` " \
|
|
14
|
+
"it is typecycling: instead search your library for a card matching `for`, reveal " \
|
|
15
|
+
"it, put it into your hand, then shuffle.",
|
|
16
|
+
rules: %w[702.29a 702.29b 702.29e 702.29f 113.6m 602.2a],
|
|
17
|
+
params: { "cost" => "mana cost string, e.g. \"{2}\"",
|
|
18
|
+
"for" => "optional card spec for typecycling (see search_library)" },
|
|
19
|
+
example: { "cycling" => { "cost" => "{2}", "for" => { "supertypes" => ["basic"], "types" => ["land"] } } }
|
|
20
|
+
|
|
21
|
+
attr_reader :spec
|
|
22
|
+
|
|
23
|
+
def self.build(params, errors, path, **)
|
|
24
|
+
Mechanics.check_keys(params, %w[cost for], errors, path)
|
|
25
|
+
raw = Mechanics.fetch_param(params, :cost)
|
|
26
|
+
mana = raw.nil? ? ((errors << "#{path}.cost: required") && "{0}") : raw
|
|
27
|
+
costs = [Costs::ManaPayment.build(mana, errors, "#{path}.cost"), Costs::DiscardSelf::INSTANCE].freeze
|
|
28
|
+
spec_data = Mechanics.fetch_param(params, :for)
|
|
29
|
+
spec = spec_data.nil? ? nil : CardSpec.build(spec_data, errors, "#{path}.for")
|
|
30
|
+
new(targets: [].freeze, effects: [effect_for(spec_data)].compact.freeze, costs: costs, sorcery: false, spec: spec)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The ability's effect as card data, built like any other effect (errors in `for` are reported above).
|
|
34
|
+
def self.effect_for(spec_data)
|
|
35
|
+
data = if spec_data.nil?
|
|
36
|
+
{ "draw" => { "amount" => 1 } }
|
|
37
|
+
else
|
|
38
|
+
{ "search_library" => { "for" => spec_data, "reveal" => true } }
|
|
39
|
+
end
|
|
40
|
+
Mechanics.build_entry(:effect, data, [], "cycling")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(spec:, **)
|
|
44
|
+
@spec = spec
|
|
45
|
+
super(**)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# CR 702.29f: typecycling abilities are cycling abilities.
|
|
49
|
+
def cycling? = true
|
|
50
|
+
|
|
51
|
+
def to_s
|
|
52
|
+
name = spec ? "#{spec.to_s.delete_suffix(" card").capitalize}cycling" : "Cycling"
|
|
53
|
+
"#{name} #{costs.first}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "prowess"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Keywords
|
|
8
|
+
# Decayed (CR 702.147a): "This creature can't block" (Engine::Combat#eligible_blockers) and "When this
|
|
9
|
+
# creature attacks, sacrifice it at end of combat." A decayed counter gives it too (CR 122.1b).
|
|
10
|
+
class Decayed < Primitive
|
|
11
|
+
primitive :keyword, :decayed,
|
|
12
|
+
description: "This creature can't block. When it attacks, sacrifice it at end of combat.",
|
|
13
|
+
rules: %w[702.147a 122.1b], example: { "keywords" => ["decayed"] }
|
|
14
|
+
|
|
15
|
+
ABILITY = { "triggered" => { "when" => { "attacks" => {} },
|
|
16
|
+
"effects" => [{ "delayed_trigger" => {
|
|
17
|
+
"when" => { "step_begins" => { "step" => "end_of_combat", "whose" => "each" } },
|
|
18
|
+
"watch" => "self", "effects" => [{ "sacrifice" => { "to" => "watched" } }]
|
|
19
|
+
} }] } }.freeze
|
|
20
|
+
|
|
21
|
+
def self.intrinsic_abilities = @intrinsic_abilities ||= [Keywords.ability_from_data(ABILITY, "decayed")].freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../abilities/entering_choices"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Abilities
|
|
8
|
+
# Devour N / devour [quality] N (CR 702.82a, 702.82c): "As this object enters, you may sacrifice any number
|
|
9
|
+
# of creatures ([quality] permanents). This permanent enters with N +1/+1 counters on it for each one
|
|
10
|
+
# sacrificed this way." An "as this enters" choice of any number of permanents, picked one at a time
|
|
11
|
+
# (enter_choice `{type: "devour", object: id}`, then `{type: "done"}`); they're sacrificed as it enters,
|
|
12
|
+
# and it remembers how many it devoured (memory "devoured", CR 702.82b).
|
|
13
|
+
class Devour < EnteringChoice
|
|
14
|
+
primitive :ability, :devour,
|
|
15
|
+
description: "Devour `amount`: as this permanent enters, you may sacrifice any number of permanents " \
|
|
16
|
+
"matching `each` (default \"creature\"; \"land\" for devour land) that you control, one " \
|
|
17
|
+
"at a time; it enters with `amount` +1/+1 counters for each.",
|
|
18
|
+
rules: %w[702.82a 702.82b 702.82c 614.12 701.21a],
|
|
19
|
+
params: { "amount" => "integer >= 1", "each" => "permanent filter (default \"creature\")" },
|
|
20
|
+
example: { "devour" => { "amount" => 3, "each" => "land" } }
|
|
21
|
+
|
|
22
|
+
attr_reader :amount
|
|
23
|
+
|
|
24
|
+
def self.build(params, errors, path, **)
|
|
25
|
+
Mechanics.check_keys(params, %w[amount each], errors, path)
|
|
26
|
+
amount = Mechanics.integer_param(params, :amount, errors, path, min: 1)
|
|
27
|
+
raw = Mechanics.fetch_param(params, :each) || "creature"
|
|
28
|
+
new(amount, Selector.build({ "each" => raw, "controller" => "you" }, errors, "#{path}.each", targets_count: 0))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(amount, selector)
|
|
32
|
+
super()
|
|
33
|
+
@amount = amount
|
|
34
|
+
@selector = selector
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_s = "Devour #{amount} (#{@selector})"
|
|
39
|
+
|
|
40
|
+
# The permanents still to pick from, then "done" (CR 702.82a: any number, so none is fine).
|
|
41
|
+
def pick_options(engine, controller, chosen)
|
|
42
|
+
ctx = EffectContext.new(controller, nil, nil, [], [])
|
|
43
|
+
(@selector.resolve(engine, ctx) - chosen).map { { type: "devour", object: it } } + [{ type: "done" }]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enters_for(answer)
|
|
47
|
+
count = Array(answer[:objects]).size
|
|
48
|
+
{ "counters" => count.positive? ? { "+1/+1" => amount * count } : {}, "memory" => { "devoured" => count } }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# As it enters: the chosen permanents still there are sacrificed (CR 701.21a).
|
|
52
|
+
def pay!(engine, controller, answer)
|
|
53
|
+
Array(answer["objects"]).each do |id|
|
|
54
|
+
obj = engine.state.objects[id]
|
|
55
|
+
engine.move!(id, :graveyard, reason: "sacrifice") if obj&.zone == :battlefield && obj.controller == controller
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Keywords
|
|
6
|
+
# Evergreen keyword abilities. A keyword is a flag on the object's characteristics;
|
|
7
|
+
# the rules code asks chars(obj).keyword?(:name) at the point the rule applies.
|
|
8
|
+
class Evergreen < Primitive
|
|
9
|
+
def self.define(name, description, rules)
|
|
10
|
+
klass = Class.new(self)
|
|
11
|
+
Keywords.const_set(name.to_s.split("_").map(&:capitalize).join, klass)
|
|
12
|
+
klass.primitive :keyword, name, description: description, rules: rules, example: { "keywords" => [name.to_s.tr("_", " ")] }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
flying: ["Can't be blocked except by creatures with flying or reach.", %w[702.9b]],
|
|
18
|
+
reach: ["Can block creatures with flying.", %w[702.17b]],
|
|
19
|
+
vigilance: ["Attacking doesn't cause this creature to tap.", %w[702.20b]],
|
|
20
|
+
menace: ["Can't be blocked except by two or more creatures.", %w[702.111b]],
|
|
21
|
+
defender: ["Can't attack.", %w[702.3b]],
|
|
22
|
+
trample: ["Excess combat damage beyond lethal damage to its blockers can be assigned to the player or " \
|
|
23
|
+
"planeswalker it's attacking.", %w[702.19b 702.19e]],
|
|
24
|
+
first_strike: ["Deals combat damage in a first combat damage step, before creatures without first strike.",
|
|
25
|
+
%w[702.7b 510.4]],
|
|
26
|
+
double_strike: ["Deals combat damage in both the first-strike and the regular combat damage step.",
|
|
27
|
+
%w[702.4b 510.4]],
|
|
28
|
+
deathtouch: ["Any damage it deals to a creature is enough to destroy it; 1 damage counts as lethal when " \
|
|
29
|
+
"assigning combat damage.", %w[702.2b 702.2c 704.5h]],
|
|
30
|
+
lifelink: ["Damage dealt by this source also causes its controller to gain that much life.", %w[702.15b]],
|
|
31
|
+
indestructible: ["Can't be destroyed: lethal damage, deathtouch and \"destroy\" effects don't put it into " \
|
|
32
|
+
"the graveyard.", %w[702.12b]],
|
|
33
|
+
hexproof: ["Can't be the target of spells or abilities your opponents control.", %w[702.11b]],
|
|
34
|
+
wither: ["Damage this source deals to creatures is dealt as that many -1/-1 counters instead of marked " \
|
|
35
|
+
"damage, put by its controller.", %w[702.80a 702.80c 702.80d 120.3d]]
|
|
36
|
+
}.each { |name, (description, rules)| Evergreen.define(name, description, rules) }
|
|
37
|
+
|
|
38
|
+
# Flash (CR 702.8): may be cast any time its controller could cast an instant.
|
|
39
|
+
class Flash < Primitive
|
|
40
|
+
primitive :keyword, :flash,
|
|
41
|
+
description: "You may cast this spell any time you could cast an instant.",
|
|
42
|
+
rules: %w[702.8a], example: { "keywords" => ["flash"] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Haste (CR 702.10): can attack and {T} the turn it comes under your control.
|
|
46
|
+
class Haste < Primitive
|
|
47
|
+
primitive :keyword, :haste,
|
|
48
|
+
description: "This creature can attack and use {T} abilities even if it hasn't been under your " \
|
|
49
|
+
"control continuously since your most recent turn began.",
|
|
50
|
+
rules: %w[702.10a 302.6], example: { "keywords" => ["haste"] }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Keywords
|
|
6
|
+
# Extort (CR 702.101a): "Whenever you cast a spell, you may pay {W/B}. If you do, each opponent loses 1 life
|
|
7
|
+
# and you gain life equal to the total life lost this way." Each instance triggers separately (CR 702.101b).
|
|
8
|
+
# The life lost inside the payment is recorded on the payment (effect 0), so the gain reads it.
|
|
9
|
+
class Extort < Primitive
|
|
10
|
+
primitive :keyword, :extort,
|
|
11
|
+
description: "Whenever you cast a spell, you may pay {W/B}. If you do, each opponent loses 1 life " \
|
|
12
|
+
"and you gain life equal to the total life lost this way.",
|
|
13
|
+
rules: %w[702.101a 702.101b 118.12], example: { "keywords" => ["extort"] }
|
|
14
|
+
|
|
15
|
+
ABILITY = { "triggered" => {
|
|
16
|
+
"when" => { "cast" => { "controller" => "you" } },
|
|
17
|
+
"effects" => [{ "pay" => { "mana" => "{W/B}",
|
|
18
|
+
"then" => [{ "lose_life" => { "amount" => 1, "to" => "each_opponent" } }] } },
|
|
19
|
+
{ "gain_life" => { "amount" => { "this_way" => 0 } } }]
|
|
20
|
+
} }.freeze
|
|
21
|
+
|
|
22
|
+
def self.intrinsic_abilities = @intrinsic_abilities ||= [Keywords.ability_from_data(ABILITY, "extort")].freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "evergreen"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Keywords
|
|
8
|
+
# Rules modifications that work like a keyword: a flag on the object's characteristics that the
|
|
9
|
+
# rules code checks, printed, handed out by a static ability (often a conditional one) or gained until end of
|
|
10
|
+
# turn.
|
|
11
|
+
{
|
|
12
|
+
attacks_despite_defender: ["Defender doesn't stop this creature from attacking.", %w[702.3b 508.1c]],
|
|
13
|
+
doesnt_untap: ["This permanent doesn't untap during its controller's untap step.", %w[502.3]],
|
|
14
|
+
assigns_damage_by_toughness: ["If this creature's toughness is greater than its power, it assigns combat damage " \
|
|
15
|
+
"equal to its toughness rather than its power.", %w[510.1a]],
|
|
16
|
+
keeps_damage: ["Damage isn't removed from this permanent during cleanup steps (remove_damage still removes it).",
|
|
17
|
+
%w[514.2 120.6]],
|
|
18
|
+
abilities_cant_be_activated: ["Its activated abilities, mana abilities included, can't be activated (\"Activated " \
|
|
19
|
+
"abilities of artifacts can't be activated\" is a static handing it out).",
|
|
20
|
+
%w[602.5 605.3a 101.2]]
|
|
21
|
+
}.each { |name, (description, rules)| Evergreen.define(name, description, rules) }
|
|
22
|
+
|
|
23
|
+
# Combat restrictions and requirements (CR 508.1c-d, 509.1b). They aren't keyword abilities
|
|
24
|
+
# in the CR (702), but they work exactly like one: a flag on the object's current
|
|
25
|
+
# characteristics that combat code checks. So a card prints them (`"keywords": ["cant_block"]`),
|
|
26
|
+
# a static ability hands them out ("Creatures your opponents control can't block") and the
|
|
27
|
+
# `grant` effect gives them until end of turn ("Target creature can't be blocked this turn").
|
|
28
|
+
{
|
|
29
|
+
cant_attack: ["Restriction: this creature can't attack.", %w[508.1c]],
|
|
30
|
+
cant_block: ["Restriction: this creature can't block.", %w[509.1b]],
|
|
31
|
+
cant_be_blocked: ["Restriction: this creature can't be blocked.", %w[509.1b]],
|
|
32
|
+
cant_attack_alone: ["Restriction: this creature can't attack unless another creature attacks too. It is " \
|
|
33
|
+
"offered as an attacker only while another creature attacks or could still be declared, " \
|
|
34
|
+
"and declaring attackers can't finish while it attacks alone.", %w[506.5 508.1c]],
|
|
35
|
+
attacks_each_combat: ["Requirement: this creature attacks each combat if able. Declaring attackers can't " \
|
|
36
|
+
"finish while it could still attack.", %w[508.1d]],
|
|
37
|
+
must_be_blocked: ["Requirement: this attacking creature must be blocked if able. Declaring blockers can't " \
|
|
38
|
+
"finish while it is unblocked and a creature that could block it blocks nothing.", %w[509.1c]]
|
|
39
|
+
}.each { |name, (description, rules)| Evergreen.define(name, description, rules) }
|
|
40
|
+
|
|
41
|
+
# "This spell can't be countered." (CR 101.2: a "can't" wins over an effect that would counter it.)
|
|
42
|
+
Evergreen.define(:cant_be_countered, "This spell can't be countered: counter effects leave it on the stack.",
|
|
43
|
+
%w[701.6a 101.2])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "alternative_costs"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Abilities
|
|
8
|
+
# Foretell (CR 702.143a-b): any time its owner has priority during their turn, they may pay {2} and exile
|
|
9
|
+
# the card from their hand face down (a special action, `{type: "special_action", object: id, ability:
|
|
10
|
+
# ref}`, CR 116.2h). They may look at it while it remains exiled, and cast it after that turn has ended by
|
|
11
|
+
# paying its foretell cost rather than its mana cost (cast option `via: "alt:<ref>"`, an alternative cost).
|
|
12
|
+
class Foretell < AlternativeCost
|
|
13
|
+
primitive :ability, :foretell,
|
|
14
|
+
description: "Foretell: during your turn, you may pay {2} and exile this card from your hand face down " \
|
|
15
|
+
"(special action `{type: \"special_action\"}`). After that turn you may cast it from " \
|
|
16
|
+
"exile by paying `cost` rather than its mana cost (cast option `via: \"alt:<ref>\"`). " \
|
|
17
|
+
"`{\"cast_with\": \"foretell\"}` reads it.",
|
|
18
|
+
rules: %w[702.143a 702.143b 702.143c 116.2h 406.3 118.9 601.2b 601.2f 601.2h],
|
|
19
|
+
params: { "cost" => "mana cost string, or a list of costs" },
|
|
20
|
+
example: { "foretell" => { "cost" => "{2}{R}" } }
|
|
21
|
+
|
|
22
|
+
FORETELL_COST = Mana::Cost.parse("{2}")
|
|
23
|
+
|
|
24
|
+
def zone = :exile
|
|
25
|
+
|
|
26
|
+
# CR 702.143a: a card foretold on an earlier turn (the exiled card remembers the turn).
|
|
27
|
+
def functions?(engine, obj, _index)
|
|
28
|
+
turn = obj.zone == :exile && obj.memory["foretold"]
|
|
29
|
+
turn ? engine.state.turn > turn : false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ---- the special action (Actions::SpecialAction) ----
|
|
33
|
+
|
|
34
|
+
def special_cost = FORETELL_COST
|
|
35
|
+
def special_allowed?(engine, obj, index) = obj.zone == :hand && engine.state.active == index
|
|
36
|
+
|
|
37
|
+
# Exiled face down; its owner may look at it (CR 702.143a, 406.3).
|
|
38
|
+
def special!(engine, obj, index)
|
|
39
|
+
fresh = engine.move!(obj.id, :exile, reason: "foretell", face_down: [index])
|
|
40
|
+
fresh.memory["foretold"] = engine.state.turn if fresh
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "kicker"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Costs
|
|
8
|
+
# The gift cost (CR 702.174a): "you may choose an opponent" — one `choose_cost` decision among the
|
|
9
|
+
# opponents. As the spell becomes cast it remembers the chosen player ("gift"), who the gift goes to.
|
|
10
|
+
class GiftOpponent
|
|
11
|
+
def mana? = false
|
|
12
|
+
def choices_needed = 1
|
|
13
|
+
def payable?(engine, object) = !opponents(engine, object).empty?
|
|
14
|
+
def opponents(engine, object) = engine.state.players.each_index.reject { it == object.controller }
|
|
15
|
+
def choice_options(engine, object, _chosen) = opponents(engine, object).map { { object: Filters.player_ref(engine, it) } }
|
|
16
|
+
def choose!(_engine, option) = { "object" => option[:object] }
|
|
17
|
+
def pay_chosen!(_engine, object, entries) = object.memory["gift"] = entries.first&.fetch("object")
|
|
18
|
+
def to_s = "Choose an opponent"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module Abilities
|
|
23
|
+
# Gift (CR 702.174a-b, k): "As an additional cost to cast this spell, you may choose an opponent." It is
|
|
24
|
+
# announced like kicker (the `choose_kicker` decision), then the opponent is chosen (`choose_cost`). The
|
|
25
|
+
# gift itself is ordinary card data: the effect, first in the spell's effects (CR 702.174j) or in an
|
|
26
|
+
# "enters" trigger of a permanent, with `"if": {"cast_with": "gift"}` ("if the gift was promised") and the
|
|
27
|
+
# selector "gift_recipient" for the chosen player. Targets only for a promised gift are `kicked_targets`
|
|
28
|
+
# (CR 702.174m).
|
|
29
|
+
class Gift < SpellCost
|
|
30
|
+
primitive :ability, :gift,
|
|
31
|
+
description: "Gift: as you cast this spell you may promise a gift to an opponent you choose (the " \
|
|
32
|
+
"`choose_kicker` decision, then `choose_cost` for the opponent). `{\"cast_with\": " \
|
|
33
|
+
"\"gift\"}` holds if you did, and the selector \"gift_recipient\" is that player; write " \
|
|
34
|
+
"the gift as the spell's first effect (or an enters trigger) under that condition.",
|
|
35
|
+
rules: %w[702.174a 702.174b 702.174j 702.174k 702.174m 601.2b],
|
|
36
|
+
example: "gift"
|
|
37
|
+
|
|
38
|
+
COST = Costs::GiftOpponent.new.freeze
|
|
39
|
+
|
|
40
|
+
def self.build(params, errors, path, **)
|
|
41
|
+
Mechanics.check_keys(params, [], errors, path)
|
|
42
|
+
new([COST].freeze)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def kind = :kicker
|
|
46
|
+
def to_s = "Gift"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mobilize_and_firebending"
|
|
4
|
+
|
|
5
|
+
module Enjine
|
|
6
|
+
module Mechanics
|
|
7
|
+
module Abilities
|
|
8
|
+
# Hideaway N (CR 702.75a): "When this permanent enters, look at the top N cards of your library. Exile one
|
|
9
|
+
# of them face down and put the rest on the bottom of your library in a random order." Only its controller
|
|
10
|
+
# may look at the exiled card. It's linked to the permanent (CR 607.2a): another of its abilities names it
|
|
11
|
+
# with the selector "exiled_with", e.g. `cast_free` with `play: true` ("you may play the exiled card
|
|
12
|
+
# without paying its mana cost").
|
|
13
|
+
class Hideaway < NumberedTrigger
|
|
14
|
+
primitive :ability, :hideaway,
|
|
15
|
+
description: "Hideaway `amount`: when this permanent enters, look at the top N cards of your library, " \
|
|
16
|
+
"exile one face down (linked: \"exiled_with\") and put the rest on the bottom in a random order.",
|
|
17
|
+
rules: %w[702.75a 406.3 607.2a],
|
|
18
|
+
params: { "amount" => "amount" },
|
|
19
|
+
example: { "hideaway" => { "amount" => 4 } }
|
|
20
|
+
|
|
21
|
+
def self.expansion(amount)
|
|
22
|
+
{ "when" => { "enters" => {} },
|
|
23
|
+
"effects" => [{ "dig" => { "amount" => amount, "put" => "exile", "face_down" => true, "link" => true } }] }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|