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,370 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../copy_except"
|
|
4
|
+
require_relative "../abilities/play_permissions"
|
|
5
|
+
|
|
6
|
+
module Enjine
|
|
7
|
+
module Mechanics
|
|
8
|
+
module Effects
|
|
9
|
+
# Effects that copy something named by `of` (CR 707.1). "of": "self" is the source in any zone,
|
|
10
|
+
# or, when it has left its zone (a card exiled from a graveyard to pay the cost), its card.
|
|
11
|
+
module CopyOf
|
|
12
|
+
def setup_of(params, errors, path)
|
|
13
|
+
raw = Mechanics.fetch_param(params, :of)
|
|
14
|
+
return (errors << "#{path}.of: required") && nil if raw.nil?
|
|
15
|
+
|
|
16
|
+
Selector.build(raw, errors, "#{path}.of", targets_count: @targets_count, results_count: @results_count)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def of_refs(engine, ctx)
|
|
20
|
+
return [] unless @of
|
|
21
|
+
return @of.resolve(engine, ctx) unless @of.kind == :self
|
|
22
|
+
|
|
23
|
+
[ctx.source].compact
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The copiable values [card, except] of each referenced object (CR 707.2). An object that left its
|
|
27
|
+
# zone since is used as it last existed (CR 608.2h).
|
|
28
|
+
def copy_values(engine, ctx)
|
|
29
|
+
of_refs(engine, ctx).filter_map do |ref|
|
|
30
|
+
obj = engine.state.objects[ref]
|
|
31
|
+
next engine.copiable_values(obj) if obj && !obj.ability?
|
|
32
|
+
|
|
33
|
+
lki = engine.resolving_last_known(ref)
|
|
34
|
+
next [lki["card"], lki["except"] || {}] if lki&.key?("card")
|
|
35
|
+
|
|
36
|
+
source_card(engine, ctx) if @of.kind == :self
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def source_card(engine, ctx)
|
|
41
|
+
stacked = engine.state.objects[ctx.object]
|
|
42
|
+
stacked&.ability? ? [stacked.card, {}] : nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Choosing new targets for a spell or ability on the stack, one target at a time: keep the old one (even if
|
|
47
|
+
# it's illegal now) or pick a legal new one (CR 115.7d, 707.10c). `legal_for`: whose targets they are
|
|
48
|
+
# (legality, CR 115.7); `chooser`: who decides.
|
|
49
|
+
module NewTargets
|
|
50
|
+
def choose_new_targets(engine, spell, legal_for, locals, chooser: legal_for)
|
|
51
|
+
old = spell.targets || []
|
|
52
|
+
specs = engine.resolving_ability(spell)&.targets_for(spell.modes || [], kicked: spell.memory.key?("kicked_with")) || []
|
|
53
|
+
slots = engine.target_slots(old, specs) # each target of a counted slot in turn; same number (CR 707.10c)
|
|
54
|
+
chosen = answers(locals).map { it["target"] }
|
|
55
|
+
return engine.regroup_targets(chosen, old) if chosen.size >= slots.size
|
|
56
|
+
|
|
57
|
+
i = chosen.size
|
|
58
|
+
current, spec = slots[i]
|
|
59
|
+
keep = [{ "type" => "target", "target" => current }]
|
|
60
|
+
others = spec ? new_target_choices(engine, spec, legal_for, spell, current) : []
|
|
61
|
+
ask(engine, locals, chooser, "Choose target ##{i + 1} for #{engine.catalog.fetch(spell.card).name}", keep + others)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def new_target_choices(engine, spec, controller, spell, current)
|
|
67
|
+
source = spell.ability? ? spell.source : nil # an ability's copy has the same source (CR 707.10b)
|
|
68
|
+
spec.candidates_for(engine, controller, source).filter_map do |ref|
|
|
69
|
+
next if ref == current || ref == spell.id || !engine.targetable?(ref, controller, source, targeter: source || spell.id)
|
|
70
|
+
|
|
71
|
+
{ "type" => "target", "target" => ref }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# "Create a token that's a copy of ..." (CR 707.8, 111.1).
|
|
77
|
+
class CreateTokenCopy < Effect
|
|
78
|
+
include CopyOf
|
|
79
|
+
|
|
80
|
+
primitive :effect, :create_token_copy,
|
|
81
|
+
description: "For each object `of` refers to (a permanent, a card in any zone, or the source with " \
|
|
82
|
+
"\"self\"), the referenced players (default: you) each create `amount` tokens that " \
|
|
83
|
+
"are copies of it: its copiable values (its card as modified by other copy effects, " \
|
|
84
|
+
"not counters, status or other effects), with the `except` modifications. The tokens " \
|
|
85
|
+
"are the effect's result, for later effects such as granting haste or a delayed " \
|
|
86
|
+
"sacrifice.",
|
|
87
|
+
rules: %w[707.2 707.8 707.9a 707.9b 111.1 614.1c],
|
|
88
|
+
params: { "of" => "selector (what to copy)", "amount" => "amount (default 1)", "tapped" => "boolean",
|
|
89
|
+
"except" => CopyExcept::PARAM, "to" => "selector (default \"you\")" },
|
|
90
|
+
example: { "create_token_copy" => { "of" => { "target" => 0 }, "except" => { "legendary" => false } } }
|
|
91
|
+
|
|
92
|
+
def self.keys = %w[of amount tapped except to if]
|
|
93
|
+
def self.default_to = "you"
|
|
94
|
+
|
|
95
|
+
def initialize(to:, params:, errors:, path:, targets_count: 0, results_count: 0)
|
|
96
|
+
params = params.transform_keys(&:to_s)
|
|
97
|
+
params["amount"] = 1 unless params.key?("amount")
|
|
98
|
+
super
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def setup(params, errors, path)
|
|
102
|
+
@of = setup_of(params, errors, path)
|
|
103
|
+
@tapped = Mechanics.flag_param(params, :tapped, errors, path)
|
|
104
|
+
@except = CopyExcept.build(Mechanics.fetch_param(params, :except), errors, "#{path}.except")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_s
|
|
108
|
+
"create #{amount} token cop#{amount == 1 ? "y" : "ies"} of #{@of}#{" tapped" if @tapped}" \
|
|
109
|
+
"#{CopyExcept.to_s(@except)} → #{to}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def apply(engine, ctx)
|
|
113
|
+
n = amount_for(engine, ctx)
|
|
114
|
+
values = copy_values(engine, ctx)
|
|
115
|
+
players(engine, ctx).each do |index|
|
|
116
|
+
values.each do |card, except|
|
|
117
|
+
merged = [card, Engine::Copies.merge_except(except, @except)]
|
|
118
|
+
n.times { engine.create_token_copy!(merged, index, tapped: @tapped) }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# "... becomes a copy of ..." (CR 707.4, 613.2a), for a duration.
|
|
125
|
+
class BecomeCopy < Effect
|
|
126
|
+
include CopyOf
|
|
127
|
+
|
|
128
|
+
DURATIONS = %w[end_of_turn permanent].freeze
|
|
129
|
+
|
|
130
|
+
primitive :effect, :become_copy,
|
|
131
|
+
description: "Each referenced permanent becomes a copy of the (first) object `of` refers to, with the " \
|
|
132
|
+
"`except` modifications, until end of turn (default) or for as long as it stays on the " \
|
|
133
|
+
"battlefield (`duration: \"permanent\"`). It keeps its counters, status and other " \
|
|
134
|
+
"effects; nothing enters or leaves, so no enter or leave abilities trigger. Several copy " \
|
|
135
|
+
"effects apply in timestamp order.",
|
|
136
|
+
rules: %w[707.2 707.4 613.2a 613.7 611.2a 514.2],
|
|
137
|
+
params: { "to" => "selector (the permanents that become copies)", "of" => "selector (what they copy)",
|
|
138
|
+
"except" => CopyExcept::PARAM, "duration" => DURATIONS.join(" | ") },
|
|
139
|
+
example: { "become_copy" => { "to" => "self", "of" => { "target" => 0 } } }
|
|
140
|
+
|
|
141
|
+
def self.keys = %w[of except duration to if]
|
|
142
|
+
|
|
143
|
+
def setup(params, errors, path)
|
|
144
|
+
@of = setup_of(params, errors, path)
|
|
145
|
+
@except = CopyExcept.build(Mechanics.fetch_param(params, :except), errors, "#{path}.except")
|
|
146
|
+
@duration = Mechanics.fetch_param(params, :duration) || "end_of_turn"
|
|
147
|
+
errors << "#{path}.duration: must be one of #{DURATIONS.join(", ")}" unless DURATIONS.include?(@duration)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def to_s = "becomes a copy of #{@of}#{CopyExcept.to_s(@except)} (#{@duration.tr("_", " ")}) → #{to}"
|
|
151
|
+
|
|
152
|
+
def apply(engine, ctx)
|
|
153
|
+
first = copy_values(engine, ctx).first or return
|
|
154
|
+
values = [first[0], Engine::Copies.merge_except(first[1], @except)]
|
|
155
|
+
objects(engine, ctx).each { engine.become_copy!(engine.object(it), values, duration: @duration) }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# "Copy [spell]" / "copy target activated or triggered ability" (CR 707.10): the copy goes on the stack
|
|
160
|
+
# with the same choices; it isn't cast or activated.
|
|
161
|
+
class CopySpell < ChoiceEffect
|
|
162
|
+
include CopyOf
|
|
163
|
+
include NewTargets
|
|
164
|
+
|
|
165
|
+
primitive :effect, :copy_spell,
|
|
166
|
+
description: "Copy each spell or ability on the stack `of` refers to (\"self\": the spell this ability " \
|
|
167
|
+
"or spell belongs to; a `stack_ability` target: an activated or triggered ability) " \
|
|
168
|
+
"`amount` times. The copies are put on the stack under the referenced player's " \
|
|
169
|
+
"control (default: you) with the same modes, X, kicker and targets; they aren't cast " \
|
|
170
|
+
"or activated, and an ability's copy has the same source. With `new_targets: true` " \
|
|
171
|
+
"the controller may choose new targets for each copy. A copy of a permanent spell " \
|
|
172
|
+
"becomes a token as it resolves. `except`: the copies have these differences (e.g. " \
|
|
173
|
+
"{\"keywords\": [\"wither\"]}: \"the copy gains wither\").",
|
|
174
|
+
rules: %w[707.10 707.10a 707.10b 707.10c 707.10f 608.3f],
|
|
175
|
+
params: { "of" => "selector (spells or abilities on the stack)", "amount" => "amount (default 1)",
|
|
176
|
+
"new_targets" => "boolean", "to" => "selector (default \"you\")", "except" => CopyExcept::PARAM },
|
|
177
|
+
example: { "copy_spell" => { "of" => "self" } }
|
|
178
|
+
|
|
179
|
+
def self.keys = %w[of amount new_targets except to if]
|
|
180
|
+
def self.default_to = "you"
|
|
181
|
+
|
|
182
|
+
def initialize(to:, params:, errors:, path:, targets_count: 0, results_count: 0)
|
|
183
|
+
params = params.transform_keys(&:to_s)
|
|
184
|
+
params["amount"] = 1 unless params.key?("amount")
|
|
185
|
+
super
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def setup(params, errors, path)
|
|
189
|
+
@of = setup_of(params, errors, path)
|
|
190
|
+
@new_targets = Mechanics.flag_param(params, :new_targets, errors, path)
|
|
191
|
+
@except = CopyExcept.build(Mechanics.fetch_param(params, :except), errors, "#{path}.except")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def to_s = "copy #{@of}#{" #{amount} times" if amount != 1}#{" (new targets)" if @new_targets} → #{to}"
|
|
195
|
+
|
|
196
|
+
# Each copy is planned as [spell, controller]; with new targets, its targets are chosen one at a
|
|
197
|
+
# time (keep the old one or pick a legal new one, CR 707.10c) before it's put on the stack.
|
|
198
|
+
def run(engine, ctx, locals)
|
|
199
|
+
return :done unless locals["plan"] || condition.nil? || condition.true?(engine, ctx)
|
|
200
|
+
|
|
201
|
+
locals["plan"] ||= plan(engine, ctx)
|
|
202
|
+
while (spell_id, controller = locals["plan"].first)
|
|
203
|
+
spell = engine.state.objects[spell_id]
|
|
204
|
+
if spell&.zone == :stack
|
|
205
|
+
targets = @new_targets ? choose_new_targets(engine, spell, controller, locals) : spell.targets&.dup
|
|
206
|
+
return :wait if targets == :wait
|
|
207
|
+
|
|
208
|
+
engine.copy_spell!(spell, controller, targets, except: @except)
|
|
209
|
+
end
|
|
210
|
+
locals["plan"].shift
|
|
211
|
+
locals.delete("answers")
|
|
212
|
+
locals.delete("asked")
|
|
213
|
+
end
|
|
214
|
+
:done
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def plan(engine, ctx)
|
|
220
|
+
n = amount_for(engine, ctx)
|
|
221
|
+
spells = of_refs(engine, ctx).select { engine.state.objects[it]&.zone == :stack }
|
|
222
|
+
players(engine, ctx).flat_map { |index| spells.flat_map { |id| Array.new(n) { [id, index] } } }
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# "You may choose new targets for that spell" (CR 115.7d): the controller of this effect goes through each
|
|
227
|
+
# target of the spell or ability `of` refers to, keeping it or choosing a new legal one (legal for the spell's
|
|
228
|
+
# controller). Only the final set of targets is checked (CR 115.7e).
|
|
229
|
+
class ChooseNewTargets < ChoiceEffect
|
|
230
|
+
include CopyOf
|
|
231
|
+
include NewTargets
|
|
232
|
+
|
|
233
|
+
primitive :effect, :choose_new_targets,
|
|
234
|
+
description: "You may choose new targets for each spell or ability on the stack `of` refers to (e.g. " \
|
|
235
|
+
"{\"triggering\": \"object\"}: \"that spell\"): for each target, keep it or pick another " \
|
|
236
|
+
"legal target for that spell.",
|
|
237
|
+
rules: %w[115.7 115.7d 115.7e],
|
|
238
|
+
params: { "of" => "selector (spells or abilities on the stack)" },
|
|
239
|
+
example: { "choose_new_targets" => { "of" => { "triggering" => "object" } } }
|
|
240
|
+
|
|
241
|
+
def self.keys = %w[of if]
|
|
242
|
+
def self.default_to = "you"
|
|
243
|
+
def setup(params, errors, path) = @of = setup_of(params, errors, path)
|
|
244
|
+
def to_s = "you may choose new targets for #{@of}"
|
|
245
|
+
|
|
246
|
+
def run(engine, ctx, locals)
|
|
247
|
+
return :done unless locals["spells"] || condition.nil? || condition.true?(engine, ctx)
|
|
248
|
+
|
|
249
|
+
locals["spells"] ||= of_refs(engine, ctx).select { engine.state.objects[it]&.zone == :stack }
|
|
250
|
+
while (id = locals["spells"].first)
|
|
251
|
+
spell = engine.state.objects[id]
|
|
252
|
+
if spell&.zone == :stack && !Array(spell.targets).flatten.compact.empty?
|
|
253
|
+
targets = choose_new_targets(engine, spell, spell.controller, locals, chooser: ctx.controller)
|
|
254
|
+
return :wait if targets == :wait
|
|
255
|
+
|
|
256
|
+
engine.change_targets!(spell, targets, ctx.controller)
|
|
257
|
+
end
|
|
258
|
+
locals["spells"].shift
|
|
259
|
+
locals.delete("answers")
|
|
260
|
+
end
|
|
261
|
+
:done
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# "Copy that card" / "create a copy of this card" (CR 707.12): copies of cards, in the zone the card is in.
|
|
266
|
+
class CopyCard < Effect
|
|
267
|
+
include CopyOf
|
|
268
|
+
|
|
269
|
+
primitive :effect, :copy_card,
|
|
270
|
+
description: "Create `amount` copies of each card `of` refers to (\"self\": the source card), in the zone " \
|
|
271
|
+
"that card is in, owned by you. They are the effect's result, e.g. for `cast_free` or " \
|
|
272
|
+
"`cast`. A card copy ceases to exist if it is anywhere but the stack or the " \
|
|
273
|
+
"battlefield (so one that isn't cast now is gone at the next state-based-action check).",
|
|
274
|
+
rules: %w[707.12 707.12a 707.10a 704.5d],
|
|
275
|
+
params: { "of" => "selector (cards)", "amount" => "amount (default 1)" },
|
|
276
|
+
example: { "copy_card" => { "of" => { "result" => 0 }, "amount" => 2 } }
|
|
277
|
+
|
|
278
|
+
def self.keys = %w[of amount if]
|
|
279
|
+
def self.default_to = "you"
|
|
280
|
+
|
|
281
|
+
def initialize(to:, params:, errors:, path:, targets_count: 0, results_count: 0)
|
|
282
|
+
params = params.transform_keys(&:to_s)
|
|
283
|
+
params["amount"] = 1 unless params.key?("amount")
|
|
284
|
+
super
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def setup(params, errors, path) = @of = setup_of(params, errors, path)
|
|
288
|
+
def to_s = "copy #{@of}#{" #{amount} times" if amount != 1}"
|
|
289
|
+
|
|
290
|
+
def apply(engine, ctx)
|
|
291
|
+
n = amount_for(engine, ctx)
|
|
292
|
+
of_refs(engine, ctx).each do |ref|
|
|
293
|
+
obj = engine.state.objects[ref]
|
|
294
|
+
next unless obj && !obj.ability? && obj.zone != :battlefield
|
|
295
|
+
|
|
296
|
+
n.times { engine.create_card_copy!(engine.copiable_values(obj), ctx.controller, obj.zone) }
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# "You may cast [it]" while the effect resolves, paying its costs (CR 608.2g, 707.12, 601.2a): like
|
|
302
|
+
# cast_free, but the spell's mana cost is paid as usual.
|
|
303
|
+
class Cast < CastFree
|
|
304
|
+
primitive :effect, :cast,
|
|
305
|
+
description: "The controller may cast referenced cards (or card copies) now, while this resolves, " \
|
|
306
|
+
"paying their costs; timing restrictions don't apply. `to`, `for`, `not_types`, " \
|
|
307
|
+
"`max_mana_value`, `count`, `exile_instead` and `any_mana_type` work as for `cast_free`.",
|
|
308
|
+
rules: %w[608.2g 707.12 707.12a 601.2a 601.2f],
|
|
309
|
+
params: { "to" => "selector", "for" => "card spec", "not_types" => "list of card types",
|
|
310
|
+
"max_mana_value" => "amount", "count" => "1 or \"any\"",
|
|
311
|
+
"exile_instead" => "true or a card spec (as for cast_free)",
|
|
312
|
+
"any_mana_type" => "boolean: mana of any type can be spent to cast it (CR 609.4b)" },
|
|
313
|
+
example: { "cast" => { "to" => { "result" => 0 } } }
|
|
314
|
+
|
|
315
|
+
def to_s = super.sub(" free", "")
|
|
316
|
+
def via = "resolving"
|
|
317
|
+
def prompt = "You may cast a spell"
|
|
318
|
+
def castable_now?(engine, index, id) = engine.castable_while_resolving?(index, id, any_mana_type: @riders["any_mana_type"])
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# A card or permanent gains an ability (CR 113.6b, 611.2): "that card gains '...'". The ability lives
|
|
322
|
+
# in this card's data (its reference "granted:<card>:<ref>"); it lasts while the object stays in its
|
|
323
|
+
# zone, or until end of turn.
|
|
324
|
+
class GrantAbility < Effect
|
|
325
|
+
DURATIONS = %w[permanent end_of_turn].freeze
|
|
326
|
+
|
|
327
|
+
primitive :effect, :grant_ability,
|
|
328
|
+
description: "Each referenced object (a permanent, or a card in any zone, e.g. an exiled card) gains " \
|
|
329
|
+
"`ability` (any ability entry) for as long as it stays in its zone (default) or until " \
|
|
330
|
+
"end of turn. `for` (a card spec) keeps only matching objects (\"that nonland card\"). " \
|
|
331
|
+
"An activated ability with `zone` works from that zone (\"activate only if this card is " \
|
|
332
|
+
"exiled\").",
|
|
333
|
+
rules: %w[113.6b 611.2a 613.1f],
|
|
334
|
+
params: { "ability" => "an ability", "to" => "selector", "for" => "card spec",
|
|
335
|
+
"duration" => DURATIONS.join(" | ") },
|
|
336
|
+
example: { "grant_ability" => { "to" => { "result" => 0 },
|
|
337
|
+
"ability" => { "activated" => { "cost" => [{ "mana" => "{1}" }],
|
|
338
|
+
"zone" => "exile",
|
|
339
|
+
"effects" => [{ "draw" => { "amount" => 1 } }] } } } }
|
|
340
|
+
|
|
341
|
+
attr_reader :ref, :granted_ability
|
|
342
|
+
|
|
343
|
+
def self.keys = %w[ability duration for to if]
|
|
344
|
+
|
|
345
|
+
def setup(params, errors, path)
|
|
346
|
+
@ref = "grant:#{path}"
|
|
347
|
+
@for = CardSpec.build(Mechanics.fetch_param(params, :for), errors, "#{path}.for")
|
|
348
|
+
raw = Mechanics.fetch_param(params, :ability)
|
|
349
|
+
@granted_ability = if raw.nil?
|
|
350
|
+
(errors << "#{path}.ability: required") && nil
|
|
351
|
+
else
|
|
352
|
+
Mechanics.build_entry(:ability, raw, errors,
|
|
353
|
+
"#{path}.ability")
|
|
354
|
+
end
|
|
355
|
+
@duration = Mechanics.fetch_param(params, :duration) || "permanent"
|
|
356
|
+
errors << "#{path}.duration: must be one of #{DURATIONS.join(", ")}" unless DURATIONS.include?(@duration)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def nested_effects = @granted_ability.respond_to?(:effects) ? @granted_ability.effects : []
|
|
360
|
+
def to_s = "gains \"#{@granted_ability}\"#{" until end of turn" if @duration == "end_of_turn"} → #{to}"
|
|
361
|
+
|
|
362
|
+
def apply(engine, ctx)
|
|
363
|
+
card = engine.state.objects[ctx.object]&.card or return
|
|
364
|
+
affected = refs(engine, ctx).select { (obj = engine.state.objects[it]) && @for.match?(engine, obj) }
|
|
365
|
+
engine.grant_card_ability!(ctx, affected, card, @ref, @duration) unless affected.empty?
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Counter a spell or ability (CR 701.6).
|
|
7
|
+
class CounterSpell < Effect
|
|
8
|
+
primitive :effect, :counter, description: "Counter each referenced spell or ability: a spell leaves the " \
|
|
9
|
+
"stack for its owner's graveyard without resolving, an ability " \
|
|
10
|
+
"ceases to exist. A spell that can't be countered stays.",
|
|
11
|
+
rules: %w[701.6a 701.6b 101.2], params: { "to" => "selector" },
|
|
12
|
+
example: { "counter" => { "to" => { "target" => 0 } } }
|
|
13
|
+
def self.keys = %w[to if]
|
|
14
|
+
|
|
15
|
+
def apply(engine, ctx)
|
|
16
|
+
refs(engine, ctx).each { engine.counter!(it) if engine.state.stack.include?(it) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Put counters on permanents or players (CR 122).
|
|
7
|
+
class AddCounters < Effect
|
|
8
|
+
primitive :effect, :add_counters,
|
|
9
|
+
description: "Put `amount` counters named `counter` on each referenced permanent or player. Any " \
|
|
10
|
+
"counter name works (+1/+1, -1/-1, loyalty, stun, shield, oil, a keyword, ...; on a " \
|
|
11
|
+
"player poison, experience, ...); see docs/reference/counters.md for the ones with " \
|
|
12
|
+
"rules. A player with ten or more poison counters loses the game.",
|
|
13
|
+
rules: %w[122.1 122.1f 122.6 613.4c 704.5c],
|
|
14
|
+
params: { "counter" => "counter name (+X/+Y, -X/-Y or a lowercase word)", "amount" => "integer",
|
|
15
|
+
"to" => "selector" },
|
|
16
|
+
example: { "add_counters" => { "counter" => "+1/+1", "amount" => 1,
|
|
17
|
+
"to" => { "each" => "creature", "controller" => "you" } } }
|
|
18
|
+
|
|
19
|
+
attr_reader :counter
|
|
20
|
+
|
|
21
|
+
def self.keys = %w[counter amount to if]
|
|
22
|
+
|
|
23
|
+
def setup(params, errors, path)
|
|
24
|
+
@counter = Counters.parse(Mechanics.fetch_param(params, :counter), errors, "#{path}.counter")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s = "#{amount} #{counter} counter(s) → #{to}"
|
|
28
|
+
|
|
29
|
+
def apply(engine, ctx)
|
|
30
|
+
n = amount_for(engine, ctx)
|
|
31
|
+
objects(engine, ctx).each { engine.add_counters!(engine.object(it), counter, n, by_player: ctx.controller) }
|
|
32
|
+
players(engine, ctx).each { engine.add_player_counters!(it, counter, n) } # "that player gets two poison counters"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# "Remove two +1/+1 counters from target creature." (CR 122.8)
|
|
37
|
+
class RemoveCounters < Effect
|
|
38
|
+
primitive :effect, :remove_counters,
|
|
39
|
+
description: "Remove up to `amount` counters named `counter` from each referenced permanent.",
|
|
40
|
+
rules: %w[122.8],
|
|
41
|
+
params: { "counter" => "counter name", "amount" => "amount", "to" => "selector" },
|
|
42
|
+
example: { "remove_counters" => { "counter" => "+1/+1", "amount" => 2, "to" => { "target" => 0 } } }
|
|
43
|
+
|
|
44
|
+
attr_reader :counter
|
|
45
|
+
|
|
46
|
+
def self.keys = %w[counter amount to if]
|
|
47
|
+
|
|
48
|
+
def setup(params, errors, path)
|
|
49
|
+
@counter = Counters.parse(Mechanics.fetch_param(params, :counter), errors, "#{path}.counter")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def apply(engine, ctx)
|
|
53
|
+
n = amount_for(engine, ctx)
|
|
54
|
+
objects(engine, ctx).each { engine.remove_counters!(engine.object(it), counter, n) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_s = "remove #{amount} #{counter} counter(s) → #{to}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# "Remove all +1/+1 counters from it" / "remove all counters from it".
|
|
61
|
+
class RemoveAllCounters < Effect
|
|
62
|
+
primitive :effect, :remove_all_counters,
|
|
63
|
+
description: "Remove every counter named `counter` (every counter, if no `counter` is given) from " \
|
|
64
|
+
"each referenced permanent.",
|
|
65
|
+
rules: %w[122.8],
|
|
66
|
+
params: { "counter" => "optional counter name", "to" => "selector" },
|
|
67
|
+
example: { "remove_all_counters" => { "counter" => "+1/+1", "to" => { "target" => 0 } } }
|
|
68
|
+
|
|
69
|
+
attr_reader :counter
|
|
70
|
+
|
|
71
|
+
def self.keys = %w[counter to if]
|
|
72
|
+
|
|
73
|
+
def setup(params, errors, path)
|
|
74
|
+
raw = Mechanics.fetch_param(params, :counter)
|
|
75
|
+
@counter = raw && Counters.parse(raw, errors, "#{path}.counter")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def apply(engine, ctx) = objects(engine, ctx).each { engine.remove_all_counters!(engine.object(it), counter) }
|
|
79
|
+
def to_s = "remove all #{counter || "counters"} → #{to}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# "Choose a counter on target permanent. Put another counter of that kind on it." (CR 122.1, 122.6):
|
|
83
|
+
# for each referenced permanent with counters, its controller's chosen kind among the kinds already on
|
|
84
|
+
# it (no choice with only one kind) gets one more counter. Nothing happens to one without counters.
|
|
85
|
+
class AnotherCounter < ChoiceEffect
|
|
86
|
+
primitive :effect, :another_counter,
|
|
87
|
+
description: "For each referenced permanent, the controller chooses a kind of counter already on it " \
|
|
88
|
+
"(asked only when there are several kinds) and puts one more counter of that kind on it.",
|
|
89
|
+
rules: %w[122.1 122.6],
|
|
90
|
+
params: { "to" => "selector" },
|
|
91
|
+
example: { "another_counter" => { "to" => { "target" => 0 } } }
|
|
92
|
+
|
|
93
|
+
def self.keys = %w[to if]
|
|
94
|
+
|
|
95
|
+
def to_s = "another counter of a kind already there → #{to}"
|
|
96
|
+
|
|
97
|
+
def run(engine, ctx, locals)
|
|
98
|
+
unless locals["ids"]
|
|
99
|
+
return :done unless condition.nil? || condition.true?(engine, ctx)
|
|
100
|
+
|
|
101
|
+
locals["ids"] = objects(engine, ctx)
|
|
102
|
+
locals["i"] = 0
|
|
103
|
+
end
|
|
104
|
+
while locals["i"] < locals["ids"].size
|
|
105
|
+
obj = engine.state.objects[locals["ids"][locals["i"]]]
|
|
106
|
+
kinds = obj&.zone == :battlefield ? obj.counters.select { |_, n| n.positive? }.keys.sort : []
|
|
107
|
+
kind = kinds.first
|
|
108
|
+
if kinds.size > 1
|
|
109
|
+
answer = answers(locals)[locals.fetch("k", 0)] or return ask_kind(engine, ctx, obj, kinds)
|
|
110
|
+
kind = answer["counter"]
|
|
111
|
+
locals["k"] = locals.fetch("k", 0) + 1
|
|
112
|
+
end
|
|
113
|
+
engine.add_counters!(obj, kind, 1, by_player: ctx.controller) if kind
|
|
114
|
+
locals["i"] += 1
|
|
115
|
+
end
|
|
116
|
+
:done
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def ask_kind(engine, ctx, obj, kinds)
|
|
122
|
+
engine.ask!("kind" => "effect_choice", "player" => ctx.controller,
|
|
123
|
+
"prompt" => "Choose a kind of counter on #{obj.id} to put another of",
|
|
124
|
+
"choices" => kinds.map { { "type" => "counter", "counter" => it } })
|
|
125
|
+
:wait
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# "Distribute N +1/+1 counters among one or two target creatures" (CR 601.2d): the division was
|
|
130
|
+
# announced while casting (the spell ability's `divide`); each target that is still legal gets its
|
|
131
|
+
# share (CR 608.2b).
|
|
132
|
+
class DistributeCounters < Effect
|
|
133
|
+
primitive :effect, :distribute_counters,
|
|
134
|
+
description: "Put on each legal target the number of `counter` counters assigned to it as the spell " \
|
|
135
|
+
"or ability was cast (its `divide` amount, at least one per target).",
|
|
136
|
+
rules: %w[601.2d 608.2b 122.1],
|
|
137
|
+
params: { "counter" => "counter name" },
|
|
138
|
+
example: { "distribute_counters" => { "counter" => "+1/+1" } }
|
|
139
|
+
|
|
140
|
+
attr_reader :counter
|
|
141
|
+
|
|
142
|
+
def self.keys = %w[counter if]
|
|
143
|
+
def self.default_to = "you"
|
|
144
|
+
|
|
145
|
+
def setup(params, errors, path)
|
|
146
|
+
@counter = Counters.parse(Mechanics.fetch_param(params, :counter), errors, "#{path}.counter")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def to_s = "distribute #{counter} counters among the targets"
|
|
150
|
+
|
|
151
|
+
def apply(engine, ctx)
|
|
152
|
+
engine.object(ctx.object).memory.fetch("division", {}).each do |ref, n|
|
|
153
|
+
next unless ctx.targets.flatten.include?(ref) # still a legal target (CR 608.2b)
|
|
154
|
+
|
|
155
|
+
engine.add_counters!(engine.object(ref), counter, n, by_player: ctx.controller)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enjine
|
|
4
|
+
module Mechanics
|
|
5
|
+
module Effects
|
|
6
|
+
# Deal N damage (CR 120).
|
|
7
|
+
class DealDamage < Effect
|
|
8
|
+
primitive :effect, :deal_damage,
|
|
9
|
+
description: "Deal `amount` damage to each referenced creature, planeswalker or player. Damage to a " \
|
|
10
|
+
"player causes life loss; to a planeswalker removes loyalty counters; to a creature " \
|
|
11
|
+
"is marked until cleanup. {\"this_way\": n} naming it is the excess damage it dealt " \
|
|
12
|
+
"(beyond lethal damage, or a planeswalker's loyalty).",
|
|
13
|
+
rules: %w[120.3a 120.3c 120.3e 120.6 120.4a 120.10],
|
|
14
|
+
params: { "amount" => "integer", "to" => "selector" },
|
|
15
|
+
example: { "deal_damage" => { "amount" => 3, "to" => { "target" => 0 } } }
|
|
16
|
+
|
|
17
|
+
# The excess damage it deals (beyond lethal damage to a creature, or a planeswalker's loyalty) is
|
|
18
|
+
# what {"this_way": n} reads ("if excess damage was dealt this way", CR 120.4a, 120.10).
|
|
19
|
+
def apply(engine, ctx)
|
|
20
|
+
n = amount_for(engine, ctx)
|
|
21
|
+
source = ctx.source || ctx.object
|
|
22
|
+
refs(engine, ctx).each do |ref|
|
|
23
|
+
lethal = engine.lethal_damage(source, ref)
|
|
24
|
+
engine.record_amount!(engine.excess_damage(lethal, engine.deal_damage!(source, ref, n)))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# "Deals 3 damage divided as you choose among one, two, or three targets" (CR 601.2d): the division was
|
|
30
|
+
# announced as the spell or ability was put on the stack (its `divide` amount, at least one per target);
|
|
31
|
+
# each target that is still legal is dealt its share (CR 608.2b).
|
|
32
|
+
class DistributeDamage < Effect
|
|
33
|
+
primitive :effect, :distribute_damage,
|
|
34
|
+
description: "Deal each legal target the damage assigned to it as the spell or ability was cast or " \
|
|
35
|
+
"put on the stack (its `divide` amount, at least one per target). {\"this_way\": n} " \
|
|
36
|
+
"naming it is the excess damage dealt.",
|
|
37
|
+
rules: %w[601.2d 608.2b 120.3 120.4a],
|
|
38
|
+
params: {},
|
|
39
|
+
example: { "distribute_damage" => {} }
|
|
40
|
+
|
|
41
|
+
def self.keys = %w[if]
|
|
42
|
+
def self.default_to = "you"
|
|
43
|
+
|
|
44
|
+
def to_s = "deal the divided damage to the targets"
|
|
45
|
+
|
|
46
|
+
def apply(engine, ctx)
|
|
47
|
+
source = ctx.source || ctx.object
|
|
48
|
+
excess = engine.object(ctx.object).memory.fetch("division", {}).sum do |ref, n|
|
|
49
|
+
next 0 unless ctx.targets.flatten.include?(ref) # still a legal target (CR 608.2b)
|
|
50
|
+
|
|
51
|
+
engine.excess_damage(engine.lethal_damage(source, ref), engine.deal_damage!(source, ref, n))
|
|
52
|
+
end
|
|
53
|
+
engine.record_amount!(excess)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# "All damage already dealt to it is healed" / "remove all damage from it" (CR 120.6, 514.2).
|
|
58
|
+
class RemoveDamage < Effect
|
|
59
|
+
primitive :effect, :remove_damage,
|
|
60
|
+
description: "Remove all damage marked on each referenced permanent.",
|
|
61
|
+
rules: %w[120.6 514.2],
|
|
62
|
+
params: { "to" => "selector" },
|
|
63
|
+
example: { "remove_damage" => { "to" => { "target" => 0 } } }
|
|
64
|
+
|
|
65
|
+
def self.keys = %w[to if]
|
|
66
|
+
|
|
67
|
+
def apply(engine, ctx) = objects(engine, ctx).each { engine.remove_damage!(engine.object(it)) }
|
|
68
|
+
def to_s = "remove all damage → #{to}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|