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.
Files changed (269) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +201 -0
  5. data/SUPPORTED.md +387 -0
  6. data/docs/ARCHITECTURE.md +141 -0
  7. data/docs/DESIGN.md +139 -0
  8. data/docs/MECHANICS_COVERAGE.md +670 -0
  9. data/docs/guides/adding-a-mechanic.md +60 -0
  10. data/docs/guides/api.md +614 -0
  11. data/docs/guides/card-data-format.md +1472 -0
  12. data/docs/guides/deck-builder.md +157 -0
  13. data/docs/plans/standard.md +371 -0
  14. data/docs/reference/abilities.md +1680 -0
  15. data/docs/reference/costs.md +339 -0
  16. data/docs/reference/counters.md +121 -0
  17. data/docs/reference/decisions.md +102 -0
  18. data/docs/reference/effects.md +2340 -0
  19. data/docs/reference/filters.md +172 -0
  20. data/docs/reference/keywords.md +647 -0
  21. data/docs/reference/subtypes.md +169 -0
  22. data/docs/reference/supertypes.md +31 -0
  23. data/docs/reference/triggers.md +797 -0
  24. data/docs/reference/types.md +216 -0
  25. data/exe/enjine +7 -0
  26. data/lib/enjine/actions/activate_mana_ability.rb +24 -0
  27. data/lib/enjine/actions/cast.rb +57 -0
  28. data/lib/enjine/actions/pass.rb +18 -0
  29. data/lib/enjine/actions/play_land.rb +52 -0
  30. data/lib/enjine/actions/special_action.rb +37 -0
  31. data/lib/enjine/actions.rb +20 -0
  32. data/lib/enjine/agents/busy.rb +27 -0
  33. data/lib/enjine/agents/random.rb +28 -0
  34. data/lib/enjine/card_definition.rb +433 -0
  35. data/lib/enjine/card_faces.rb +116 -0
  36. data/lib/enjine/catalog.rb +56 -0
  37. data/lib/enjine/characteristics.rb +39 -0
  38. data/lib/enjine/cli/text.rb +295 -0
  39. data/lib/enjine/cli.rb +312 -0
  40. data/lib/enjine/decisions/bottom.rb +29 -0
  41. data/lib/enjine/decisions/casting.rb +188 -0
  42. data/lib/enjine/decisions/combat.rb +75 -0
  43. data/lib/enjine/decisions/discard.rb +29 -0
  44. data/lib/enjine/decisions/effect_choice.rb +21 -0
  45. data/lib/enjine/decisions/enter_choice.rb +26 -0
  46. data/lib/enjine/decisions/legend_rule.rb +21 -0
  47. data/lib/enjine/decisions/mulligan.rb +26 -0
  48. data/lib/enjine/decisions/order_triggers.rb +21 -0
  49. data/lib/enjine/decisions/play_or_draw.rb +19 -0
  50. data/lib/enjine/decisions/priority.rb +22 -0
  51. data/lib/enjine/decisions/untap_choice.rb +35 -0
  52. data/lib/enjine/decisions.rb +22 -0
  53. data/lib/enjine/deck.rb +151 -0
  54. data/lib/enjine/deck_builder.rb +454 -0
  55. data/lib/enjine/docs.rb +90 -0
  56. data/lib/enjine/engine/ability_layers.rb +118 -0
  57. data/lib/enjine/engine/attachments.rb +86 -0
  58. data/lib/enjine/engine/casting/alt_casting.rb +254 -0
  59. data/lib/enjine/engine/casting/cast_choices.rb +292 -0
  60. data/lib/enjine/engine/casting/casting.rb +326 -0
  61. data/lib/enjine/engine/casting/cost_options.rb +99 -0
  62. data/lib/enjine/engine/casting/costs.rb +117 -0
  63. data/lib/enjine/engine/casting/payment.rb +353 -0
  64. data/lib/enjine/engine/casting/permissions.rb +412 -0
  65. data/lib/enjine/engine/casting/target_counts.rb +113 -0
  66. data/lib/enjine/engine/casting/waterbend.rb +90 -0
  67. data/lib/enjine/engine/characteristics.rb +379 -0
  68. data/lib/enjine/engine/class_levels.rb +25 -0
  69. data/lib/enjine/engine/combat.rb +315 -0
  70. data/lib/enjine/engine/copies.rb +208 -0
  71. data/lib/enjine/engine/damage.rb +196 -0
  72. data/lib/enjine/engine/designations.rb +46 -0
  73. data/lib/enjine/engine/emblems.rb +37 -0
  74. data/lib/enjine/engine/entering.rb +156 -0
  75. data/lib/enjine/engine/face_down.rb +30 -0
  76. data/lib/enjine/engine/faces.rb +222 -0
  77. data/lib/enjine/engine/follow_ups.rb +27 -0
  78. data/lib/enjine/engine/mana_sources.rb +230 -0
  79. data/lib/enjine/engine/phasing.rb +74 -0
  80. data/lib/enjine/engine/pregame.rb +95 -0
  81. data/lib/enjine/engine/priority.rb +47 -0
  82. data/lib/enjine/engine/resolution.rb +168 -0
  83. data/lib/enjine/engine/rules.rb +287 -0
  84. data/lib/enjine/engine/sagas.rb +41 -0
  85. data/lib/enjine/engine/speed.rb +104 -0
  86. data/lib/enjine/engine/spell_grants.rb +46 -0
  87. data/lib/enjine/engine/state_based_actions.rb +157 -0
  88. data/lib/enjine/engine/triggers.rb +364 -0
  89. data/lib/enjine/engine/turn_history.rb +240 -0
  90. data/lib/enjine/engine/turn_structure.rb +224 -0
  91. data/lib/enjine/engine/zones.rb +278 -0
  92. data/lib/enjine/engine.rb +55 -0
  93. data/lib/enjine/errors.rb +62 -0
  94. data/lib/enjine/events.rb +161 -0
  95. data/lib/enjine/frames.rb +84 -0
  96. data/lib/enjine/fuzz.rb +112 -0
  97. data/lib/enjine/game.rb +204 -0
  98. data/lib/enjine/history.rb +26 -0
  99. data/lib/enjine/mana.rb +142 -0
  100. data/lib/enjine/mechanics/abilities/additional_triggers.rb +86 -0
  101. data/lib/enjine/mechanics/abilities/chosen_values.rb +213 -0
  102. data/lib/enjine/mechanics/abilities/cost_changes.rb +154 -0
  103. data/lib/enjine/mechanics/abilities/damage_replacements.rb +569 -0
  104. data/lib/enjine/mechanics/abilities/double_counters.rb +71 -0
  105. data/lib/enjine/mechanics/abilities/entering_choices.rb +91 -0
  106. data/lib/enjine/mechanics/abilities/entering_copy.rb +69 -0
  107. data/lib/enjine/mechanics/abilities/enters_tapped.rb +159 -0
  108. data/lib/enjine/mechanics/abilities/enters_with_counters.rb +116 -0
  109. data/lib/enjine/mechanics/abilities/mana_ability.rb +209 -0
  110. data/lib/enjine/mechanics/abilities/play_permissions.rb +527 -0
  111. data/lib/enjine/mechanics/abilities/player_rules.rb +169 -0
  112. data/lib/enjine/mechanics/abilities/replacement_effects.rb +368 -0
  113. data/lib/enjine/mechanics/abilities/restrictions.rb +144 -0
  114. data/lib/enjine/mechanics/abilities/spell_keywords.rb +55 -0
  115. data/lib/enjine/mechanics/abilities/stack_abilities.rb +542 -0
  116. data/lib/enjine/mechanics/abilities/static.rb +365 -0
  117. data/lib/enjine/mechanics/abilities/tapping_power.rb +71 -0
  118. data/lib/enjine/mechanics/abilities/turn_rules.rb +49 -0
  119. data/lib/enjine/mechanics/card_spec.rb +177 -0
  120. data/lib/enjine/mechanics/copy_except.rb +73 -0
  121. data/lib/enjine/mechanics/costs/base.rb +115 -0
  122. data/lib/enjine/mechanics/costs/behold.rb +76 -0
  123. data/lib/enjine/mechanics/costs/discard.rb +65 -0
  124. data/lib/enjine/mechanics/costs/exert.rb +29 -0
  125. data/lib/enjine/mechanics/costs/exile_cards.rb +39 -0
  126. data/lib/enjine/mechanics/costs/from_graveyard.rb +45 -0
  127. data/lib/enjine/mechanics/costs/get_counters.rb +39 -0
  128. data/lib/enjine/mechanics/costs/mana.rb +37 -0
  129. data/lib/enjine/mechanics/costs/one_of.rb +49 -0
  130. data/lib/enjine/mechanics/costs/pay_life.rb +50 -0
  131. data/lib/enjine/mechanics/costs/remove_counters.rb +51 -0
  132. data/lib/enjine/mechanics/costs/sacrifice.rb +59 -0
  133. data/lib/enjine/mechanics/costs/tap.rb +56 -0
  134. data/lib/enjine/mechanics/costs/waterbend.rb +29 -0
  135. data/lib/enjine/mechanics/counters/kinds.rb +190 -0
  136. data/lib/enjine/mechanics/effects/add_subtypes.rb +30 -0
  137. data/lib/enjine/mechanics/effects/airbend.rb +41 -0
  138. data/lib/enjine/mechanics/effects/amass.rb +124 -0
  139. data/lib/enjine/mechanics/effects/assimilate.rb +72 -0
  140. data/lib/enjine/mechanics/effects/base.rb +148 -0
  141. data/lib/enjine/mechanics/effects/become.rb +182 -0
  142. data/lib/enjine/mechanics/effects/behold.rb +48 -0
  143. data/lib/enjine/mechanics/effects/blight.rb +94 -0
  144. data/lib/enjine/mechanics/effects/choose_cards.rb +168 -0
  145. data/lib/enjine/mechanics/effects/copy.rb +370 -0
  146. data/lib/enjine/mechanics/effects/counter_spell.rb +21 -0
  147. data/lib/enjine/mechanics/effects/counters.rb +161 -0
  148. data/lib/enjine/mechanics/effects/damage.rb +72 -0
  149. data/lib/enjine/mechanics/effects/delayed_triggers.rb +112 -0
  150. data/lib/enjine/mechanics/effects/destroy.rb +22 -0
  151. data/lib/enjine/mechanics/effects/discard.rb +86 -0
  152. data/lib/enjine/mechanics/effects/draw.rb +16 -0
  153. data/lib/enjine/mechanics/effects/earthbend.rb +70 -0
  154. data/lib/enjine/mechanics/effects/emblem.rb +52 -0
  155. data/lib/enjine/mechanics/effects/endure.rb +63 -0
  156. data/lib/enjine/mechanics/effects/exile.rb +49 -0
  157. data/lib/enjine/mechanics/effects/extra_turn.rb +122 -0
  158. data/lib/enjine/mechanics/effects/faces.rb +115 -0
  159. data/lib/enjine/mechanics/effects/fight.rb +76 -0
  160. data/lib/enjine/mechanics/effects/flip_coins.rb +97 -0
  161. data/lib/enjine/mechanics/effects/gain_control.rb +94 -0
  162. data/lib/enjine/mechanics/effects/game_end.rb +42 -0
  163. data/lib/enjine/mechanics/effects/library.rb +327 -0
  164. data/lib/enjine/mechanics/effects/life.rb +83 -0
  165. data/lib/enjine/mechanics/effects/look_at_hand.rb +17 -0
  166. data/lib/enjine/mechanics/effects/loot.rb +118 -0
  167. data/lib/enjine/mechanics/effects/mana.rb +96 -0
  168. data/lib/enjine/mechanics/effects/may.rb +82 -0
  169. data/lib/enjine/mechanics/effects/meld.rb +77 -0
  170. data/lib/enjine/mechanics/effects/mill.rb +22 -0
  171. data/lib/enjine/mechanics/effects/pay.rb +231 -0
  172. data/lib/enjine/mechanics/effects/phasing.rb +51 -0
  173. data/lib/enjine/mechanics/effects/proliferate.rb +72 -0
  174. data/lib/enjine/mechanics/effects/pump.rb +60 -0
  175. data/lib/enjine/mechanics/effects/put_in_library.rb +98 -0
  176. data/lib/enjine/mechanics/effects/return_choice.rb +29 -0
  177. data/lib/enjine/mechanics/effects/return_to_battlefield.rb +80 -0
  178. data/lib/enjine/mechanics/effects/return_to_hand.rb +20 -0
  179. data/lib/enjine/mechanics/effects/roll_dice.rb +45 -0
  180. data/lib/enjine/mechanics/effects/sacrifice.rb +118 -0
  181. data/lib/enjine/mechanics/effects/scry_and_surveil.rb +74 -0
  182. data/lib/enjine/mechanics/effects/search.rb +156 -0
  183. data/lib/enjine/mechanics/effects/tap_untap.rb +23 -0
  184. data/lib/enjine/mechanics/effects/tokens.rb +76 -0
  185. data/lib/enjine/mechanics/filters/any_target.rb +15 -0
  186. data/lib/enjine/mechanics/filters/base.rb +386 -0
  187. data/lib/enjine/mechanics/filters/graveyard.rb +23 -0
  188. data/lib/enjine/mechanics/filters/one_of.rb +55 -0
  189. data/lib/enjine/mechanics/filters/permanents.rb +52 -0
  190. data/lib/enjine/mechanics/filters/players.rb +20 -0
  191. data/lib/enjine/mechanics/filters/stack.rb +97 -0
  192. data/lib/enjine/mechanics/follow_up.rb +46 -0
  193. data/lib/enjine/mechanics/keywords/alternative_costs.rb +361 -0
  194. data/lib/enjine/mechanics/keywords/beam_me_up.rb +78 -0
  195. data/lib/enjine/mechanics/keywords/changeling.rb +58 -0
  196. data/lib/enjine/mechanics/keywords/convoke.rb +77 -0
  197. data/lib/enjine/mechanics/keywords/crew.rb +155 -0
  198. data/lib/enjine/mechanics/keywords/cycling.rb +58 -0
  199. data/lib/enjine/mechanics/keywords/decayed.rb +25 -0
  200. data/lib/enjine/mechanics/keywords/devour.rb +61 -0
  201. data/lib/enjine/mechanics/keywords/evergreen.rb +54 -0
  202. data/lib/enjine/mechanics/keywords/extort.rb +26 -0
  203. data/lib/enjine/mechanics/keywords/flags.rb +46 -0
  204. data/lib/enjine/mechanics/keywords/foretell.rb +45 -0
  205. data/lib/enjine/mechanics/keywords/gift.rb +50 -0
  206. data/lib/enjine/mechanics/keywords/hideaway.rb +28 -0
  207. data/lib/enjine/mechanics/keywords/increment.rb +31 -0
  208. data/lib/enjine/mechanics/keywords/job_select.rb +29 -0
  209. data/lib/enjine/mechanics/keywords/kicker.rb +92 -0
  210. data/lib/enjine/mechanics/keywords/mobilize_and_firebending.rb +84 -0
  211. data/lib/enjine/mechanics/keywords/paradigm.rb +91 -0
  212. data/lib/enjine/mechanics/keywords/power_up.rb +48 -0
  213. data/lib/enjine/mechanics/keywords/protection.rb +111 -0
  214. data/lib/enjine/mechanics/keywords/prowess.rb +30 -0
  215. data/lib/enjine/mechanics/keywords/riot.rb +35 -0
  216. data/lib/enjine/mechanics/keywords/saddle.rb +115 -0
  217. data/lib/enjine/mechanics/keywords/speed.rb +89 -0
  218. data/lib/enjine/mechanics/keywords/station.rb +163 -0
  219. data/lib/enjine/mechanics/keywords/storied.rb +41 -0
  220. data/lib/enjine/mechanics/keywords/storm_and_cascade.rb +86 -0
  221. data/lib/enjine/mechanics/keywords/sunburst.rb +29 -0
  222. data/lib/enjine/mechanics/keywords/suspend.rb +191 -0
  223. data/lib/enjine/mechanics/keywords/teamwork.rb +45 -0
  224. data/lib/enjine/mechanics/keywords/ward.rb +67 -0
  225. data/lib/enjine/mechanics/linked.rb +67 -0
  226. data/lib/enjine/mechanics/mana_spent.rb +99 -0
  227. data/lib/enjine/mechanics/selector.rb +207 -0
  228. data/lib/enjine/mechanics/subtypes/auras_and_equipment.rb +176 -0
  229. data/lib/enjine/mechanics/subtypes/basic_land_types.rb +36 -0
  230. data/lib/enjine/mechanics/subtypes/class_levels.rb +102 -0
  231. data/lib/enjine/mechanics/subtypes/saga.rb +89 -0
  232. data/lib/enjine/mechanics/supertypes/basic.rb +17 -0
  233. data/lib/enjine/mechanics/supertypes/legendary.rb +18 -0
  234. data/lib/enjine/mechanics/triggers/ability_triggers.rb +51 -0
  235. data/lib/enjine/mechanics/triggers/any_of.rb +43 -0
  236. data/lib/enjine/mechanics/triggers/base.rb +228 -0
  237. data/lib/enjine/mechanics/triggers/casting.rb +219 -0
  238. data/lib/enjine/mechanics/triggers/combat.rb +123 -0
  239. data/lib/enjine/mechanics/triggers/damage.rb +130 -0
  240. data/lib/enjine/mechanics/triggers/dilemma.rb +37 -0
  241. data/lib/enjine/mechanics/triggers/draw_and_discard.rb +85 -0
  242. data/lib/enjine/mechanics/triggers/keyword_actions.rb +55 -0
  243. data/lib/enjine/mechanics/triggers/life.rb +35 -0
  244. data/lib/enjine/mechanics/triggers/steps.rb +39 -0
  245. data/lib/enjine/mechanics/triggers/tap_and_counters.rb +111 -0
  246. data/lib/enjine/mechanics/triggers/zone_changes.rb +249 -0
  247. data/lib/enjine/mechanics/types/kindred.rb +30 -0
  248. data/lib/enjine/mechanics/types/land.rb +24 -0
  249. data/lib/enjine/mechanics/types/permanent_types.rb +67 -0
  250. data/lib/enjine/mechanics/types/spell_types.rb +38 -0
  251. data/lib/enjine/mechanics/values.rb +675 -0
  252. data/lib/enjine/mechanics.rb +74 -0
  253. data/lib/enjine/player.rb +30 -0
  254. data/lib/enjine/primitive.rb +61 -0
  255. data/lib/enjine/replacements.rb +191 -0
  256. data/lib/enjine/rng.rb +57 -0
  257. data/lib/enjine/rules_monitor.rb +404 -0
  258. data/lib/enjine/setup.rb +45 -0
  259. data/lib/enjine/snapshot/player_checks.rb +86 -0
  260. data/lib/enjine/snapshot/turn_checks.rb +91 -0
  261. data/lib/enjine/snapshot.rb +307 -0
  262. data/lib/enjine/state.rb +173 -0
  263. data/lib/enjine/steps.rb +117 -0
  264. data/lib/enjine/tokens.rb +25 -0
  265. data/lib/enjine/verify.rb +233 -0
  266. data/lib/enjine/version.rb +7 -0
  267. data/lib/enjine/view.rb +162 -0
  268. data/lib/enjine.rb +46 -0
  269. 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