diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index 8c6cf84283..d817f40807 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -15,7 +15,7 @@ in-scored? installed? operation? program? resource? rezzed? runner? upgrade?]] [game.core.card-defs :refer [card-def]] [game.core.cost-fns :refer [rez-cost install-cost]] - [game.core.damage :refer [damage damage-bonus]] + [game.core.damage :refer [damage]] [game.core.def-helpers :refer [corp-recur defcard do-net-damage offer-jack-out reorder-choice take-credits get-x-fn]] [game.core.drawing :refer [draw draw-up-to]] @@ -38,12 +38,13 @@ trash trash-cards]] [game.core.optional :refer [get-autoresolve set-autoresolve]] [game.core.payment :refer [can-pay? ->c]] + [game.core.prevention :refer [damage-boost preventable? prevent-jack-out]] [game.core.prompts :refer [cancellable clear-wait-prompt show-wait-prompt]] [game.core.props :refer [add-counter add-prop]] [game.core.purging :refer [purge]] [game.core.revealing :refer [reveal]] [game.core.rezzing :refer [derez rez]] - [game.core.runs :refer [end-run force-ice-encounter jack-out-prevent]] + [game.core.runs :refer [end-run force-ice-encounter]] [game.core.say :refer [system-msg]] [game.core.servers :refer [is-remote? target-server zone->name]] [game.core.shuffling :refer [shuffle! shuffle-into-deck @@ -1201,12 +1202,20 @@ {:on-score {:silent (req true) :async true :effect (effect (add-counter eid card :power 2 nil))} - :interactions {:prevent [{:type #{:jack-out} - :req (req (pos? (get-counters card :power)))}]} - :abilities [{:req (req (:run @state)) - :cost [(->c :power 1)] - :msg "prevent the Runner from jacking out" - :effect (effect (jack-out-prevent))}]}) + :prevention [{:prevents :jack-out + :type :ability + :ability {:cost [(->c :power 1)] + :msg "prevent the runner from jacking out for the remainder of this run" + :condition :active + :async true + :req (req (preventable? context)) + :effect (req (wait-for (prevent-jack-out state side) + (register-lingering-effect + state side card + {:type :cannot-jack-out + :value true + :duration :end-of-run}) + (effect-completed state side eid)))}}]}) (defcard "License Acquisition" {:on-score {:interactive (req true) @@ -2236,11 +2245,19 @@ :effect (effect (gain-tags eid 1))}}) (defcard "The Cleaners" - {:events [{:event :pre-damage - :req (req (and (= :meat (:type context)) - (= :corp side))) - :msg "do 1 additional meat damage" - :effect (effect (damage-bonus :meat 1))}]}) + {:prevention [{:prevents :pre-damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :condition :active + :req (req + (and (= :meat (:type context)) + (not= :all (:prevented context)) + (= :corp (:source-player context)) + (not (:unboostable context)))) + :msg "increase the pending meat damage by 1" + :effect (req (damage-boost state side eid 1))}}]}) (defcard "The Future is Now" {:on-score {:interactive (req true) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index ff920fb6a2..e059c33d80 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -7,8 +7,7 @@ [game.core.actions :refer [score]] [game.core.agendas :refer [update-all-advancement-requirements update-all-agenda-points]] - [game.core.bad-publicity :refer [bad-publicity-prevent gain-bad-publicity - lose-bad-publicity]] + [game.core.bad-publicity :refer [gain-bad-publicity lose-bad-publicity]] [game.core.board :refer [all-active-installed all-installed all-installed-runner-type get-remotes installable-servers]] [game.core.card :refer [agenda? asset? can-be-advanced? corp? event? corp-installable-type? @@ -18,7 +17,7 @@ operation? program? resource? rezzed? runner? upgrade?]] [game.core.card-defs :refer [card-def]] [game.core.checkpoint :refer [fake-checkpoint]] - [game.core.damage :refer [damage damage-prevent]] + [game.core.damage :refer [damage]] [game.core.def-helpers :refer [corp-recur corp-rez-toast defcard reorder-choice spend-credits take-credits trash-on-empty get-x-fn with-revealed-hand]] [game.core.drawing :refer [draw first-time-draw-bonus max-draw @@ -27,7 +26,6 @@ [game.core.eid :refer [complete-with-result effect-completed is-basic-advance-action? make-eid get-ability-targets]] [game.core.engine :refer [not-used-once? pay register-events resolve-ability trigger-event-sync]] [game.core.events :refer [first-event? no-event? turn-events event-count]] - [game.core.expose :refer [expose-prevent]] [game.core.flags :refer [lock-zone prevent-current prevent-draw register-turn-flag! release-zone]] @@ -46,6 +44,7 @@ [game.core.play-instants :refer [play-instant]] [game.core.prompts :refer [cancellable]] [game.core.props :refer [add-counter add-icon add-prop remove-icon set-prop]] + [game.core.prevention :refer [damage-name preventable? prevent-bad-publicity prevent-damage prevent-expose]] [game.core.revealing :refer [reveal]] [game.core.rezzing :refer [can-pay-to-rez? derez rez]] [game.core.runs :refer [end-run]] @@ -445,11 +444,15 @@ (damage state side eid :meat 1 {:card card}))))}}) (defcard "Broadcast Square" - {:events [{:event :pre-bad-publicity - :async true - :trace {:base 3 - :successful {:msg "prevents all bad publicity" - :effect (effect (bad-publicity-prevent Integer/MAX_VALUE))}}}]}) + {:prevention [{:prevents :bad-publicity + :type :event + :max-uses 1 + :mandatory true + :ability {:req (req (preventable? context)) + :trace {:base 3 + :successful {:msg "prevent all bad publicity" + :async true + :effect (req (prevent-bad-publicity state side eid :all))}}}}]}) (defcard "C.I. Fund" {:derezzed-events [corp-rez-toast] @@ -1707,16 +1710,13 @@ {:derezzed-events [corp-rez-toast] :events [(assoc ability :event :corp-turn-begins)] :data {:counter {:credit 8}} - :abilities [(set-autoresolve :auto-reshuffle "Marilyn Campaign shuffling itself back into R&D")] - :on-trash {:interactive (req true) - :optional - {:waiting-prompt true - :prompt (msg "Shuffle " (:title card) " into R&D?") - :autoresolve (get-autoresolve :auto-reshuffle) - :player :corp - :yes-ability {:msg "shuffle itself back into R&D" - :effect (effect (move :corp card :deck) - (shuffle! :corp :deck))}}}})) + :prevention [{:prevents :trash + :type :event + :label "Shuffle Marilyn Campaign into R&D" + :max-uses 1 + :ability {:msg "shuffle itself into R&D instead of moving it to Archives" + :req (req (some #(same-card? % card) (map :card (get-in @state [:prevent :trash :remaining])))) + :effect (req (swap! state update-in [:prevent :trash :remaining] (fn [ctx] (mapv #(if (same-card? card (:card %)) (assoc % :destination :deck :shuffle-rd true) %) ctx))))}}]})) (defcard "Mark Yale" {:events [{:event :agenda-counter-spent @@ -2208,16 +2208,18 @@ card nil)))}]})) (defcard "Prāna Condenser" - {:interactions {:prevent [{:type #{:net} - :req (req (= :corp (:side target)))}]} - :abilities [{:label "Prevent 1 net damage to place power counter on Prāna Condenser" - :msg "prevent 1 net damage, place 1 power counter, and gain 3 [Credits]" - :async true - :req (req true) - :effect (req (damage-prevent state :corp :net 1) - (wait-for (add-counter state side card :power 1 nil) - (gain-credits state :corp eid 3)))} - {:action true + {:prevention [{:prevents :damage + :type :event + :max-uses 1 + :ability {:async true + :msg "prevent 1 net damage, place 1 counter on itself, and gain 3 [Credits]" + :req (req (and (= :net (:type context)) + (= :corp (:source-player context)) + (preventable? context))) + :effect (req (wait-for (prevent-damage state side 1) + (wait-for (add-counter state side card :power 1 {:suppress-checkpoint true}) + (gain-credits state side eid 3))))}}] + :abilities [{:action true :msg (msg "deal " (get-counters card :power) " net damage") :label "deal net damage" :cost [(->c :click 2) (->c :trash-can)] @@ -2712,9 +2714,9 @@ :no-ability {:effect (effect (system-msg (str "declines to use " (:title card))))} :yes-ability {:async true :cost [(->c :credit 4)] - :msg "do 3 net damage and give the Runner 1 tag" - :effect (req (wait-for (damage state side :net 3 {:card card}) - (gain-tags state :corp eid 1)))}}}}) + :msg "give the Runner 1 tag and do 3 net damage" + :effect (req (wait-for (gain-tags state :corp 1 {:suppress-checkpoint true}) + (damage state side eid :net 3 {:card card})))}}}}) (defcard "Space Camp" {:flags {:rd-reveal (req true)} @@ -3281,27 +3283,34 @@ (rez state side eid (last (:hosted (get-card state card))) {:cost-bonus -2})))}]}) (defcard "Zaibatsu Loyalty" - {:interactions {:prevent [{:type #{:expose} - :req (req true)}]} - :derezzed-events [{:event :pre-expose + {:prevention [{:prevents :expose + :type :ability + :label "1 [Credit]: Zaibatsu Loyalty" + :ability {:cost [(->c :credit 1)] + :req (req (preventable? context)) + :msg "prevent a card from being exposed" + :async true + :effect (req (prevent-expose state side eid card))}} + {:prevents :expose + :type :ability + :label "[trash]: Zaibatsu Loyalty" + :ability {:cost [(->c :trash-can)] + :req (req (preventable? context)) + :msg "prevent a card from being exposed" + :async true + :effect (req (prevent-expose state side eid card))}}] + :derezzed-events [{:event :expose-interrupt :async true - :effect (req (let [etarget target] + :effect (req (let [ctx context] (continue-ability state side {:optional {:req (req (not (rezzed? card))) :player :corp - :prompt (msg "The Runner is about to expose " (:title etarget) ". Rez Zaibatsu Loyalty?") + :prompt (msg "The Runner is about to expose " (enumerate-str (map #(card-str state % {:visible true}) (:cards ctx))) ". Rez Zaibatsu Loyalty?") :yes-ability {:async true :effect (effect (rez eid card))}}} - card nil)))}] - :abilities [{:msg "prevent 1 card from being exposed" - :cost [(->c :credit 1)] - :effect (effect (expose-prevent 1))} - {:msg "prevent 1 card from being exposed" - :label "Prevent 1 card from being exposed" - :cost [(->c :trash-can)] - :effect (effect (expose-prevent 1))}]}) + card nil)))}]}) (defcard "Zealous Judge" {:rez-req (req tagged) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index f8c3803ad6..3a04632793 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -13,9 +13,10 @@ installed? is-type? operation? program? resource? rezzed? runner? upgrade?]] [game.core.charge :refer [can-charge charge-ability charge-card]] [game.core.checkpoint :refer [fake-checkpoint]] + [game.core.choose-one :refer [choose-one-helper]] [game.core.cost-fns :refer [install-cost play-cost rez-cost]] - [game.core.damage :refer [damage damage-prevent]] - [game.core.def-helpers :refer [breach-access-bonus choose-one-helper defcard offer-jack-out + [game.core.damage :refer [damage]] + [game.core.def-helpers :refer [breach-access-bonus defcard offer-jack-out reorder-choice with-revealed-hand]] [game.core.drawing :refer [draw]] [game.core.effects :refer [register-lingering-effect]] @@ -49,6 +50,7 @@ swap-ice trash trash-cards]] [game.core.payment :refer [can-pay? ->c]] [game.core.play-instants :refer [play-instant]] + [game.core.prevention :refer [damage-name prevent-damage preventable? prevent-up-to-n-tags prevent-up-to-n-damage]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-icon add-prop remove-icon]] [game.core.revealing :refer [reveal reveal-loud]] @@ -63,7 +65,7 @@ zones->sorted-names]] [game.core.set-aside :refer [get-set-aside set-aside]] [game.core.shuffling :refer [shuffle! shuffle-into-deck]] - [game.core.tags :refer [gain-tags lose-tags tag-prevent]] + [game.core.tags :refer [gain-tags lose-tags]] [game.core.threat :refer [threat threat-level]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] @@ -1060,7 +1062,7 @@ :choices {:card #(and (installed? %) (ice? %))} :async true - :effect (req (wait-for (expose state side target) + :effect (req (wait-for (expose state side [target]) (continue-ability state side {:prompt "Choose a server" @@ -1208,8 +1210,8 @@ (= (last (get-zone topmost)) :content) (not (:rezzed %))))} :async true - :effect (req (wait-for (expose state side target) - (if-let [target async-result] + :effect (req (wait-for (expose state side [target]) + (if-let [target (when async-result (first (:cards async-result)))] (if (or (asset? target) (upgrade? target)) (do (system-msg state :runner (str "uses " (:title card) " to trash " (:title target))) @@ -1498,7 +1500,7 @@ (= (last (get-zone topmost)) :content) (not (rezzed? %))))} :async true - :effect (req (wait-for (expose state side target) + :effect (req (wait-for (expose state side [target]) (continue-ability state :runner (when (and async-result ;; expose was successful @@ -1930,7 +1932,7 @@ {:choices {:card #(and (installed? %) (not (rezzed? %)))} :async true - :effect (effect (expose eid target))} + :effect (effect (expose eid [target]))} {:msg "gain 2 [Credits]" :async true :effect (effect (gain-credits eid 2))}) @@ -2379,22 +2381,29 @@ {:req (req (some #{:hq} (:successful-run runner-reg))) :player :corp :prompt "Take 2 bad publicity?" + :waiting-prompt true :yes-ability {:player :corp :msg "takes 2 bad publicity" :effect (effect (gain-bad-publicity :corp 2))} :no-ability {:player :runner :msg "is immune to damage until the beginning of the Runner's next turn" - :effect (effect - (register-events - card - [{:event :pre-damage - :duration :until-runner-turn-begins - :effect (effect (damage-prevent :net Integer/MAX_VALUE) - (damage-prevent :meat Integer/MAX_VALUE) - (damage-prevent :brain Integer/MAX_VALUE))} - {:event :runner-turn-begins - :duration :until-runner-turn-begins - :effect (effect (unregister-floating-events :until-runner-turn-begins))}]))}}}}) + :effect (req + (register-lingering-effect + state side card + {:type :prevention + :duration :until-runner-turn-begins + :req (req (= :runner side)) + :value {:prevents :damage + :type :floating + :max-uses 1 + :card card + :mandatory true + :ability {:async true + :card card + :condition :floating + :req (req (preventable? context)) + :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") + :effect (req (prevent-damage state side eid :damage :all))}}}))}}}}) (defcard "Levy AR Lab Access" {:on-play @@ -2686,26 +2695,21 @@ (draw state :runner eid 4)))}}) (defcard "On the Lam" - {:on-play {:prompt "Choose a resource to host On the Lam on" + {:prevention [{:prevents :tag + :type :ability + :prompt "Trash On the Lam to avoid up to 3 tags?" + :ability (assoc (prevent-up-to-n-tags 3) :cost [(->c :trash-can)])} + {:prevents :damage + :type :ability + :prompt "Trash On the Lam to prevent up to 3 damage?" + :ability (assoc (prevent-up-to-n-damage 3 #{:net :meat :core :brain}) :cost [(->c :trash-can)])}] + :on-play {:prompt "Choose a resource to host On the Lam on" :choices {:card #(and (resource? %) (installed? %))} :change-in-game-state (req (some resource? (all-active-installed state :runner))) :async true :effect (req (system-msg state side (str "hosts On the Lam on " (:title target))) - (install-as-condition-counter state side eid card target))} - :interactions {:prevent [{:type #{:net :brain :meat :tag} - :req (req true)}]} - :abilities [{:label "Avoid 3 tags" - :msg "avoid up to 3 tags" - :async true - :cost [(->c :trash-can)] - :effect (effect (tag-prevent :runner eid 3))} - {:label "Prevent up to 3 damage" - :msg "prevent up to 3 damage" - :cost [(->c :trash-can)] - :effect (effect (damage-prevent :net 3) - (damage-prevent :meat 3) - (damage-prevent :brain 3))}]}) + (install-as-condition-counter state side eid card target))}}) (defcard "Out of the Ashes" (let [ashes-run {:prompt "Choose a server" @@ -3503,10 +3507,7 @@ :async true :change-in-game-state (req (some (complement faceup?) (all-installed state :corp))) :effect (req (if (pos? (count targets)) - (wait-for (expose state side target) - (if (= 2 (count targets)) - (expose state side eid (second targets)) - (effect-completed state side eid))) + (expose state side eid targets) (effect-completed state side eid)))}}) (defcard "Scavenge" @@ -3704,7 +3705,7 @@ (not (ice? %)) (corp? %))} :async true - :effect (req (wait-for (expose state side target) + :effect (req (wait-for (expose state side [target]) (continue-ability state side {:prompt "Choose a server" @@ -3916,9 +3917,31 @@ (defcard "The Noble Path" {:makes-run true + :static-abilities [{:type :cannot-pay-net + :req (req run) + :value true} + {:type :cannot-pay-brain + :req (req run) + :value true} + {:type :cannot-pay-meat + :req (req run) + :value true}] + :prevention [{:prevents :damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req + (and + run + (same-card? card (get-in @state [:runner :play-area 0])) + (preventable? context))) + :condition :active + :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :effect (req (prevent-damage state side eid :all))}}] :on-play {:async true :change-in-game-state (req (or (seq (:hand runner)) - (seq runnable-servers))) + (seq runnable-servers))) :effect (req (wait-for (trash-cards state side (:hand runner) {:cause-card card}) (continue-ability @@ -3929,12 +3952,7 @@ :msg (msg "trash [their] grip and make a run on " target ", preventing all damage") :effect (effect (make-run eid target card))} - card nil)))} - :events [{:event :pre-damage - :duration :end-of-run - :effect (effect (damage-prevent :net Integer/MAX_VALUE) - (damage-prevent :meat Integer/MAX_VALUE) - (damage-prevent :brain Integer/MAX_VALUE))}]}) + card nil)))}}) (defcard "The Price" {:on-play {:async true diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 10312b8ad6..a343cb8799 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -10,14 +10,15 @@ in-hand? in-scored? installed? is-type? program? resource? rezzed? runner? virus-program? faceup?]] [game.core.card-defs :refer [card-def]] + [game.core.choose-one :refer [choose-one-helper]] [game.core.cost-fns :refer [install-cost rez-additional-cost-bonus rez-cost trash-cost]] - [game.core.damage :refer [chosen-damage damage damage-prevent + [game.core.damage :refer [chosen-damage damage enable-runner-damage-choice runner-can-choose-damage?]] [game.core.def-helpers :refer [breach-access-bonus defcard offer-jack-out reorder-choice spend-credits take-credits trash-on-empty get-x-fn]] [game.core.drawing :refer [draw]] - [game.core.effects :refer [register-lingering-effect - unregister-effects-for-card unregister-lingering-effects]] + [game.core.effects :refer [any-effects register-lingering-effect + unregister-effect-by-uuid unregister-effects-for-card unregister-lingering-effects]] [game.core.eid :refer [effect-completed make-eid make-result]] [game.core.engine :refer [can-trigger? register-events register-once register-suppress resolve-ability trigger-event @@ -42,17 +43,18 @@ [game.core.optional :refer [get-autoresolve never? set-autoresolve]] [game.core.payment :refer [build-cost-string can-pay? cost-value ->c]] [game.core.play-instants :refer [play-instant]] + [game.core.prevention :refer [damage-name damage-type preventable? prevent-damage prevent-encounter prevent-end-run prevent-tag prevent-up-to-n-damage]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal]] - [game.core.rezzing :refer [derez rez]] - [game.core.runs :refer [bypass-ice end-run end-run-prevent + [game.core.rezzing :refer [can-pay-to-rez? derez rez]] + [game.core.runs :refer [bypass-ice end-run get-current-encounter jack-out make-run successful-run-replace-breach total-cards-accessed]] [game.core.say :refer [system-msg]] [game.core.servers :refer [target-server is-central?]] [game.core.shuffling :refer [shuffle!]] - [game.core.tags :refer [gain-tags lose-tags tag-prevent]] + [game.core.tags :refer [gain-tags lose-tags]] [game.core.threat :refer [threat-level]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] @@ -97,30 +99,23 @@ (defcard "AirbladeX (JSRF Ed.)" {:data {:counter {:power 3}} - :interactions {:prevent [{:type #{:net} - :req (req (and run (pos? (get-counters card :power))))}]} - :events [(trash-on-empty :power) - {:event :prevent-encounter-ability - :interactive (req true) - :req (req (and (not (get-in @state [:run :prevent-encounter-ability])) - (pos? (get-counters card :power)))) - :async true - :effect (req - (if (get-in @state [:run :prevent-encounter-ability]) - (effect-completed state side eid) - (continue-ability - state side - {:optional {:prompt (msg "Prevent a \"when encountered\" ability on " (:title current-ice) (when (:ability-name target) - (str " (" (:ability-name target) ")"))) - :yes-ability {:cost [(->c :power 1)] - :msg (msg "prevent the encounter ability on " (:title current-ice) (when (:ability-name target) - (str " (" (:ability-name target) ")"))) - :effect (req (swap! state assoc-in [:run :prevent-encounter-ability] true))}}} - card targets)))}] - :abilities [{:cost [(->c :power 1)] - :req (req run) - :msg "prevent 1 net damage" - :effect (effect (damage-prevent :net 1))}]}) + :prevention [{:prevents :damage + :type :ability + :ability {:async true + :cost [(->c :power 1)] + :msg "prevent 1 net damage" + :req (req (and run + (= :net (:type context)) + (preventable? context))) + :effect (req (prevent-damage state side eid 1))}} + {:prevents :encounter + :type :ability + :ability {:async true + :cost [(->c :power 1)] + :req (req (pos? (:remaining context))) + :msg (msg "prevent the encounter ability on " (:title current-ice)) + :effect (req (prevent-encounter state side eid))}}] + :events [(trash-on-empty :power)]}) (defcard "Akamatsu Mem Chip" {:static-abilities [(mu+ 1)]}) @@ -240,15 +235,17 @@ :in-play [:click-per-turn 1]}) (defcard "Blackguard" - {:static-abilities [(mu+ 2)] - :events [{:event :expose - :msg (msg "attempt to force the rez of " (:title target)) + (letfn [(force-a-rez [c] + {:msg (msg "attempt to force the rez of " (:title c)) :async true - :effect (req (let [c target - cname (:title c) - cost (rez-cost state side target) - additional-costs (rez-additional-cost-bonus state side target)] - (if (seq additional-costs) + :effect (req (let [cname (:title c) + cost (rez-cost state side c) + additional-costs (rez-additional-cost-bonus state side c) + payable? (can-pay-to-rez? state :corp eid c)] + (cond + (not payable?) + (effect-completed state side eid) + (seq additional-costs) (continue-ability state side {:optional @@ -262,7 +259,27 @@ :no-ability {:msg (msg "declines to pay additional costs" " and is not forced to rez " cname)}}} card nil) - (rez state :corp eid target))))}]}) + :else (rez state :corp eid c))))}) + (choose-a-card [cards] + (if (= 1 (count cards)) + (force-a-rez (first cards)) + {:prompt "Force the Corp to rez which card?" + :req (req (seq cards)) + :choices (req cards) + :async true + :effect (req (wait-for (resolve-ability + state side + (force-a-rez target) + card nil) + (continue-ability + state side + (choose-a-card (filterv #(not (same-card? % target)) cards)) + card nil)))}))] + {:static-abilities [(mu+ 2)] + :events [{:event :expose + :req (req (seq (:cards context))) + :async true + :effect (req (continue-ability state side (choose-a-card (:cards context)) card nil))}]})) (defcard "BMI Buffer" (let [grip-program-trash? @@ -404,8 +421,9 @@ targets)) triggered-ability {:once-per-instance true - :req (req (and (grip-or-stack-trash? targets) - (first-trash? state grip-or-stack-trash?))) + :req (req + (and (grip-or-stack-trash? targets) + (first-trash? state grip-or-stack-trash?))) :prompt "Choose 1 trashed card to add to the bottom of the stack" :choices (req (conj (sort (keep #(->> (:moved-card %) :title) targets)) "No action")) :async true @@ -745,8 +763,17 @@ (make-run eid target (get-card state card)))}}}]}) (defcard "Dorm Computer" - {:flags {:forced-to-avoid-tag true} - :data {:counter {:power 4}} + {:data {:counter {:power 4}} + :static-abilities [{:type :forced-to-avoid-tag + :value true + ;; TODO - replace this with a 'this-card-is-run-source- fn, it's in playtest + ;; note that this needs to account for the card being trashed mid-run? oh no + :req (req (= (get-in run [:source-card :title]) (:title card)))}] + :events [{:event :tag-interrupt + :req (req (= (get-in run [:source-card :title]) (:title card))) + :async true + :msg "avoid all tags" + :effect (req (prevent-tag state :runner eid :all))}] :abilities [{:action true :cost [(->c :click 1) (->c :power 1)] :req (req (not run)) @@ -755,14 +782,7 @@ :msg "make a run and avoid all tags for the remainder of the run" :makes-run true :async true - :effect (effect (register-events - card - [{:event :pre-tag - :duration :end-of-run - :async true - :msg "avoid all tags during the run" - :effect (effect (tag-prevent :runner eid Integer/MAX_VALUE))}]) - (make-run eid target card))}]}) + :effect (effect (make-run eid target card))}]}) (defcard "Dyson Fractal Generator" {:recurring 1 @@ -830,15 +850,20 @@ :abilities [(break-sub [(->c :power 2)] 2 "All")]})) (defcard "Feedback Filter" - {:interactions {:prevent [{:type #{:net :brain} - :req (req true)}]} - :abilities [{:cost [(->c :credit 3)] - :msg "prevent 1 net damage" - :effect (effect (damage-prevent :net 1))} - {:label "Prevent up to 2 core damage" - :msg "prevent up to 2 core damage" - :cost [(->c :trash-can)] - :effect (effect (damage-prevent :brain 2))}]}) + {:prevention [{:prevents :damage + :type :ability + :label "Feedback Filter (Net)" + :ability {:async true + :cost [(->c :credit 3)] + :msg "prevent 1 net damage" + :req (req (and (= :net (:type context)) + (preventable? context))) + :effect (req (prevent-damage state side eid 1))}} + {:prevents :damage + :type :ability + :label "Feedback Filter (Core)" + :ability (assoc (prevent-up-to-n-damage 2 #{:brain :core}) + :cost [(->c :trash-can)])}]}) (defcard "Flame-out" (let [register-flame-effect @@ -910,19 +935,26 @@ :effect (effect (lose-tags eid 1))}]}) (defcard "Forger" - {:interactions {:prevent [{:type #{:tag} - :req (req true)}]} - :static-abilities [(link+ 1)] - :abilities [{:msg "avoid 1 tag" - :label "Avoid 1 tag" - :async true - :cost [(->c :trash-can)] - :effect (effect (tag-prevent :runner eid 1))} - {:msg "remove 1 tag" - :label "Remove 1 tag" - :cost [(->c :trash-can)] - :async true - :effect (effect (lose-tags eid 1))}]}) + (let [avoid-ab {:msg "avoid 1 tag" + :label "Avoid 1 tag" + :async true + :cost [(->c :trash-can)] + :effect (effect (prevent-tag :runner eid 1))}] + {:events [(choose-one-helper + {:event :tag-interrupt + :req (req (and (pos? (get-in @state [:prevent :tag :remaining])) + (not (any-effects state side :prevent-paid-ability true? card [avoid-ab 0])))) + :optional true + :interactive (req true)} + [{:option "Avoid 1 tag" + :cost [(->c :trash-can)] + :ability avoid-ab}])] + :static-abilities [(link+ 1)] + :abilities [{:msg "remove 1 tag" + :label "Remove 1 tag" + :cost [(->c :trash-can)] + :async true + :effect (effect (lose-tags eid 1))}]})) (defcard "Friday Chip" (let [ability {:msg (msg "move 1 virus counter to " (:title target)) @@ -1107,9 +1139,8 @@ (ice? current-ice) (not (rezzed? current-ice)))) :label "expose approached ice" - :msg "expose the approached ice" :async true - :effect (req (wait-for (expose state side (make-eid state eid) current-ice) + :effect (req (wait-for (expose state side (make-eid state eid) [current-ice]) (continue-ability state side (offer-jack-out) card nil)))}]}) (defcard "Grimoire" @@ -1122,14 +1153,14 @@ (defcard "Heartbeat" {:static-abilities [(mu+ 1)] - :interactions {:prevent [{:type #{:net :brain :meat} - :req (req true)}]} - :abilities [{:label "Prevent 1 damage" - :msg "prevent 1 damage" - :cost [(->c :trash-installed 1)] - :effect (effect (damage-prevent :brain 1) - (damage-prevent :meat 1) - (damage-prevent :net 1))}]}) + :prevention [{:prevents :damage + :type :ability + :label "Heartbeat" + :ability {:async true + :cost [(->c :trash-installed 1)] + :msg (msg "prevent 1 " (damage-name state) " damage") + :req (req (preventable? context)) + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "Hermes" (let [ab {:interactive (req true) @@ -1261,8 +1292,8 @@ :cost [(->c :click 1) (->c :credit 1)] :req (req (some #{:hq} (:successful-run runner-reg))) :choices {:card installed?} - :effect (effect (expose eid target)) - :msg "expose 1 card"}]}) + :label "Expose a card" + :effect (effect (expose eid [target] {:card card}))}]}) (defcard "LilyPAD" {:events [{:event :runner-install @@ -1310,13 +1341,15 @@ (move target :hand))}]}) (defcard "Lucky Charm" - {:interactions {:prevent [{:type #{:end-run} - :req (req (and (some #{:hq} (:successful-run runner-reg)) - (corp? (:card-cause target))))}]} - :abilities [{:msg "prevent the run from ending" - :req (req (some #{:hq} (:successful-run runner-reg))) - :cost [(->c :remove-from-game)] - :effect (effect (end-run-prevent))}]}) + {:prevention [{:prevents :end-run + :type :ability + :ability {:req (req (and (some #{:hq} (:successful-run runner-reg)) + (pos? (:remaining context)) + (= :corp (get-in @state [:prevent :end-run :source-player])))) + :cost [(->c :remove-from-game)] + :async true + :msg "prevent the run from ending" + :effect (req (prevent-end-run state side eid))}}]}) (defcard "Mâché" (letfn [(pred [{:keys [card accessed]}] @@ -1464,15 +1497,16 @@ :msg-keys {:install-source card :display-origin true}}) (continue-ability state side (when (< n 3) (mh (inc n))) card nil)))})] - {:interactions {:prevent [{:type #{:net :brain} - :req (req true)}]} - :static-abilities [(mu+ 3)] + {:static-abilities [(mu+ 3)] :on-install {:async true :effect (effect (continue-ability (mhelper 1) card nil))} - :abilities [{:msg "prevent 1 brain or net damage" - :cost [(->c :trash-program-from-hand 1)] - :effect (effect (damage-prevent :brain 1) - (damage-prevent :net 1))}]})) + :prevention [{:prevents :damage + :type :ability + :ability {:async true + :cost [(->c :trash-program-from-hand 1)] + :msg (msg "prevent 1 " (damage-name state) " damage") + :req (req (and (not (= :meat (:type context))) + (preventable? context)))}}]})) (defcard "Mu Safecracker" {:implementation "Stealth credit restriction not enforced" @@ -1500,12 +1534,17 @@ card [(breach-access-bonus :rd 1 {:duration :end-of-run})]))}}}]}) (defcard "Muresh Bodysuit" - {:events [{:event :pre-damage - :once-key :muresh-bodysuit - :req (req (and (= (:type context) :meat) - (first-event? state side :pre-damage #(= :meat (:type (first %)))))) - :msg "prevent the first meat damage this turn" - :effect (effect (damage-prevent :meat 1))}]}) + {:prevention [{:prevents :damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req (and (= :meat (:type context)) + (first-event? state side :pre-damage-flag + #(= :meat (:type (first %)))) + (preventable? context))) + :msg "reduce the pending meat damage by 1" + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "Net-Ready Eyes" {:on-install {:async true @@ -1745,12 +1784,15 @@ (defcard "Plascrete Carapace" {:data {:counter {:power 4}} - :interactions {:prevent [{:type #{:meat} - :req (req true)}]} - :events [(trash-on-empty :power)] - :abilities [{:cost [(->c :power 1)] - :msg "prevent 1 meat damage" - :effect (req (damage-prevent state side :meat 1))}]}) + :prevention [{:prevents :damage + :type :ability + :ability {:async true + :cost [(->c :power 1)] + :msg "prevent 1 meat damage" + :req (req (and (preventable? context) + (= :meat (:type context)))) + :effect (req (prevent-damage state side eid 1))}}] + :events [(trash-on-empty :power)]}) (defcard "Poison Vial" (auto-icebreaker @@ -1850,24 +1892,29 @@ (assoc e :event :corp-trash)])}) (defcard "Qianju PT" - {:flags {:runner-phase-12 (req true) - :forced-to-avoid-tag true} + {:flags {:runner-phase-12 (req true)} :abilities [{:label "Lose [Click], avoid 1 tag (start of turn)" :once :per-turn :req (req (:runner-phase-12 @state)) - :effect (effect (update! (assoc card :qianju-active true))) - :msg (msg "lose [Click] and avoid the first tag received until [their] next turn")}] - :events [{:event :corp-turn-ends - :effect (effect (update! (dissoc card :qianju-active)))} - {:event :runner-turn-begins - :req (req (:qianju-active card)) - :effect (effect (lose-clicks 1))} - {:event :pre-tag - :async true - :req (req (:qianju-active card)) - :msg "avoid the first tag received" - :effect (effect (update! (dissoc card :qianju-active)) - (tag-prevent :runner eid 1))}]}) + :cost [(->c :lose-click 1)] + :msg "avoid the first tag received until [their] next turn" + ;; TODO - I should do this to fix klevetnik, lmao + :effect (req (let [current-turn (:turn @state) + lingering (register-lingering-effect + state side card + {:type :forced-to-avoid-tag + :duration :until-next-runner-turn-begins + :value true})] + (register-events + state side card + [{:event :tag-interrupt + :unregister-once-resolved true + :duration :until-next-runner-turn-begins + :async true + :msg "avoid 1 tag" + :effect (req + (unregister-effect-by-uuid state side lingering) + (prevent-tag state :runner eid 1))}])))}]}) (defcard "R&D Interface" {:events [(breach-access-bonus :rd 1)]}) @@ -1888,42 +1935,36 @@ (effect-completed state side eid)))}}}}) (defcard "Ramujan-reliant 550 BMI" - {:interactions {:prevent [{:type #{:net :brain} - :req (req true)}]} - :abilities [{:async true - :label "prevent net or core damage" - :trash-icon true - :req (req (not-empty (:deck runner))) - :effect (req (let [n (count (filter #(= (:title %) (:title card)) (all-active-installed state :runner)))] - (continue-ability - state side - {:async true - :prompt "How much damage do you want to prevent?" - :choices {:number (req (min n (count (:deck runner))))} - :msg (msg "trash " (enumerate-str (map :title (take target (:deck runner)))) - " from the stack and prevent " target " damage") - :cost [(->c :trash-can)] - :effect (effect (damage-prevent :net target) - (damage-prevent :brain target) - (mill :runner eid :runner target))} - card nil)))}]}) + (letfn [(max-trash [state] (inc (count (filter #(= (:title %) "Ramujan-reliant 550 BMI") (all-installed state :runner)))))] + {:prevention [{:prevents :damage + :type :ability + :ability {:async true + :cost [(->c :trash-can)] + :msg (msg "prevent up to " (max-trash state) " damage") + :req (:req (prevent-up-to-n-damage 1 #{:net :core :brain})) + :effect (req (let [prevented (:prevented context)] + (wait-for (resolve-ability + state side + (prevent-up-to-n-damage (max-trash state) #{:net :core :brain}) + card targets) + (let [prevented-this-instance (- (get-in @state [:prevent :damage :prevented]) prevented)] + (system-msg state side (str "uses " (:title card) " to trash the top " prevented-this-instance " cards of the stack")) + (mill state :runner eid :runner prevented-this-instance)))))}}]})) (defcard "Recon Drone" - ; eventmap uses reverse so we get the most recent event of each kind into map - (letfn [(eventmap [s] - (into {} (reverse (get s :turn-events))))] - {:interactions {:prevent [{:type #{:net :brain :meat} - :req (req (and (:access @state) - (= (:cid (:card (first (:pre-damage (eventmap @state))))) - (:cid (first (:pre-access-card (eventmap @state)))))))}]} - :abilities [{:cost [(->c :x-credits) (->c :trash-can)] - :label "prevent damage" - :req (req (and (:access @state) - (= (:cid (:card (first (:pre-damage (eventmap @state))))) - (:cid (first (:pre-access-card (eventmap @state))))))) - :msg (msg "prevent " (cost-value eid :x-credits) " damage") - :effect (effect (damage-prevent (:type (first (:pre-damage (eventmap @state)))) - (cost-value eid :x-credits)))}]})) + {:prevention [{:prevents :damage + :type :ability + :ability {:async true + :trash-icon true + :req (req (and (preventable? context) + (same-card? (:source-card context) (:access @state)))) + :effect (req (continue-ability + state side + {:cost [(->c :trash-can) (->c :x-credits 0 {:maximum (:remaining context)})] + :msg (msg "prevent " (cost-value eid :x-credits) " " (damage-type state) " damage") + :async true + :effect (req (prevent-damage state side eid (cost-value eid :x-credits)))} + card nil))}}]}) (defcard "Record Reconstructor" {:events [(successful-run-replace-breach @@ -2573,12 +2614,22 @@ :effect (effect (move (last (:deck runner)) :hand))}]}) (defcard "Zamba" - {:implementation "Credit gain is automatic" + {:special {:auto-gain-credits :always} + :implementation "Credit gain is automatic" :static-abilities [(mu+ 2)] + :abilities [(set-autoresolve :auto-gain-credits "Zamba gaining credits on expose")] :events [{:event :expose + :interactive (get-autoresolve :auto-gain-credits (complement never?)) + :silent (get-autoresolve :auto-gain-credits never?) :async true - :effect (effect (gain-credits :runner eid 1)) - :msg "gain 1 [Credits]"}]}) + :optional + {:waiting-prompt true + :prompt (msg "Gain " (count (:cards context)) " [Credits]?") + :player :runner + :autoresolve (get-autoresolve :auto-gain-credits) + :yes-ability {:msg (msg "gain " (count (:cards context)) " [Credits]") + :async true + :effect (effect (gain-credits eid (count (:cards context))))}}}]}) (defcard "Zenit Chip JZ-2MJ" {:on-install {:async true diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index 62d40188af..9cbbc6105e 100644 --- a/src/clj/game/cards/ice.clj +++ b/src/clj/game/cards/ice.clj @@ -19,14 +19,14 @@ do-brain-damage do-net-damage offer-jack-out reorder-choice get-x-fn with-revealed-hand]] [game.core.drawing :refer [draw maybe-draw draw-up-to]] - [game.core.effects :refer [get-effects is-disabled? is-disabled-reg? register-lingering-effect unregister-effects-for-card unregister-static-abilities update-disabled-cards]] + [game.core.effects :refer [any-effects get-effects is-disabled? is-disabled-reg? register-lingering-effect unregister-effects-for-card unregister-effect-by-uuid unregister-static-abilities update-disabled-cards]] [game.core.eid :refer [complete-with-result effect-completed make-eid]] [game.core.engine :refer [gather-events pay register-default-events register-events resolve-ability trigger-event trigger-event-simult unregister-events ]] [game.core.events :refer [first-event? run-events]] [game.core.finding :refer [find-cid]] - [game.core.flags :refer [can-rez? card-flag? prevent-draw prevent-jack-out + [game.core.flags :refer [can-rez? card-flag? prevent-draw register-run-flag! register-turn-flag! run-flag? zone-locked?]] [game.core.gaining :refer [gain-credits lose-clicks lose-credits]] [game.core.hand-size :refer [hand-size]] @@ -125,8 +125,7 @@ ;;; Checks if the runner has active events that would force them to avoid/prevent a tag (defn forced-to-avoid-tags? [state side] - (let [cards (map :card (gather-events state side :pre-tag nil))] - (pos? (count (filter #(card-flag? % :forced-to-avoid-tag true) cards))))) + (any-effects state side :forced-to-avoid-tag)) ;;; Break abilities on ice should only occur when encountering that ice (defn currently-encountering-card @@ -1857,7 +1856,7 @@ (decapitalize target))) :player :runner :prompt "Choose one" - :choices (req [(when-not (forced-to-avoid-tags? state side) + :choices (req [(when-not (forced-to-avoid-tags? state :runner) "Take 1 tag") "End the run"]) :waiting-prompt true @@ -2367,22 +2366,17 @@ :value true})))}]))} {:msg "prevent the Runner from jacking out until after the next piece of ice" :effect - (req (prevent-jack-out state side) - (register-events - state side card - [{:event :encounter-ice - :duration :end-of-run - :unregister-once-resolved true - :effect - (req (let [encountered-ice (:ice context)] - (register-events - state side card - [{:event :end-of-encounter - :duration :end-of-encounter - :unregister-once-resolved true - :msg (msg "can jack out again after encountering " (:title encountered-ice)) - :effect (req (swap! state update :run dissoc :cannot-jack-out)) - :req (req (same-card? encountered-ice (:ice context)))}])))}]))}]}) + (req (let [lingering (register-lingering-effect + state side card + {:type :cannot-jack-out + :value true + :duration :end-of-run})] + (register-events + state side card + [{:event :encounter-ice + :duration :end-of-run + :unregister-once-resolved true + :effect (req (unregister-effect-by-uuid state side lingering))}])))}]}) (defcard "Information Overload" {:on-encounter (tag-trace 1) @@ -2566,23 +2560,7 @@ (encounter-ends state side eid)))}}}]}) (defcard "Klevetnik" - ;; TODO - make this use a floating effect to disable cards - (let [re-enable-target - (fn [t] {:event :corp-turn-ends - :unregister-once-resolved true - :msg (msg "unblank " (:title t)) - :async true - :effect (req (if (:disabled (get-card state t)) - (do (enable-card state :runner (get-card state t)) - (if-let [reactivate-effect (:reactivate (card-def t))] - (resolve-ability state :runner eid reactivate-effect (get-card state t) nil) - (effect-completed state side eid))) - (effect-completed state side eid)))}) - register-corp-next-turn-end - (fn [t] {:event :corp-turn-ends ;; delayed registration to make it wait the Corp next turn end - :unregister-once-resolved true - :effect (effect (register-events card [(re-enable-target t)]))}) - on-rez-ability {:prompt "Choose an installed resource" + (let [on-rez-ability {:prompt "Choose an installed resource" :waiting-prompt true :choices {:card #(and (installed? %) (resource? %))} @@ -2590,16 +2568,18 @@ :msg (msg "let the Runner gain 2 [Credits] to" " blank the text box of " (:title target) " until the Corp next turn ends") - :effect - (req (let [t target] - (wait-for (gain-credits state :runner 2) - (disable-card state :runner t) - (register-events - state side card - [(if (= (:active-player @state) :runner) - (re-enable-target t) - (register-corp-next-turn-end t))]) - (effect-completed state side eid))))}] + :effect (req (let [t target + duration (if (= :corp (:active-player @state)) + :until-next-corp-turn-ends + :until-corp-turn-ends)] + (wait-for (gain-credits state :runner 2) + (register-lingering-effect + state side card + {:type :disable-card + :req (req (same-card? t target)) + :duration duration + :value true}) + (effect-completed state side eid))))}] {:subroutines [end-the-run] :on-rez {:optional {:prompt "Let the Runner gain 2 [Credits]?" @@ -3948,22 +3928,25 @@ (defcard "Susanoo-no-Mikoto" {:subroutines [{:async true - :req (req (not= (:server run) [:discard])) :msg "make the Runner continue the run on Archives" - :effect (req (if run - (do (prevent-jack-out state side) - (register-events - state side card - [{:event :encounter-ice - :duration :end-of-run - :unregister-once-resolved true - :effect (req (swap! state update :run dissoc :cannot-jack-out))}]) - (if (and (= 1 (count (:encounters @state))) - (not= :success (:phase run))) - (do (redirect-run state side "Archives" :approach-ice) - (encounter-ends state side eid)) - (effect-completed state side eid))) - (effect-completed state side eid)))}]}) + :change-in-game-state (req (and run + (not= (:server run) [:discard]))) + :effect (req (let [lingering (register-lingering-effect + state side card + {:type :cannot-jack-out + :value true + :duration :end-of-run})] + (register-events + state side card + [{:event :encounter-ice + :duration :end-of-run + :unregister-once-resolved true + :effect (req (unregister-effect-by-uuid state side lingering))}]) + (if (and (= 1 (count (:encounters @state))) + (not= :success (:phase run))) + (do (redirect-run state side "Archives" :approach-ice) + (encounter-ends state side eid)) + (effect-completed state side eid))))}]}) (defcard "Swarm" (let [sub {:player :runner @@ -4509,7 +4492,11 @@ {:subroutines [{:label "The Runner cannot jack out for the remainder of this run" :msg "prevent the Runner from jacking out and trash itself" :async true - :effect (req (prevent-jack-out state side) + :effect (req (register-lingering-effect + state side card + {:type :cannot-jack-out + :value true + :duration :end-of-run}) (wait-for (trash state :corp (make-eid state eid) card {:cause :subroutine}) (encounter-ends state side eid)))}]}) diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index eac326e05f..67172b0066 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -11,11 +11,12 @@ has-any-subtype? ice? in-discard? in-deck? in-hand? in-play-area? in-rfg? installed? is-type? operation? program? resource? rezzed? runner? upgrade?]] [game.core.charge :refer [charge-ability]] + [game.core.choose-one :refer [choose-one-helper]] [game.core.cost-fns :refer [install-cost play-cost rez-additional-cost-bonus rez-cost]] [game.core.damage :refer [chosen-damage corp-can-choose-damage? damage enable-corp-damage-choice]] - [game.core.def-helpers :refer [choose-one-helper corp-recur defcard offer-jack-out with-revealed-hand]] + [game.core.def-helpers :refer [corp-recur defcard offer-jack-out with-revealed-hand]] [game.core.drawing :refer [draw]] [game.core.effects :refer [register-lingering-effect is-disabled?]] [game.core.eid :refer [effect-completed get-ability-targets is-basic-advance-action? make-eid]] @@ -38,6 +39,7 @@ [game.core.moving :refer [mill move swap-ice trash trash-cards]] [game.core.optional :refer [get-autoresolve never? set-autoresolve]] [game.core.payment :refer [build-cost-label can-pay? cost->string merge-costs ->c]] + [game.core.prevention :refer [prevent-tag]] [game.core.pick-counters :refer [pick-virus-counters-to-spend]] [game.core.play-instants :refer [play-instant]] [game.core.prompts :refer [cancellable clear-wait-prompt]] @@ -51,7 +53,7 @@ [game.core.servers :refer [central->name is-central? is-remote? name-zone target-server zone->name]] [game.core.shuffling :refer [shuffle! shuffle-into-deck]] - [game.core.tags :refer [gain-tags lose-tags tag-prevent]] + [game.core.tags :refer [gain-tags lose-tags]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] [game.core.update :refer [update!]] @@ -109,7 +111,7 @@ :effect (req (if (not (can-pay? state :corp eid card nil (->c :credit 1))) (do (toast state :corp "Cannot afford to pay 1 [Credit] to block card exposure" "info") - (expose state :runner eid (:card context))) + (expose state :runner eid [(:card context)])) (continue-ability state side {:optional @@ -118,7 +120,7 @@ :player :corp :no-ability {:async true - :effect (effect (expose :runner eid (:card context)))} + :effect (effect (expose :runner eid [(:card context)]))} :yes-ability {:async true :effect @@ -1031,12 +1033,14 @@ card nil))}]}) (defcard "Jesminder Sareen: Girl Behind the Curtain" - {:flags {:forced-to-avoid-tag true} - :events [{:event :pre-tag + {:static-abilities [{:type :forced-to-avoid-tag + :req (req (and run (zero? (run-event-count state side :tag-interrupt)))) + :value true}] + :events [{:event :tag-interrupt :async true - :req (req (and run (<= (run-event-count state side :pre-tag) 1))) - :msg "avoid the first tag during this run" - :effect (effect (tag-prevent :runner eid 1))}]}) + :req (req (and run (<= (run-event-count state side :tag-interrupt) 1))) + :msg "avoid 1 tag" + :effect (effect (prevent-tag :runner eid 1))}]}) (defcard "Jinteki Biotech: Life Imagined" {:events [{:event :pre-first-turn @@ -1253,8 +1257,8 @@ (fn [targets] (let [context (first targets)] (is-central? (:server context))))))) - :effect (req (wait-for (gain-tags state :runner 1) - (draw state :runner eid 2)))}]}) + :effect (req (wait-for (draw state :runner 2 {:suppress-checkpoint true}) + (gain-tags state :runner eid 1)))}]}) (defcard "Los: Data Hijacker" {:events [{:event :rez @@ -1912,8 +1916,7 @@ (first-successful-run-on-server? state :hq))) :choices {:card #(and (installed? %) (not (rezzed? %)))} - :msg "expose 1 card" - :effect (effect (expose eid target))}]}) + :effect (effect (expose eid [target]))}]}) (defcard "Skorpios Defense Systems: Persuasive Power" (let [set-resolution-mode diff --git a/src/clj/game/cards/operations.clj b/src/clj/game/cards/operations.clj index 9f37a15269..c1b15dd454 100644 --- a/src/clj/game/cards/operations.clj +++ b/src/clj/game/cards/operations.clj @@ -13,17 +13,18 @@ in-discard? in-hand? installed? is-type? operation? program? resource? rezzed? runner? upgrade?]] [game.core.card-defs :refer [card-def]] + [game.core.choose-one :refer [choose-one-helper cost-option]] [game.core.cost-fns :refer [play-cost trash-cost]] [game.core.costs :refer [total-available-credits]] - [game.core.damage :refer [damage damage-bonus]] - [game.core.def-helpers :refer [choose-one-helper corp-recur cost-option defcard do-brain-damage reorder-choice something-can-be-advanced? get-x-fn with-revealed-hand]] + [game.core.damage :refer [damage]] + [game.core.def-helpers :refer [corp-recur defcard do-brain-damage reorder-choice something-can-be-advanced? get-x-fn with-revealed-hand]] [game.core.drawing :refer [draw]] [game.core.effects :refer [register-lingering-effect]] [game.core.eid :refer [effect-completed make-eid make-result]] [game.core.engine :refer [do-nothing pay register-events resolve-ability should-trigger?]] [game.core.events :refer [event-count first-event? last-turn? no-event? not-last-turn? turn-events ]] [game.core.flags :refer [can-score? clear-persistent-flag! in-corp-scored? - in-runner-scored? is-scored? prevent-jack-out + in-runner-scored? is-scored? register-persistent-flag! register-turn-flag! when-scored? zone-locked?]] [game.core.gaining :refer [gain-clicks gain-credits lose-clicks lose-credits]] @@ -37,6 +38,7 @@ trash-cards]] [game.core.payment :refer [can-pay? cost-target ->c]] [game.core.play-instants :refer [play-instant]] + [game.core.prevention :refer [damage-boost]] [game.core.prompts :refer [cancellable clear-wait-prompt show-wait-prompt]] [game.core.props :refer [add-counter add-prop]] [game.core.purging :refer [purge]] @@ -210,9 +212,13 @@ :player :runner :yes-ability {:msg (str "let the Runner make a run on " serv) :async true - :effect (effect (clear-wait-prompt :corp) - (make-run eid serv card) - (prevent-jack-out))} + :effect (req (clear-wait-prompt state :corp) + (register-lingering-effect + state side card + {:type :cannot-jack-out + :value true + :duration :end-of-run}) + (make-run state :runner eid serv card))} :no-ability {:msg "add itself to [their] score area as an agenda worth 1 agenda point" :effect (effect (clear-wait-prompt :corp) (as-agenda :corp card 1))}}}) @@ -713,11 +719,21 @@ true))))}}) (defcard "Defective Brainchips" - {:events [{:event :pre-damage - :req (req (and (= (:type context) :brain) - (first-event? state side :pre-damage #(= :brain (:type (first %)))))) - :msg "do 1 additional core damage" - :effect (effect (damage-bonus :brain 1))}]}) + {:prevention [{:prevents :pre-damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :condition :active + :req (req + (and (or (= :brain (:type context)) + (= :core (:type context))) + (first-event? state side :pre-damage-flag #(= :brain (:type (first %)))) + (not= :all (:prevented context)) + (pos? (:remaining context)) + (not (:unboostable context)))) + :msg "increase the pending core damage by 1" + :effect (req (damage-boost state side eid 1))}}]}) (defcard "Digital Rights Management" {:on-play diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index 6744f82e73..57be735975 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -10,10 +10,11 @@ is-type? program? resource? rezzed? runner?]] [game.core.card-defs :refer [card-def]] [game.core.charge :refer [charge-ability]] + [game.core.choose-one :refer [choose-one-helper]] [game.core.cost-fns :refer [install-cost rez-cost]] [game.core.costs :refer [total-available-credits]] - [game.core.damage :refer [damage damage-prevent]] - [game.core.def-helpers :refer [breach-access-bonus choose-one-helper defcard offer-jack-out trash-on-empty get-x-fn rfg-on-empty]] + [game.core.damage :refer [damage]] + [game.core.def-helpers :refer [breach-access-bonus defcard offer-jack-out trash-on-empty get-x-fn rfg-on-empty]] [game.core.drawing :refer [draw]] [game.core.effects :refer [any-effects is-disabled-reg? register-lingering-effect unregister-effects-for-card update-disabled-cards]] [game.core.eid :refer [effect-completed make-eid]] @@ -39,15 +40,15 @@ [game.core.link :refer [get-link]] [game.core.mark :refer [identify-mark-ability mark-changed-event]] [game.core.memory :refer [available-mu expected-mu update-mu]] - [game.core.moving :refer [flip-facedown mill move swap-cards swap-ice trash trash-cards - trash-prevent]] + [game.core.moving :refer [flip-facedown mill move swap-cards swap-ice trash trash-cards]] [game.core.optional :refer [get-autoresolve set-autoresolve never?]] [game.core.payment :refer [build-cost-label can-pay? cost-target cost-value ->c value]] + [game.core.prevention :refer [preventable? prevent-damage prevent-end-run prevent-up-to-n-damage prevent-trash-installed-by-type]] [game.core.prompts :refer [cancellable]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal]] [game.core.rezzing :refer [derez get-rez-cost rez]] - [game.core.runs :refer [active-encounter? bypass-ice continue end-run end-run-prevent + [game.core.runs :refer [active-encounter? bypass-ice continue end-run get-current-encounter make-run successful-run-replace-breach update-current-encounter]] [game.core.sabotage :refer [sabotage-ability]] @@ -547,7 +548,7 @@ :effect (effect (add-counter eid card :virus 1 nil))} {:event :expose :async true - :effect (effect (add-counter eid card :virus 1 nil))}]})) + :effect (effect (add-counter eid card :virus (count (:cards context)) nil))}]})) (defcard "Aurora" (auto-icebreaker {:abilities [(break-sub 2 1 "Barrier") @@ -592,29 +593,15 @@ :msg (msg "prevent " (card-str state current-ice) " from ending the run this encounter") :effect (req (let [target-ice (:ice (get-current-encounter state))] - (register-lingering-effect - state side - card - {:type :auto-prevent-run-end - :duration :end-of-encounter - :req (req - (let [target (second targets)] - (and (same-card? target target-ice) - ;;special case for border control/MIC - ;; this is an ugly hack, but we have - ;; no way of knowing which *ability* - ;; actually ended the run - ;; these seem like the safe hedge. - ;; MIC is included for paint effects. - ;; TODO - fix this, add :cause :subroutine to a bunch of - ;; end the run effects - (if (#{"Border Control" "M.I.C."} (:title target-ice)) - (not (some #(and - (same-card? target (:card (first %))) - (= (:cause (first %)) :ability-cost)) - (run-events state :corp :corp-trash))) - true)))) - :value (req true)})))}]})) + (register-events + state side card + [{:event :end-run-interrupt + :duration :end-of-encounter + :async true + :silent (req true) + :req (req (= :subroutine (->> context :source-eid :source-type))) + :msg "prevent the run from ending" + :effect (req (prevent-end-run state side eid))}])))}]})) (defcard "Battering Ram" (auto-icebreaker {:abilities [(break-sub 2 2 "Barrier") @@ -1249,12 +1236,11 @@ (strength-pump 2 3))) (defcard "Deus X" - {:interactions {:prevent [{:type #{:net} - :req (req true)}]} - :abilities [(break-sub [(->c :trash-can)] 0 "AP") - {:msg "prevent any amount of net damage" - :cost [(->c :trash-can)] - :effect (effect (damage-prevent :net Integer/MAX_VALUE))}]}) + {:prevention [{:prevents :damage + :type :ability + :ability (assoc (prevent-up-to-n-damage :all #{:net}) + :cost [(->c :trash-can)])}] + :abilities [(break-sub [(->c :trash-can)] 0 "AP")]}) (defcard "Dhegdheer" {:implementation "Discount not considered by any engine functions when checking if a program is playable" @@ -1529,12 +1515,9 @@ (let [abi {:label "Take 1 tag to place 2 virus counters (start of turn)" :once :per-turn :async true - :effect (req (wait-for (gain-tags state :runner 1) - (if (not (get-in @state [:tag :tag-prevent])) - (do (system-msg state side - (str "takes 1 tag to place 2 virus counters on God of War")) - (add-counter state side eid card :virus 2 nil)) - (effect-completed state side eid))))}] + :effect (req (wait-for (gain-tags state :runner 1 {:unpreventable true}) + (system-msg state side (str "takes 1 tag to place 2 virus counters on God of War")) + (add-counter state side eid card :virus 2 nil)))}] {:flags {:runner-phase-12 (req true)} :events [(choose-one-helper {:event :runner-turn-begins @@ -2018,14 +2001,10 @@ (strength-pump 1 2)]}))) (defcard "LLDS Energy Regulator" - {:interactions {:prevent [{:type #{:trash-hardware} - :req (req true)}]} - :abilities [{:cost [(->c :credit 3)] - :msg "prevent a piece of hardware from being trashed" - :effect (effect (trash-prevent :hardware 1))} - {:cost [(->c :trash-can)] - :msg "prevent a piece of hardware from being trashed" - :effect (effect (trash-prevent :hardware 1))}]}) + (letfn [(valid-context? [context] (and (not= :ability-cost (:cause context)) + (not (:game-trash context))))] + {:prevention [(prevent-trash-installed-by-type "3 [Credits]: LLDS Energy Regulator" #{"Hardware"} [(->c :credit 3)] valid-context?) + (prevent-trash-installed-by-type "[Trash]: LLDS Energy Regulator" #{"Hardware"} [(->c :trash-can)] valid-context?)]})) (defcard "Lobisomem" (auto-icebreaker {:data {:counter {:power 1}} @@ -2385,14 +2364,16 @@ card nil))}]}) (defcard "Net Shield" - {:interactions {:prevent [{:type #{:net} - :req (req true)}]} - ;; TODO - once a proper prevention system is set up, we can actually enforce the conditions - ;; on this card. nbkelly, 2024 - :abilities [{:cost [(->c :credit 1)] - :once :per-turn - :msg "prevent the first net damage this turn" - :effect (effect (damage-prevent :net 1))}]}) + {:prevention [{:prevents :damage + :type :ability + :max-uses 1 + :ability {:async true + :cost [(->c :credit 1)] + :msg "prevent 1 net damage" + :req (req (and (= :net (:type context)) + (preventable? context) + (first-event? state side :pre-damage-flag #(= :net (:type (first %)))))) + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "Nfr" (auto-icebreaker {:abilities [(break-sub 1 1 "Barrier")] @@ -3132,9 +3113,8 @@ :prompt "Expose approached piece of ice?" :yes-ability {:async true - :msg "expose the approached piece of ice" :effect (req (wait-for - (expose state side (:ice context)) + (expose state side [(:ice context)]) (continue-ability state side (offer-jack-out) card nil)))}}}]}) (defcard "Snowball" @@ -3503,11 +3483,11 @@ (not (rezzed? %)))} :async true :msg (str "name " chosen-subtype) - :effect (req (wait-for (expose state side target) - (when (has-subtype? async-result chosen-subtype) - (do (move state :corp async-result :hand) + :effect (req (wait-for (expose state side [target]) + (when (and async-result (has-subtype? target chosen-subtype)) + (do (move state :corp target :hand) (system-msg state :runner - (str "add " (:title async-result) " to HQ")))) + (str "add " (:title target) " to HQ")))) (effect-completed state side eid)))})] {:events [{:event :successful-run :interactive (req true) diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 9d3aec8cb7..18af95f27d 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -20,7 +20,7 @@ [game.core.cost-fns :refer [has-trash-ability? install-cost rez-cost trash-cost]] [game.core.costs :refer [total-available-credits]] - [game.core.damage :refer [damage damage-prevent]] + [game.core.damage :refer [damage]] [game.core.def-helpers :refer [breach-access-bonus defcard offer-jack-out reorder-choice spend-credits take-credits trash-on-empty do-net-damage]] [game.core.drawing :refer [draw click-draw-bonus]] @@ -52,12 +52,12 @@ [game.core.mark :refer [identify-mark-ability mark-changed-event is-mark?]] [game.core.memory :refer [available-mu]] [game.core.moving :refer [as-agenda flip-faceup forfeit mill move - remove-from-currently-drawing trash trash-cards - trash-prevent]] + remove-from-currently-drawing trash trash-cards]] [game.core.optional :refer [get-autoresolve never? set-autoresolve]] [game.core.payment :refer [build-spend-msg can-pay? ->c]] [game.core.pick-counters :refer [pick-virus-counters-to-spend]] [game.core.play-instants :refer [play-instant]] + [game.core.prevention :refer [damage-name prevent-damage preventable? prevent-encounter prevent-tag prevent-trash-installed-by-type prevent-up-to-n-tags prevent-up-to-n-damage]] [game.core.prompts :refer [cancellable]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal reveal-loud]] @@ -74,7 +74,7 @@ zone->name zones->sorted-names]] [game.core.set-aside :refer [set-aside set-aside-for-me]] [game.core.shuffling :refer [shuffle!]] - [game.core.tags :refer [gain-tags lose-tags tag-prevent]] + [game.core.tags :refer [gain-tags lose-tags]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] [game.core.threat :refer [threat-level]] @@ -586,25 +586,27 @@ :effect (req (mill state :corp eid :corp 1))}]}) (defcard "Bio-Modeled Network" - {:interactions {:prevent [{:type #{:net} - :req (req true)}]} - :events [{:event :pre-damage - :req (req (= (:type context) :net)) - :effect (effect (update! (assoc card :dmg-amount (:card context))))}] - :abilities [{:msg (msg "prevent " (dec (:dmg-amount card)) " net damage") - :label "prevent net damage" - :cost [(->c :trash-can)] - :effect (effect (damage-prevent :net (dec (:dmg-amount card))))}]}) + {:prevention [{:prevents :damage + :type :ability + :max-uses 1 + :ability {:async true + :cost [(->c :trash-can)] + :req (req + (and (> (:remaining context) 1) + (= :net (:type context)) + (preventable? context))) + :msg (msg "prevent " (dec (:remaining context)) " " (damage-name state) " damage") + :effect (req (prevent-damage state side eid (dec (:remaining context))))}}]}) (defcard "Biometric Spoofing" - {:interactions {:prevent [{:type #{:net :brain :meat} - :req (req true)}]} - :abilities [{:label "Prevent 2 damage" - :msg "prevent 2 damage" - :cost [(->c :trash-can)] - :effect (effect (damage-prevent :brain 2) - (damage-prevent :net 2) - (damage-prevent :meat 2))}]}) + {:prevention [{:prevents :damage + :type :ability + :max-uses 1 + :ability {:async true + :cost [(->c :trash-can)] + :req (req (preventable? context)) + :msg (msg "prevent " (min 2 (:remaining context)) " " (damage-name state) " damage") + :effect (req (prevent-damage state side eid (min 2 (:remaining context))))}}]}) (defcard "Blockade Runner" {:abilities [{:action true @@ -658,14 +660,14 @@ (trash state side eid card {:cause-card card})))}]}) (defcard "Caldera" - {:interactions {:prevent [{:type #{:net :brain} - :req (req true)}]} - :abilities [{:cost [(->c :credit 3)] - :msg "prevent 1 net damage" - :effect (effect (damage-prevent :net 1))} - {:cost [(->c :credit 3)] - :msg "prevent 1 core damage" - :effect (effect (damage-prevent :brain 1))}]}) + {:prevention [{:prevents :damage + :type :ability + :ability {:async true + :cost [(->c :credit 3)] + :msg (msg "prevent 1 " (damage-name state) " damage") + :req (req (and (contains? #{:net :core :brain} (:type context)) + (preventable? context))) + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "Charlatan" {:abilities [{:action true @@ -740,17 +742,26 @@ (effect-completed state side eid))))}]}) (defcard "Chrome Parlor" - {:events [{:event :pre-damage - :req (req (has-subtype? (:card context) "Cybernetic")) - :effect (effect (damage-prevent (:type context) Integer/MAX_VALUE))}]}) + {:prevention [{:prevents :damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req (and (has-subtype? (:source-card context) "Cybernetic") + (preventable? context))) + :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :effect (req (prevent-damage state side eid :all))}}]}) (defcard "Citadel Sanctuary" - {:interactions {:prevent [{:type #{:meat} - :req (req true)}]} - :abilities [{:label "Prevent all meat damage" - :msg "prevent all meat damage" - :cost [(->c :trash-can) (->c :trash-entire-hand)] - :effect (effect (damage-prevent :meat Integer/MAX_VALUE))}] + {:prevention [{:prevents :damage + :type :ability + :prompt "Use Citadel Sanctuary to prevent meat damage?" + :ability {:async true + :cost [(->c :trash-can) (->c :trash-entire-hand)] + :req (req (and (= :meat (:type context)) + (preventable? context))) + :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :effect (req (prevent-damage state side eid :all))}}] :events [{:event :runner-turn-ends :interactive (req true) :msg "force the Corp to initiate a trace" @@ -920,17 +931,15 @@ (make-run eid target card))}]})) (defcard "Crash Space" - {:interactions {:prevent [{:type #{:meat} - :req (req true)}] - :pay-credits {:req (req (or (= :remove-tag (:source-type eid)) + {:prevention [{:prevents :damage + :type :ability + :ability (assoc (prevent-up-to-n-damage 3 #{:meat}) + :cost [(->c :trash-can)])}] + :interactions {:pay-credits {:req (req (or (= :remove-tag (:source-type eid)) (and (same-card? (:source eid) (:basic-action-card runner)) (= 5 (:ability-idx (:source-info eid)))))) :type :recurring}} - :recurring 2 - :abilities [{:label "Trash to prevent up to 3 meat damage" - :msg "prevent up to 3 meat damage" - :cost [(->c :trash-can)] - :effect (effect (damage-prevent :meat 3))}]}) + :recurring 2}) (defcard "Crowdfunding" (let [ability {:async true @@ -1150,12 +1159,15 @@ :type :credit}}}) (defcard "Decoy" - {:interactions {:prevent [{:type #{:tag} - :req (req true)}]} - :abilities [{:async true - :cost [(->c :trash-can)] - :msg "avoid 1 tag" - :effect (effect (tag-prevent :runner eid 1))}]}) + {:prevention [{:prevents :tag + :type :ability + :label "Decoy" + :prompt "Trash Decoy to avoid 1 tag?" + :ability {:async true + :cost [(->c :trash-can)] + :msg "avoid 1 tag" + :req (req (preventable? context)) + :effect (req (prevent-tag state :runner eid 1))}}]}) (defcard "District 99" (letfn [(eligible-cards [runner] @@ -1319,18 +1331,10 @@ :msg "draw 10 cards"}]}) (defcard "Dummy Box" - (letfn [(better-name [card-type] (if (= "hardware" card-type) "piece of hardware" card-type)) - (dummy-prevent [card-type] - {:msg (str "prevent a " (better-name card-type) " from being trashed") - :cost [(->c (keyword (str "trash-" card-type "-from-hand")) 1)] - :effect (effect (trash-prevent (keyword card-type) 1))})] - {:interactions {:prevent [{:type #{:trash-hardware :trash-resource :trash-program} - :req (req (and (installed? (:prevent-target target)) - (not= :runner-ability (:cause target)) - (not= :purge (:cause target))))}]} - :abilities [(dummy-prevent "hardware") - (dummy-prevent "program") - (dummy-prevent "resource")]})) + (letfn [(valid-context? [context] (= :corp (:source-player context)))] + {:prevention [(prevent-trash-installed-by-type "Dummy Box (Hardware)" #{"Hardware"} [(->c :trash-hardware-from-hand 1)] valid-context?) + (prevent-trash-installed-by-type "Dummy Box (Program)" #{"Program"} [(->c :trash-program-from-hand 1)] valid-context?) + (prevent-trash-installed-by-type "Dummy Box (Resource)" #{"Resource"} [(->c :trash-resource-from-hand 1)] valid-context?)]})) (defcard "Earthrise Hotel" (let [ability {:msg "draw 2 cards" @@ -1428,17 +1432,13 @@ (make-run state side eid :archives (get-card state card))))}]})) (defcard "Fall Guy" - {:interactions {:prevent [{:type #{:trash-resource} - :req (req true)}]} - :abilities [{:label "Prevent another installed resource from being trashed" - :msg "prevent a resource from being trashed" - :cost [(->c :trash-can)] - :effect (effect (trash-prevent :resource 1))} - {:label "Gain 2 [Credits]" - :msg "gain 2 [Credits]" - :cost [(->c :trash-can)] - :async true - :effect (effect (gain-credits eid 2))}]}) + (letfn [(valid-context? [context] (not= :ability-cost (:cause context)))] + {:prevention [(prevent-trash-installed-by-type "Fall Guy" #{"Resource"} [(->c :trash-can)] valid-context?)] + :abilities [{:label "Gain 2 [Credits]" + :msg "gain 2 [Credits]" + :cost [(->c :trash-can)] + :async true + :effect (effect (gain-credits eid 2))}]})) (defcard "Fan Site" {:events [{:event :agenda-scored @@ -1633,32 +1633,33 @@ {:static-abilities [{:type :cannot-pay-net :value true} {:type :cannot-pay-meat - :value true} - {:type :cannot-pay-brain :value true}] - :events [{:event :pre-damage - :req (req (and (#{:meat :net} (:type context)) - (pos? (:amount context)))) - :msg (msg "prevent all " (name (:type context)) " damage") - :effect (req (damage-prevent state side :meat Integer/MAX_VALUE) - (damage-prevent state side :net Integer/MAX_VALUE) - (register-events - state side card - [{:event :pre-resolve-damage - :unregister-once-resolved true - :async true - :msg (msg (if (= target "Trash Guru Davinder") - "trash itself" - (decapitalize target))) - :prompt "Choose one" - :waiting-prompt true - :choices (req [(when (can-pay? state :runner (assoc eid :source card :source-type :ability) card nil (->c :credit 4)) - "Pay 4 [Credits]") - "Trash Guru Davinder"]) - :effect (req (if (= target "Trash Guru Davinder") - (trash state :runner eid card {:cause :runner-ability :cause-card card}) - (pay state :runner eid card (->c :credit 4))))}]))}]}) - + :prevention [{:prevents :damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req (and (or (= :meat (:type context)) + (= :net (:type context))) + (preventable? context))) + + :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :effect (req (wait-for (prevent-damage state side :all) + (continue-ability + state side + {:msg (msg (if (= target "Trash Guru Davinder") + "trash itself" + (decapitalize target))) + :prompt "Choose one" + :waiting-prompt true + :choices (req [(when (can-pay? state :runner (assoc eid :source card :source-type :ability) card nil (->c :credit 4)) + "Pay 4 [Credits]") + "Trash Guru Davinder"]) + :async true + :effect (req (if (= target "Trash Guru Davinder") + (trash state :runner eid card {:cause :runner-ability :cause-card card}) + (pay state :runner eid card (->c :credit 4))))} + card nil)))}}]}) (defcard "Hades Shard" (shard-constructor "Hades Shard" :archives "breach Archives" @@ -1729,23 +1730,14 @@ :effect (effect (gain-credits :runner eid (get-agenda-points (:card context))))}]}) (defcard "Hunting Grounds" - {:events [{:event :prevent-encounter-ability - :interactive (req true) - :async true - :req (req (and (not (get-in @state [:run :prevent-encounter-ability])) - (not-used-once? state {:once :per-turn} card))) - :effect (req - (if (get-in @state [:run :prevent-encounter-ability]) - (effect-completed state side eid) - (continue-ability - state side - {:optional {:prompt (msg "Prevent a \"when encountered\" ability on " (:title current-ice) (when (:ability-name target) - (str " (" (:ability-name target) ")"))) - :once :per-turn - :yes-ability {:msg (msg "prevent the encounter ability on " (:title current-ice) (when (:ability-name target) - (str " (" (:ability-name target) ")"))) - :effect (req (swap! state assoc-in [:run :prevent-encounter-ability] true))}}} - card targets)))}] + {:prevention [{:prevents :encounter + :type :event + :ability {:async true + :once :per-turn + :req (req (and (preventable? context) + (not-used-once? state {:once :per-turn} card))) + :msg (msg "prevent the encounter ability on " (:title current-ice)) + :effect (req (prevent-encounter state side eid))}}] :abilities [(letfn [(ri [cards] (when (seq cards) {:async true @@ -1871,14 +1863,17 @@ (defcard "Jarogniew Mercs" {:on-install {:async true :effect (req (wait-for (gain-tags state :runner 1) - (add-counter state :runner eid card :power (+ 3 (count-tags state)))))} + (add-counter state :runner eid card :power (+ 3 (count-tags state)) nil)))} :events [(trash-on-empty :power)] :flags {:untrashable-while-resources true} - :interactions {:prevent [{:type #{:meat} - :req (req true)}]} - :abilities [{:label "Prevent 1 meat damage" - :cost [(->c :power 1)] - :effect (req (damage-prevent state side :meat 1))}]}) + :prevention [{:prevents :damage + :type :ability + :ability {:async true + :cost [(->c :power 1)] + :msg "prevent 1 meat damage" + :req (req (and (= :meat (:type context)) + (preventable? context))) + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "John Masanori" {:events [{:event :successful-run @@ -2335,7 +2330,7 @@ run (= :corp (:active-player @state)) (#{:psi :trace} (:source-type eid)) - (#{:net :meat :brain :tag} (get-in @state [:prevent :current])))) + (get-in @state [:prevent]))) :type :credit}}}) (defcard "Network Exchange" @@ -2364,16 +2359,33 @@ :effect (req (swap! state assoc-in [:runner :register :must-trash-with-credits] false))}]}) (defcard "New Angeles City Hall" - {:interactions {:prevent [{:type #{:tag} - :req (req true)}]} + (letfn [(prevent-another-tag [] + {:optional + {:req (req (and (pos? (get-in @state [:prevent :tag :remaining])) + (can-pay? state side (assoc eid :source card :source-type :ability) card nil [(->c :credit 2)]))) + :prompt (msg "Pay 2 [Credits] to avoid another tag? (" (get-in @state [:prevent :tag :remaining]) " remaining)") + :yes-ability {:async true + :cost [(->c :credit 2)] + :msg "avoid 1 tag" + :effect (req (wait-for (prevent-tag state :runner 1) + (continue-ability + state side + (prevent-another-tag) + card nil)))}}})] + {:prevention [{:prevents :tag + :type :ability + :label "New Angeles City Hall" + :prompt "Pay 2 [Credits] to avoid a tag?" + :ability {:async true + :cost [(->c :credit 2)] + :msg "avoid 1 tag" + :req (req (preventable? context)) + :effect (req (wait-for (prevent-tag state :runner 1) + (continue-ability state side (prevent-another-tag) card nil)))}}] :events [{:event :agenda-stolen :async true :msg "trash itself" - :effect (effect (trash eid card {:cause :runner-ability :cause-card card}))}] - :abilities [{:async true - :cost [(->c :credit 2)] - :msg "avoid 1 tag" - :effect (effect (tag-prevent :runner eid 1))}]}) + :effect (effect (trash eid card {:cause :runner-ability :cause-card card}))}]})) (defcard "No Free Lunch" {:abilities [{:label "Gain 3 [Credits]" @@ -2388,31 +2400,43 @@ :effect (effect (lose-tags :runner eid 1))}]}) (defcard "No One Home" - (letfn [(first-chance? [state side] - (< (+ (event-count state side :pre-tag) - (event-count state side :pre-damage #(= :net (:type (first %))))) - 2)) - (start-trace [type] - (let [message (str "avoid any " (if (= type :net) - "amount of net damage" - "number of tags"))] - {:player :corp - :label (str "Trace 0 - if unsuccessful, " message) - :trace {:base 0 - :unsuccessful {:async true - :msg message - :effect (req (if (= type :net) - (do (damage-prevent state :runner :net Integer/MAX_VALUE) - (effect-completed state side eid)) - (tag-prevent state :runner eid Integer/MAX_VALUE)))}}}))] - {:interactions {:prevent [{:type #{:net :tag} - :req (req (first-chance? state side))}]} - :abilities [{:msg "force the Corp to trace" - :async true - :effect (req (let [prevent-type (get-in @state [:prevent :current])] - (wait-for (trash state side card {:unpreventable true :cause-card card}) - (continue-ability state side (start-trace prevent-type) - card nil))))}]})) + {:prevention [{:prevents :damage + :type :event + :prompt "Trash No One Home to force the Corp to trace" + :ability {:async true + :msg "force the Corp to trace" + :req (req (and (= :net (:type context)) + (first-event? state side :pre-damage-flag #(= :net (:type (first %)))) + (no-event? state side :runner-prevents-all-tags) + (no-event? state side :runner-gain-tag) + (preventable? state :damage))) + :effect (req (wait-for + (trash state side card {:unpreventable true :cause-card card}) + (continue-ability + state :corp + {:label "Trace 0 - if unsuccessful, the Runner prevents any amount of net damage" + :trace {:base 0 + :unsuccessful {:async true + :effect (req (continue-ability state :runner (prevent-up-to-n-damage :all #{:net}) card nil))}}} + card nil)))}} + {:prevents :tag + :type :event + :prompt "Trash No One Home to force the Corp to trace" + :ability {:async true + :msg "force the Corp to trace" + :req (req (and (first-event? state side :tag-interrupt) + (preventable? state :tag) + (no-event? state side :all-damage-was-prevented #(= :net (:type (first %)))) + (no-event? state side :damage #(= :net (:damage-type (first %)))))) + :effect (req (wait-for + (trash state side card {:unpreventable true :cause-card card}) + (continue-ability + state :corp + {:label "Trace 0 - if unsuccessful, the Runner avoids any number of tags" + :trace {:base 0 + :unsuccessful {:async true + :effect (req (continue-ability state :runner (prevent-up-to-n-tags :all) card nil))}}} + card nil)))}}]}) (defcard "Off-Campus Apartment" {:flags {:runner-install-draw true} @@ -2528,12 +2552,17 @@ :effect (req (take-credits state side eid card :credit 1))})) (defcard "Paparazzi" - {:static-abilities [{:type :is-tagged - :val true}] - :events [{:event :pre-damage - :req (req (= (:type context) :meat)) - :msg "prevent all meat damage" - :effect (effect (damage-prevent :meat Integer/MAX_VALUE))}]}) + {:prevention [{:prevents :damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req (and (= :meat (:type context)) + (preventable? context))) + :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :effect (req (prevent-damage state side eid :all))}}] + :static-abilities [{:type :is-tagged + :value true}]}) (defcard "Patron" (let [ability {:prompt "Choose a server" @@ -2745,12 +2774,11 @@ :async true :msg "breach HQ" :effect (req (breach-server state :runner eid [:hq] {:no-root true}))}] - :abilities [{:msg "expose 1 card" - :label "Expose 1 installed card" + :abilities [{:label "Expose 1 installed card" :choices {:card installed?} :async true :cost [(->c :trash-can)] - :effect (effect (expose eid target))}]}) + :effect (effect (expose eid [target]))}]}) (defcard "Reclaim" {:abilities @@ -2880,41 +2908,36 @@ card nil))}]})) (defcard "Sacrificial Clone" - {:interactions {:prevent [{:type #{:net :brain :meat} - :req (req true)}]} - :abilities [{:cost [(->c :trash-can)] - :label "prevent damage" - :async true - :msg (msg (let [cards (concat (get-in runner [:rig :hardware]) - (filter #(not (has-subtype? % "Virtual")) - (get-in runner [:rig :resource])) - (:hand runner))] - (str "prevent all damage, trash " - (quantify (count cards) "card") - " (" (enumerate-str (map :title cards)) ")," - " lose " (quantify (:credit (:runner @state)) "credit") - ", and lose " (quantify (count-real-tags state) "tag")))) - :effect (req (damage-prevent state side :net Integer/MAX_VALUE) - (damage-prevent state side :meat Integer/MAX_VALUE) - (damage-prevent state side :brain Integer/MAX_VALUE) - (wait-for - (trash-cards - state side - (concat (get-in runner [:rig :hardware]) - (filter #(not (has-subtype? % "Virtual")) - (get-in runner [:rig :resource])) - (:hand runner)) - {:cause-card card}) - (wait-for (lose-credits state side (make-eid state eid) :all) - (lose-tags state side eid :all))))}]}) - + {:prevention [{:prevents :damage + :type :ability + :max-uses 1 + :ability {:async true + :cost [(->c :trash-can)] + :req (req (preventable? context)) + :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :effect (req (wait-for (prevent-damage state side :all) + (let [cards (concat (get-in runner [:rig :hardware]) + (filter #(not (has-subtype? % "Virtual")) + (get-in runner [:rig :resource])) + (:hand runner))] + (system-msg state side (str "uses " (:title card) " to trash " + (quantify (count cards) "card") + " (" (enumerate-str (map :title cards)) ")," + " lose " (quantify (:credit (:runner @state)) "credit") + ", and lose " (quantify (count-real-tags state) "tag"))) + (wait-for (trash-cards + state side + (concat (get-in runner [:rig :hardware]) + (filter #(not (has-subtype? % "Virtual")) + (get-in runner [:rig :resource])) + (:hand runner)) + {:cause-card card}) + (wait-for (lose-credits state side (make-eid state eid) :all) + (lose-tags state side eid :all))))))}}]}) (defcard "Sacrificial Construct" - {:interactions {:prevent [{:type #{:trash-program :trash-hardware} - :req (req true)}]} - :abilities [{:cost [(->c :trash-can)] - :label "prevent a program trash" - :effect (effect (trash-prevent :program 1) - (trash-prevent :hardware 1))}]}) + (letfn [(valid-context? [context] (and (not= :ability-cost (:cause context)) + (not (:game-trash context))))] + {:prevention [(prevent-trash-installed-by-type "Sacrificial Construct" #{"Program" "Hardware"} [(->c :trash-can)] valid-context?)]})) (defcard "Safety First" {:static-abilities [(runner-hand-size+ -2)] diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index e6b4834710..f8cf177770 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -23,7 +23,6 @@ [game.core.engine :refer [dissoc-req pay register-default-events register-events resolve-ability unregister-events]] [game.core.events :refer [first-event? first-run-event? no-event? turn-events]] - [game.core.expose :refer [expose-prevent]] [game.core.finding :refer [find-cid find-latest]] [game.core.flags :refer [clear-persistent-flag! is-scored? register-persistent-flag! register-run-flag!]] @@ -1743,31 +1742,17 @@ (force-ice-encounter state side eid current-ice))))}}}]}) (defcard "Tori Hanzō" - {:events [{:event :pre-resolve-damage - :optional - {:req (req (and this-server - (= target :net) - (= :corp (second targets)) - (pos? (last targets)) - (first-run-event? state side :pre-resolve-damage - (fn [[t s]] - (and (= :net t) - (= :corp s)))) - (can-pay? state :corp (assoc eid :source card :source-type :ability) card nil [(->c :credit 2)]))) - :waiting-prompt true - :prompt "Pay 2 [Credits] to do 1 core damage?" - :player :corp - :yes-ability - {:async true - :msg "do 1 core damage instead of net damage" - :effect (req (swap! state update :damage dissoc :damage-replace :defer-damage) - (wait-for (pay state :corp card (->c :credit 2)) - (system-msg state side (:msg async-result)) - (wait-for (damage state side :brain 1 {:card card}) - (swap! state assoc-in [:damage :damage-replace] true) - (effect-completed state side eid))))} - :no-ability - {:effect (req (swap! state update :damage dissoc :damage-replace))}}}]}) + {:prevention [{:prevents :damage + :type :event + :max-uses 1 + :prompt "Pay 2 [Credits] to do 1 core damage instead?" + :ability {:cost [(->c :credit 2)] + :msg "instead do 1 core damage" + :req (req (and (= :net (:type context)) + (= :corp (:source-player context)) + (first-run-event? state side :pre-damage-flag #(= :net (:type (first %)))) + (pos? (:remaining context)))) + :effect (req (swap! state update-in [:prevent :damage] merge {:type :brain :prevented 0 :count 1 :remaining 1 :source-card card}))}}]}) (defcard "Traffic Analyzer" {:events [{:event :rez @@ -1836,11 +1821,10 @@ :msg (msg "prevent a subroutine on " (:title current-ice) " from being broken")}]}) (defcard "Underway Grid" - {:events [{:event :pre-expose - :req (req (same-server? card target)) - :msg "prevent 1 card from being exposed" - :effect (effect (expose-prevent 1))}] - :static-abilities [{:type :bypass-ice + {:static-abilities [{:type :cannot-be-exposed + :req (req (same-server? card target)) + :value true} + {:type :bypass-ice :req (req (same-server? card target)) :value false}]}) @@ -1973,7 +1957,7 @@ (some #(:printed %) (:subroutines target)) (not (:disabled target)))) :value (req {:async true - :ability-name "ZATO Ability" + :ability-name "ZATO City Grid" :interactive (req true) :optional {:waiting-prompt true diff --git a/src/clj/game/core.clj b/src/clj/game/core.clj index 7effe0352d..8fccba8b6d 100644 --- a/src/clj/game/core.clj +++ b/src/clj/game/core.clj @@ -145,7 +145,6 @@ (expose-vars [game.core.bad-publicity - bad-publicity-prevent gain-bad-publicity lose-bad-publicity]) @@ -281,9 +280,6 @@ chosen-damage corp-can-choose-damage? damage - damage-bonus - damage-count - damage-prevent enable-corp-damage-choice enable-runner-damage-choice runner-can-choose-damage?]) @@ -388,8 +384,7 @@ (expose-vars [game.core.expose - expose - expose-prevent]) + expose]) (expose-vars [game.core.finding @@ -400,7 +395,6 @@ (expose-vars [game.core.flags - ab-can-prevent? any-flag-fn? can-access-loud can-access? @@ -411,10 +405,8 @@ can-score? can-steal? can-trash? - card-can-prevent? card-flag-fn? card-flag? - cards-can-prevent? check-flag-types? clear-all-flags-for-card! clear-persistent-flag! @@ -422,9 +414,6 @@ clear-run-register! clear-turn-flag! clear-turn-register! - get-card-prevention - get-prevent-list - get-preventing-cards has-flag? in-corp-scored? in-runner-scored? @@ -433,7 +422,6 @@ persistent-flag? prevent-current prevent-draw - prevent-jack-out register-persistent-flag! register-run-flag! register-turn-flag! @@ -594,7 +582,6 @@ swap-installed trash trash-cards - trash-prevent uninstall]) (expose-vars @@ -707,7 +694,6 @@ continue encounter-ends end-run - end-run-prevent force-ice-encounter gain-next-run-credits gain-run-credits @@ -715,7 +701,6 @@ get-runnable-zones handle-end-run jack-out - jack-out-prevent make-run pass-ice prevent-access @@ -803,8 +788,7 @@ (expose-vars [game.core.tags gain-tags - lose-tags - tag-prevent]) + lose-tags]) (expose-vars [game.core.threat diff --git a/src/clj/game/core/bad_publicity.clj b/src/clj/game/core/bad_publicity.clj index ee2873981c..069554431e 100644 --- a/src/clj/game/core/bad_publicity.clj +++ b/src/clj/game/core/bad_publicity.clj @@ -1,20 +1,14 @@ (ns game.core.bad-publicity (:require [game.core.eid :refer [effect-completed make-eid make-result]] - [game.core.engine :refer [checkpoint queue-event trigger-event trigger-event-sync]] - [game.core.flags :refer [cards-can-prevent? get-prevent-list]] + [game.core.engine :refer [queue-event checkpoint trigger-event-sync]] [game.core.gaining :refer [gain lose]] [game.core.prompts :refer [clear-wait-prompt show-prompt show-wait-prompt]] + [game.core.prevention :refer [resolve-bad-pub-prevention]] [game.core.say :refer [system-msg]] [game.core.toasts :refer [toast]] [game.macros :refer [wait-for]])) -(defn bad-publicity-prevent - [state side n] - (swap! state update-in [:bad-publicity :bad-publicity-prevent] (fnil #(+ % n) 0)) - (trigger-event state side (if (= side :corp) :corp-prevent :runner-prevent) {:type :bad-publicity - :amount n})) - (defn- resolve-bad-publicity [state side eid n {:keys [suppress-checkpoint] :as args}] (if (pos? n) @@ -26,43 +20,13 @@ (checkpoint state eid))) (effect-completed state side eid))) -(defn- bad-publicity-count - "Calculates the number of bad publicity to give, taking into account prevention and boosting effects." - [state _ n {:keys [unpreventable unboostable]}] - (-> n - (+ (or (when-not unboostable (get-in @state [:bad-publicity :bad-publicity-bonus])) 0)) - (- (or (when-not unpreventable (get-in @state [:bad-publicity :bad-publicity-prevent])) 0)) - (max 0))) - (defn gain-bad-publicity "Attempts to give the corp n bad publicity, allowing for boosting/prevention effects." ([state side n] (gain-bad-publicity state side (make-eid state) n nil)) ([state side eid n] (gain-bad-publicity state side eid n nil)) ([state side eid n {:keys [unpreventable card] :as args}] - (swap! state update-in [:bad-publicity] dissoc :bad-publicity-bonus :bad-publicity-prevent) - (wait-for (trigger-event-sync state side :pre-bad-publicity card) - (let [n (bad-publicity-count state side n args) - prevent (get-prevent-list state :corp :bad-publicity)] - (if (and (pos? n) - (not unpreventable) - (cards-can-prevent? state :corp prevent :bad-publicity)) - (do (system-msg state :corp "has the option to avoid bad publicity") - (show-wait-prompt state :runner "Corp to prevent bad publicity") - (swap! state assoc-in [:prevent :current] :bad-publicity) - (show-prompt - state :corp nil - (str "Avoid " (when (< 1 n) "any of the ") n " bad publicity?") ["Done"] - (fn [_] - (let [prevent (get-in @state [:bad-publicity :bad-publicity-prevent])] - (system-msg state :corp - (if prevent - (str "avoids " - (if (= prevent Integer/MAX_VALUE) "all" prevent) - " bad publicity") - "will not avoid bad publicity")) - (clear-wait-prompt state :runner) - (resolve-bad-publicity state side eid (max 0 (- n (or prevent 0))) args))))) - (resolve-bad-publicity state side eid n args)))))) + (wait-for (resolve-bad-pub-prevention state side n args) + (resolve-bad-publicity state side eid (:remaining async-result) args)))) (defn lose-bad-publicity ([state side n] (lose-bad-publicity state side (make-eid state) n)) diff --git a/src/clj/game/core/choose_one.clj b/src/clj/game/core/choose_one.clj new file mode 100644 index 0000000000..5c0154860c --- /dev/null +++ b/src/clj/game/core/choose_one.clj @@ -0,0 +1,93 @@ +(ns game.core.choose-one + (:require + [game.macros :refer [continue-ability req wait-for]] + [game.core.engine :refer [resolve-ability]] + [game.core.payment :refer [build-cost-string can-pay?]] + [game.core.eid :refer [effect-completed make-eid]] + [clojure.string :as str])) + +(defn choose-one-helper + ;; keys unique to this function: + ;; no-prune: can I select the same option more than once? + ;; no-wait-msg: do we hide the wait message from the runner? + ;; count: number of choices we're allowed to pick + ([xs] (choose-one-helper nil xs)) + ([{:keys [prompt count optional no-prune no-wait-msg interactive require-meaningful-choice] :as args} xs] + ;;the 'prompt' key cant compute 5-fns, so this needs to be disambiguated + (if (fn? (:count args)) + {:async true + :effect (req (let [new-count ((:count args) state side eid card targets)] + (continue-ability + state side + (choose-one-helper (assoc args :count new-count) xs) + card nil)))} + ;; xs of the form {:option ... :req (req ...) :cost ... :ability ..} + (let [next-optional (= optional :after-first) + apply-optional (and optional (not next-optional)) + xs (if-not apply-optional xs (conj xs {:option "Done"})) + base-map (select-keys args [:action :player :once :unregister-once-resolved :event + :label :change-in-game-state :location :additional-cost]) + ;; is a choice payable + payable? (fn [x state side eid card targets] + (when (or (not (:cost x)) + (can-pay? state (or (:player args) side) eid card nil (:cost x))) + x)) + ;; cost->str for a choice + costed-str (fn [x] + (if-not (:cost x) + (:option x) + (let [cs (build-cost-string (:cost x))] + (if-not (:option x) cs (str cs ": " (:option x)))))) + ;; converts options to choices + choices-fn (fn [x state side eid card targets] + (when (payable? x state side eid card targets) + (if-not (:req x) + (costed-str x) + (when ((:req x) state side eid card targets) + (costed-str x))))) + ;; this lets us selectively skip the prompt if 'done' is the only choice + meaningful-req? (when require-meaningful-choice + (req (let [cs (keep #(choices-fn % state side eid card targets) xs)] + (and (not= cs ["Done"]) + (or (nil? (:req args)) + ((:req args) state side eid card targets))))))] + ;; function for resolving choices: pick the matching choice, pay, resolve it, and continue + ;; when applicable + (letfn [(resolve-choices [xs full state side eid card target] + (if-not (seq xs) + (effect-completed state side eid ) + (if (= target (costed-str (first xs))) + ;; allow for resolving multiple options, like decues wild + (wait-for + (resolve-ability + state side (make-eid state eid) + (assoc (:ability (first xs)) :cost (:cost (first xs))) + card nil) ;; below is maybe superflous + (if (and count (> count 1) (not= target "Done")) + ;; the 'Done' is already there, so can dissoc optional + (let [args (assoc args :count (dec count) :optional next-optional) + xs (if no-prune full + (vec (remove #(= target (costed-str %)) full)))] + (continue-ability state side (choose-one-helper args xs) card nil)) + (effect-completed state side eid))) + (resolve-choices (rest xs) full state side eid card target))))] + (merge + base-map + {:choices (req (into [] (map #(choices-fn % state side eid card targets) xs))) + :waiting-prompt (or (:waiting-prompt args) (not no-wait-msg)) + :prompt (str (or (:prompt args) "Choose one") + ;; if we are resolving multiple + (when (and count (pos? count)) (str " (" count " remaining)"))) + :req (or meaningful-req? (:req args)) + ;; resolve-choices demands async + :async true + ;; interactive expects a 5-fn or nil + ;; but I want to just be able to say True or False + :interactive (when interactive (if-not (fn? interactive) (req interactive) interactive)) + :effect (req (resolve-choices xs xs state side eid card target))})))))) + +(defn cost-option + [cost side] + {:cost cost + :ability {:display-side side + :msg :cost}}) diff --git a/src/clj/game/core/cost_fns.clj b/src/clj/game/core/cost_fns.clj index 88a56c0c44..30e74d89b5 100644 --- a/src/clj/game/core/cost_fns.clj +++ b/src/clj/game/core/cost_fns.clj @@ -110,10 +110,12 @@ (defn has-trash-ability? [card] (let [abilities (:abilities (card-def card)) + prevents (map :ability (:prevention (card-def card))) + access-ab [(get-in (card-def card) [:interactions :access-ability])] events (:events (card-def card))] - (or (some :trash-icon (concat abilities events)) + (or (some :trash-icon (concat abilities events prevents access-ab)) (some #(= :trash-can (:cost/type %)) - (->> abilities + (->> (concat abilities events prevents access-ab) (map :cost) (vec) (merge-costs)))))) diff --git a/src/clj/game/core/costs.clj b/src/clj/game/core/costs.clj index bc5f6c17a2..63ac9ef3ff 100644 --- a/src/clj/game/core/costs.clj +++ b/src/clj/game/core/costs.clj @@ -176,7 +176,7 @@ :paid/type :credit :paid/value 0})))))) -;; X Credits +;; X Credits - can take ':maximum' to specify a max number that can be paid (defmethod value :x-credits [_] 0) ;We put stealth credits in the third slot rather than the empty second slot for consistency with credits (defmethod stealth-value :x-credits [cost] (or (:cost/stealth cost) 0)) @@ -191,7 +191,9 @@ state side {:async true :prompt "How many credits do you want to spend?" - :choices {:number (req (total-available-credits state side eid card))} + :choices {:number (req (if-let [maximum (->> cost :cost/args :maximum)] + (min (total-available-credits state side eid card) maximum) + (total-available-credits state side eid card)))} :effect (req (let [stealth-value (if (= -1 (stealth-value cost)) cost (stealth-value cost)) @@ -250,7 +252,7 @@ (wait-for (trash state side card {:cause :ability-cost :unpreventable true :suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "trashes " (:title card)) + (complete-with-result state side eid {:paid/msg (str "trashes " (:printed-title card)) :paid/type :trash-can :paid/value 1 :paid/targets [card]}))) diff --git a/src/clj/game/core/damage.clj b/src/clj/game/core/damage.clj index 7afe160efa..0d447a77ed 100644 --- a/src/clj/game/core/damage.clj +++ b/src/clj/game/core/damage.clj @@ -3,8 +3,8 @@ [game.core.card :refer [get-title]] [game.core.eid :refer [complete-with-result effect-completed make-eid]] [game.core.engine :refer [checkpoint queue-event trigger-event trigger-event-simult]] - [game.core.flags :refer [cards-can-prevent? get-prevent-list]] [game.core.moving :refer [trash-cards get-trash-event]] + [game.core.prevention :refer [resolve-damage-prevention]] [game.core.prompt-state :refer [add-to-prompt-queue remove-from-prompt-queue]] [game.core.prompts :refer [clear-wait-prompt show-prompt show-wait-prompt]] [game.core.say :refer [system-msg]] @@ -13,11 +13,6 @@ [game.utils :refer [dissoc-in enumerate-str side-str]] [jinteki.utils :refer [str->int]])) -(defn damage-bonus - "Registers a bonus of n damage to the next damage application of the given type." - [state _ dtype n] - (swap! state update-in [:damage :damage-bonus dtype] (fnil #(+ % n) 0))) - (defn damage-name [damage-type] (case damage-type :net "net" @@ -26,40 +21,6 @@ :brain "core" "[UNKNOWN DAMAGE TYPE]")) -(defn prevention-prompt-msg - [damage-amount damage-type prevented] - (str "Prevent " - (when (< 1 damage-amount) "any of the ") - damage-amount - " " (damage-name damage-type) " damage?" - " (" prevented "/" damage-amount " prevented)")) - -(defn- damage-prevent-update-prompt - "Look at the current runner prompt and (if a damage prevention prompt), update message." - [state side] - (when-let [prompt (first (get-in @state [side :prompt]))] - (when-let [match (re-matches #"^Prevent (?:any of the )?(\d+) (\w+) damage\?.*" (:msg prompt))] - (let [damage-amount (str->int (second match)) - damage-type (case (nth match 2) - "net" :net - "brain" :brain - "core" :brain - "meat" :meat) - prevented (get-in @state [:damage :damage-prevent damage-type] 0) - new-prompt (assoc prompt :msg (prevention-prompt-msg damage-amount - damage-type - prevented))] - (remove-from-prompt-queue state side prompt) - (if (>= prevented damage-amount) - ((:effect prompt) nil) - (add-to-prompt-queue state side new-prompt)))))) - -(defn damage-prevent - "Registers a prevention of n damage to the next damage application of the given type. Afterwards update current prevention prompt, if found." - [state side dtype n] - (swap! state update-in [:damage :damage-prevent dtype] (fnil #(+ % n) 0)) - (damage-prevent-update-prompt state side)) - (defn enable-runner-damage-choice [state _] (swap! state assoc-in [:damage :damage-choose-runner] true)) @@ -94,35 +55,28 @@ (swap! state update-in [:damage] dissoc :damage-choose-runner) (swap! state update-in [:damage] dissoc :damage-choose-corp))))) -(defn- handle-replaced-damage - [state side eid] - (swap! state update-in [:damage :defer-damage] dissoc type) - (swap! state update-in [:damage] dissoc :damage-replace) - (effect-completed state side eid)) - (defn- resolve-damage "Resolves the attempt to do n damage, now that both sides have acted to boost or prevent damage." [state side eid dmg-type n {:keys [card cause suppress-checkpoint]}] - (swap! state update-in [:damage :defer-damage] dissoc dmg-type) (swap! state dissoc-in [:damage :chosen-damage]) (damage-choice-priority state) (wait-for (trigger-event-simult state side :pre-resolve-damage nil dmg-type side n) - (if (get-in @state [:damage :damage-replace]) - (handle-replaced-damage state side eid) - (if (not (pos? n)) - (effect-completed state side eid) - (let [hand (get-in @state [:runner :hand]) - chosen-cards (seq (get-chosen-damage state)) - chosen-cids (into #{} (map :cid chosen-cards)) - leftovers (remove #(contains? chosen-cids (:cid %)) hand) - cards-trashed (->> (shuffle leftovers) - (take (- n (count chosen-cards))) - (concat chosen-cards))] - (when (= dmg-type :brain) - (swap! state update-in [:runner :brain-damage] #(+ % n))) - (when-let [trashed-msg (enumerate-str (map get-title cards-trashed))] - (system-msg state :runner (str "trashes " trashed-msg " due to " (damage-name dmg-type) " damage"))) + (if (not (pos? n)) + (do ;; shouldn't be possible, should be handled before getting here + (println "attempted to resolve 0 damage") + (effect-completed state side eid)) + (let [hand (get-in @state [:runner :hand]) + chosen-cards (seq (get-chosen-damage state)) + chosen-cids (into #{} (map :cid chosen-cards)) + leftovers (remove #(contains? chosen-cids (:cid %)) hand) + cards-trashed (->> (shuffle leftovers) + (take (- n (count chosen-cards))) + (concat chosen-cards))] + (when (= dmg-type :brain) + (swap! state update-in [:runner :brain-damage] #(+ % n))) + (when-let [trashed-msg (enumerate-str (map get-title cards-trashed))] + (system-msg state :runner (str "trashes " trashed-msg " due to " (damage-name dmg-type) " damage")) (swap! state update-in [:stats :corp :damage :all] (fnil + 0) n) (swap! state update-in [:stats :corp :damage dmg-type] (fnil + 0) n) (if (< (count hand) n) @@ -144,56 +98,18 @@ (wait-for (checkpoint state nil (make-eid state eid) args) (complete-with-result state side eid cards-trashed))))))))))) -(defn damage-count - "Calculates the amount of damage to do, taking into account prevention and boosting effects." - [state _ dtype n {:keys [unpreventable unboostable]}] - (-> n - (+ (or (when-not unboostable (get-in @state [:damage :damage-bonus dtype])) 0)) - (- (or (when-not unpreventable (get-in @state [:damage :damage-prevent dtype])) 0)) - (max 0))) - -(defn check-damage-prevention - "for a preventable damage instance, handles all damage prevention effects that a player can use for it" - ([state side eid type n player] - (let [interrupts (get-prevent-list state player type) - cards-can-prevent (cards-can-prevent? state player interrupts type nil {:side side}) - other-player (if (= player :corp) :runner :corp) - already-prevented (or (get-in @state [:damage :damage-prevent type]) 0)] - (if (and cards-can-prevent - (> n already-prevented)) - ;; player can prevent damage - (do (system-msg state player "has the option to prevent damage") - (show-wait-prompt state other-player (str (side-str player) " to prevent damage")) - (swap! state assoc-in [:prevent :current] type) - (show-prompt - state player nil - (str "Prevent " (when (< 1 (- n already-prevented)) "any of the ") (- n already-prevented) " " (damage-name type) " damage?") - ["Done"] - (fn [_] (let [prevent (get-in @state [:damage :damage-prevent type]) - damage-prevented (if prevent (- prevent already-prevented) false)] - (system-msg state player - (if damage-prevented - (str "prevents " - (if (= damage-prevented Integer/MAX_VALUE) "all" damage-prevented) - " " (damage-name type) " damage") - "will not prevent damage")) - (clear-wait-prompt state other-player) - (effect-completed state side eid))) - {:prompt-type :prevent})) - (effect-completed state side eid))))) - (defn damage "Attempts to deal n damage of the given type to the runner. Starts the prevention/boosting process and eventually resolves the damage." ([state side eid type n] (damage state side eid type n nil)) - ([state side eid type n {:keys [unpreventable card] :as args}] - (swap! state update-in [:damage :damage-bonus] dissoc type) - (swap! state update-in [:damage :damage-prevent] dissoc type) - ;; alert listeners that damage is about to be calculated. - (trigger-event state side :pre-damage {:type type :card card :amount n}) - (let [active-player (get-in @state [:active-player])] - (if unpreventable - (resolve-damage state side eid type (damage-count state side type n args) args) - (wait-for (check-damage-prevention state side type n active-player) - (wait-for (check-damage-prevention state side type n (if (= active-player :corp) :runner :corp)) - (resolve-damage state side eid type (damage-count state side type n args) args))))))) + ([state side eid type n {:keys [unpreventable card suppress-checkpoint] :as args}] + (wait-for (resolve-damage-prevention state side type n args) + (let [{:keys [remaining type source-card]} async-result] + (if (pos? remaining) + (resolve-damage state side eid type remaining (assoc args :card source-card)) + (do (queue-event state :all-damage-was-prevented {:side side + :type type + :cause-card source-card}) + (if suppress-checkpoint + (effect-completed state side eid) + (checkpoint state side eid)))))))) diff --git a/src/clj/game/core/def_helpers.clj b/src/clj/game/core/def_helpers.clj index 88709ecf15..f93c5a402d 100644 --- a/src/clj/game/core/def_helpers.clj +++ b/src/clj/game/core/def_helpers.clj @@ -286,92 +286,6 @@ (def card-defs-cache (atom {})) -(defn choose-one-helper - ;; keys unique to this function: - ;; no-prune: can I select the same option more than once? - ;; no-wait-msg: do we hide the wait message from the runner? - ;; count: number of choices we're allowed to pick - ([xs] (choose-one-helper nil xs)) - ([{:keys [prompt count optional no-prune no-wait-msg interactive require-meaningful-choice] :as args} xs] - ;;the 'prompt' key cant compute 5-fns, so this needs to be disambiguated - (if (fn? (:count args)) - {:async true - :effect (req (let [new-count ((:count args) state side eid card targets)] - (continue-ability - state side - (choose-one-helper (assoc args :count new-count) xs) - card nil)))} - ;; xs of the form {:option ... :req (req ...) :cost ... :ability ..} - (let [next-optional (= optional :after-first) - apply-optional (and optional (not next-optional)) - xs (if-not apply-optional xs (conj xs {:option "Done"})) - base-map (select-keys args [:action :player :once :unregister-once-resolved :event - :label :change-in-game-state :location :additional-cost]) - ;; is a choice payable - payable? (fn [x state side eid card targets] - (when (or (not (:cost x)) - (can-pay? state (or (:player args) side) eid card nil (:cost x))) - x)) - ;; cost->str for a choice - costed-str (fn [x] - (if-not (:cost x) - (:option x) - (let [cs (build-cost-string (:cost x))] - (if-not (:option x) cs (str cs ": " (:option x)))))) - ;; converts options to choices - choices-fn (fn [x state side eid card targets] - (when (payable? x state side eid card targets) - (if-not (:req x) - (costed-str x) - (when ((:req x) state side eid card targets) - (costed-str x))))) - ;; this lets us selectively skip the prompt if 'done' is the only choice - meaningful-req? (when require-meaningful-choice - (req (let [cs (keep #(choices-fn % state side eid card targets) xs)] - (and (not= cs ["Done"]) - (or (nil? (:req args)) - ((:req args) state side eid card targets))))))] - ;; function for resolving choices: pick the matching choice, pay, resolve it, and continue - ;; when applicable - (letfn [(resolve-choices [xs full state side eid card target] - (if-not (seq xs) - (effect-completed state side eid ) - (if (= target (costed-str (first xs))) - ;; allow for resolving multiple options, like decues wild - (wait-for - (resolve-ability - state side (make-eid state eid) - (assoc (:ability (first xs)) :cost (:cost (first xs))) - card nil) ;; below is maybe superflous - (if (and count (> count 1) (not= target "Done")) - ;; the 'Done' is already there, so can dissoc optional - (let [args (assoc args :count (dec count) :optional next-optional) - xs (if no-prune full - (vec (remove #(= target (costed-str %)) full)))] - (continue-ability state side (choose-one-helper args xs) card nil)) - (effect-completed state side eid))) - (resolve-choices (rest xs) full state side eid card target))))] - (merge - base-map - {:choices (req (into [] (map #(choices-fn % state side eid card targets) xs))) - :waiting-prompt (not no-wait-msg) - :prompt (str (or (:prompt args) "Choose one") - ;; if we are resolving multiple - (when (and count (pos? count)) (str " (" count " remaining)"))) - :req (or meaningful-req? (:req args)) - ;; resolve-choices demands async - :async true - ;; interactive expects a 5-fn or nil - ;; but I want to just be able to say True or False - :interactive (when interactive (if-not (fn? interactive) (req interactive) interactive)) - :effect (req (resolve-choices xs xs state side eid card target))})))))) - -(defn cost-option - [cost side] - {:cost cost - :ability {:display-side side - :msg :cost}}) - (defn with-revealed-hand "Resolves an ability while a player has their hand revealed (so you can click cards in their hand) You can set the side that triggers the reveal (event-side) and if it displays as a forced reveal diff --git a/src/clj/game/core/diffs.clj b/src/clj/game/core/diffs.clj index 66dfc8af83..a970dfae3a 100644 --- a/src/clj/game/core/diffs.clj +++ b/src/clj/game/core/diffs.clj @@ -6,7 +6,7 @@ [game.core.card :refer :all] [game.core.cost-fns :refer [card-ability-cost]] [game.core.engine :refer [can-trigger?]] - [game.core.effects :refer [is-disabled-reg?]] + [game.core.effects :refer [any-effects is-disabled-reg?]] [game.core.installing :refer [corp-can-pay-and-install? runner-can-pay-and-install?]] [game.core.payment :refer [can-pay? ->c]] @@ -399,6 +399,7 @@ (-> run (assoc :approached-ice-in-position? (when (= :approach-ice (:phase run)) (some? (get-card state (:current-ice run))))) + (assoc :cannot-jack-out (any-effects state :corp :cannot-jack-out true?)) (select-non-nil-keys run-keys)))) (defn encounter-ice-summary diff --git a/src/clj/game/core/effects.clj b/src/clj/game/core/effects.clj index 2152e564c8..dcd1e426e4 100644 --- a/src/clj/game/core/effects.clj +++ b/src/clj/game/core/effects.clj @@ -4,7 +4,7 @@ [game.core.card-defs :refer [card-def]] [game.core.eid :refer [make-eid]] [game.core.board :refer [get-all-cards]] - [game.utils :refer [same-card? to-keyword]])) + [game.utils :refer [remove-once same-card? to-keyword]])) (defn is-disabled-reg? [state card] @@ -139,6 +139,21 @@ (update-disabled-cards state) ability)) +(defn unregister-effect-by-uuid + "Removes a single effect handler with matching uuid" + [state _ {:keys [uuid] :as ability}] + (swap! state assoc :effects (remove-once #(= uuid (:uuid %)) (:effects @state)))) + +(defn update-lingering-effect-durations + "updates all effects with a given duration to have another duration + ie: :until-next-corp-turn-begins -> :until-corp-turn-begins" + [state _ from-key to-key] + (swap! state assoc :effects + (->> (:effects @state) + (map #(if (= (:duration %) from-key) (assoc % :duration to-key) %)) + (into []))) + (update-disabled-cards state)) + (defn unregister-lingering-effects [state _ duration] (swap! state assoc :effects diff --git a/src/clj/game/core/engine.clj b/src/clj/game/core/engine.clj index 403d0d17a0..9d9e06db2d 100644 --- a/src/clj/game/core/engine.clj +++ b/src/clj/game/core/engine.clj @@ -580,6 +580,15 @@ (into []))) (unregister-suppress state side card)))) +(defn update-floating-event-durations + "updates all effects with a given duration to have another duration + ie: :until-next-corp-turn-begins -> :until-corp-turn-begins" + [state _ from-key to-key] + (swap! state assoc :events + (->> (:events @state) + (map #(if (= (:duration %) from-key) (assoc % :duration to-key) %)) + (into [])))) + (defn unregister-floating-events "Removes all event handlers with a non-persistent duration" [state _ duration] @@ -614,11 +623,13 @@ :active (active? card) :derezzed (and (installed? card) (not (rezzed? card))) + :installed (installed? card) :facedown (and (installed? card) (facedown? card)) :faceup (and (installed? card) (faceup? card)) :hosted (:host card) + :floating true :inactive (not (active? card)) :in-location (or (and (contains? location :discard) (in-discard? card)) diff --git a/src/clj/game/core/expose.clj b/src/clj/game/core/expose.clj index 838695f87a..3654c02ca0 100644 --- a/src/clj/game/core/expose.clj +++ b/src/clj/game/core/expose.clj @@ -2,50 +2,39 @@ (:require [game.core.card :refer [rezzed?]] [game.core.card-defs :refer [card-def]] - [game.core.eid :refer [effect-completed make-eid make-result]] - [game.core.engine :refer [resolve-ability trigger-event-sync]] - [game.core.flags :refer [cards-can-prevent? get-prevent-list]] + [game.core.eid :refer [complete-with-result effect-completed make-eid make-result]] + [game.core.effects :refer [any-effects]] + [game.core.engine :refer [checkpoint queue-event register-pending-event resolve-ability trigger-event-sync]] + [game.core.prevention :refer [resolve-expose-prevention]] [game.core.prompts :refer [clear-wait-prompt show-prompt show-wait-prompt]] [game.core.say :refer [system-msg]] [game.core.to-string :refer [card-str]] + [game.utils :refer [enumerate-str]] [game.macros :refer [wait-for]])) -(defn expose-prevent - [state _ n] - (swap! state update-in [:expose :expose-prevent] #(+ (or % 0) n))) - -(defn- resolve-expose - [state side eid target] - (system-msg state side (str "exposes " (card-str state target {:visible true}))) - (if-let [ability (:on-expose (card-def target))] - (wait-for (resolve-ability state side ability target nil) - (trigger-event-sync state side (make-result eid target) :expose target)) - (trigger-event-sync state side (make-result eid target) :expose target))) +(defn resolve-expose + [state side eid targets {:keys [card] :as args}] + (if-not (seq targets) + (effect-completed state side eid) + (do (system-msg state side (str (if-not card "exposes " (str "uses " (:title card) " to expose ")) (enumerate-str (map #(card-str state % {:visible true}) targets)))) + (doseq [t targets] + (when-let [ability (:on-expose (card-def t))] + ;; if it gets rezzed by blackguard or something, the effect shouldn't fizzle + (register-pending-event state :expose t (assoc ability :condition :installed)))) + (queue-event state :expose {:cards targets}) + (wait-for (checkpoint state side {:duration :expose}) + (complete-with-result state side eid {:cards targets}))))) (defn expose - "Exposes the given card." - ([state side target] (expose state side (make-eid state) target)) - ([state side eid target] (expose state side eid target nil)) - ([state side eid target {:keys [unpreventable]}] - (swap! state update :expose dissoc :expose-prevent) - (if (or (rezzed? target) - (nil? target)) - (effect-completed state side eid) ; cannot expose faceup cards - (wait-for (trigger-event-sync state side :pre-expose target) - (let [prevent (get-prevent-list state :corp :expose)] - (if (and (not unpreventable) - (cards-can-prevent? state :corp prevent :expose)) - (do (system-msg state :corp "has the option to prevent a card from being exposed") - (show-wait-prompt state :runner "Corp to prevent the expose") - (show-prompt state :corp nil - (str "Prevent " (:title target) " from being exposed?") ["Done"] - (fn [_] - (clear-wait-prompt state :runner) - (if (get-in @state [:expose :expose-prevent]) - (effect-completed state side (make-result eid false)) - (do (system-msg state :corp "will not prevent a card from being exposed") - (resolve-expose state side eid target)))) - {:prompt-type :prevent})) - (if-not (get-in @state [:expose :expose-prevent]) - (resolve-expose state side eid target) - (effect-completed state side (make-result eid false))))))))) + "Exposes the given cards." + ([state side eid targets] (expose state side eid targets nil)) + ([state side eid targets {:keys [unpreventable] :as args}] + (let [args (assoc args :card (:source eid)) + targets (filterv #(not (or (rezzed? %) + (nil? %) + (any-effects state side :cannot-be-exposed true? %))) + targets)] + (if (empty? targets) + (effect-completed state side eid) ;; cannot expose faceup cards + (wait-for (resolve-expose-prevention state side targets args) + (resolve-expose state side eid (:remaining async-result) args)))))) diff --git a/src/clj/game/core/flags.clj b/src/clj/game/core/flags.clj index e6e31cb692..c56ad7b95b 100644 --- a/src/clj/game/core/flags.clj +++ b/src/clj/game/core/flags.clj @@ -158,9 +158,6 @@ (defn prevent-draw [state _] (swap! state assoc-in [:runner :register :cannot-draw] true)) -(defn prevent-jack-out [state _] - (swap! state assoc-in [:run :cannot-jack-out] true)) - (defn prevent-current [state _] (swap! state assoc-in [:runner :register :cannot-play-current] true)) @@ -324,41 +321,3 @@ "Checks if the specified card is able to be used for a when-scored text ability" [card] (:on-score (card-def card))) - -(defn ab-can-prevent? - "Checks if the specified ability definition should prevent. - Checks for a :req in the :prevent map of the card-def. - Defaults to false if req check not met" - ([state side card req-fn target args] - (ab-can-prevent? state side (make-eid state) card req-fn target args)) - ([state side eid card req-fn target args] - (cond - req-fn (if (req-fn state side eid card (list (assoc args :prevent-target target))) true false) - :else false))) - -(defn get-card-prevention - "Returns card prevent abilities for a given type" - [card type] - (filter #(contains? (:type %) type) - (-> card card-def :interactions :prevent))) - -(defn card-can-prevent? - "Checks if a cards req (truthy test) can be met for this type" - [state side card type target args] - (->> (get-card-prevention card type) - (map #(ab-can-prevent? state side card (:req %) target args)) - (some identity))) - -(defn cards-can-prevent? - "Checks if any cards in a list can prevent this type" - ([state side cards type] (cards-can-prevent? state side cards type nil nil)) - ([state side cards type target args] - (->> cards - (map #(card-can-prevent? state side % type target args)) - (some true?)))) - -(defn get-prevent-list - "Get list of cards that have prevent for a given type" - [state side type] - (filter #(seq (get-card-prevention % type)) - (all-active state side))) diff --git a/src/clj/game/core/moving.clj b/src/clj/game/core/moving.clj index ef5e565ed4..026626f98d 100644 --- a/src/clj/game/core/moving.clj +++ b/src/clj/game/core/moving.clj @@ -9,11 +9,12 @@ [game.core.eid :refer [complete-with-result effect-completed make-eid make-result]] [game.core.engine :as engine :refer [checkpoint dissoc-req register-pending-event queue-event register-default-events register-events should-trigger? trigger-event trigger-event-sync unregister-events]] [game.core.finding :refer [get-scoring-owner]] - [game.core.flags :refer [can-trash? card-flag? cards-can-prevent? get-prevent-list untrashable-while-resources? untrashable-while-rezzed? zone-locked?]] + [game.core.flags :refer [can-trash? card-flag? untrashable-while-resources? untrashable-while-rezzed? zone-locked?]] [game.core.hosting :refer [remove-from-host]] [game.core.ice :refer [get-current-ice set-current-ice update-breaker-strength]] [game.core.initializing :refer [card-init deactivate reset-card]] [game.core.memory :refer [init-mu-cost]] + [game.core.prevention :refer [resolve-trash-prevention]] [game.core.prompts :refer [clear-wait-prompt show-prompt show-wait-prompt]] [game.core.say :refer [enforce-msg system-msg]] [game.core.servers :refer [is-remote? same-server? target-server type->rig-zone]] @@ -263,55 +264,6 @@ (move state side card to)))) ;;; Trashing -(defn trash-prevent - [state _ type n] - (swap! state update-in [:trash :trash-prevent type] (fnil #(+ % n) 0))) - -(defn- prevent-trash-impl - [state side eid {:keys [zone type] :as card} {:keys [unpreventable cause game-trash] :as args}] - (if (and card (not-any? #{:discard} zone)) - (cond - (and (not game-trash) - (untrashable-while-rezzed? card)) - (do (enforce-msg state card "cannot be trashed while installed") - (effect-completed state side eid)) - (and (= side :runner) - (not (can-trash? state side card))) - (do (enforce-msg state card "cannot be trashed") - (effect-completed state side eid)) - (and (= side :corp) - (untrashable-while-resources? card) - (> (count (filter resource? (all-active-installed state :runner))) 1)) - (do (enforce-msg state card "cannot be trashed while there are other resources installed") - (effect-completed state side eid)) - ;; Card is not enforced untrashable - :else - (let [ktype (keyword (string/lower-case type))] - (when (and (not unpreventable) - (not= cause :ability-cost)) - (swap! state update-in [:trash :trash-prevent] dissoc ktype)) - (let [type (->> ktype name (str "trash-") keyword) - prevent (get-prevent-list state :runner type)] - ;; Check for prevention effects - (if (and (not unpreventable) - (not= cause :ability-cost) - (cards-can-prevent? state :runner prevent type card args)) - (do (system-msg state :runner "has the option to prevent trash effects") - (show-wait-prompt state :corp "Runner to prevent trash effects") - (show-prompt state :runner nil - (str "Prevent the trashing of " (:title card) "?") ["Done"] - (fn [_] - (clear-wait-prompt state :corp) - (if-let [_ (get-in @state [:trash :trash-prevent ktype])] - (do (system-msg state :runner (str "prevents the trashing of " (:title card))) - (swap! state update-in [:trash :trash-prevent] dissoc ktype) - (effect-completed state side eid)) - (do (system-msg state :runner (str "will not prevent the trashing of " (:title card))) - (complete-with-result state side eid card)))) - {:prompt-type :prevent})) - ;; No prevention effects: add the card to the trash-list - (complete-with-result state side eid card))))) - (effect-completed state side eid))) (defn update-current-ice-to-trash "If the current ice is going to be trashed, update it with any changes" @@ -326,16 +278,6 @@ (when-let [card (get-card state c)] (assoc card :seen (:seen c)))) -(defn- prevent-trash - ([state side eid cs args] (prevent-trash state side eid cs args [])) - ([state side eid cs args acc] - (if (seq cs) - (wait-for (prevent-trash-impl state side (make-eid state eid) (get-card? state (first cs)) args) - (if-let [card async-result] - (prevent-trash state side eid (rest cs) args (conj acc card)) - (prevent-trash state side eid (rest cs) args acc))) - (complete-with-result state side eid acc)))) - (defn get-trash-effect "Criteria for abilities that trigger when the card is trashed." [state side eid card {:keys [accessed cause cause-card host-trashed]}] @@ -402,57 +344,66 @@ ([state side eid cards {:keys [accessed cause cause-card keep-server-alive game-trash suppress-checkpoint] :as args}] (if (empty? (filter identity cards)) (effect-completed state side eid) - (wait-for - (prevent-trash state side (make-eid state eid) cards args) - (let [trashlist async-result - _ (update-current-ice-to-trash state trashlist)] - (wait-for - (trigger-event-sync state side :pre-trash-interrupt trashlist) - (let [trash-event (get-trash-event side game-trash) - ;; No card should end up in the opponent's discard pile, so instead - ;; of using `side`, we use the card's `:side`. - move-card (fn [card] - (move state (to-keyword (:side card)) card :discard {:keep-server-alive keep-server-alive})) - ;; If the trashed card is installed, update all of the indicies - ;; of the other installed cards in the same location - update-indicies (fn [card] - (when (installed? card) - (update-installed-card-indices state side (:zone card)))) - ;; Perform the move of the cards from their current location to - ;; the discard. At the same time, gather their `:trash-effect`s - ;; to be used in the simult event later. - moved-cards (reduce - (fn [acc card] - (if-let [card (get-card? state card)] - (let [_ (set-duration-on-trash-events state card trash-event) - moved-card (move-card card) - trash-effect (get-trash-effect state side eid card args)] - (update-indicies card) - (conj acc {:moved-card moved-card - :trash-effect trash-effect - :old-card card})) - (conj acc {:old-card card}))) - [] - trashlist)] - (swap! state update-in [:trash :trash-list] dissoc eid) - (when (and side (seq (remove #{side} (map #(to-keyword (:side %)) trashlist)))) - (swap! state assoc-in [side :register :trashed-card] true)) - ;; Pseudo-shuffle archives. Keeps seen cards in play order and shuffles unseen cards. - (swap! state assoc-in [:corp :discard] - (vec (sort-by #(if (:seen %) -1 1) (get-in @state [:corp :discard])))) - (let [eid (make-result eid (vec (keep :moved-card moved-cards)))] - (doseq [{:keys [moved-card trash-effect]} moved-cards - :when trash-effect] - (register-pending-event state trash-event moved-card trash-effect)) - (doseq [{:keys [old-card moved-card]} moved-cards] - (queue-event state trash-event {:card old-card - :moved-card moved-card - :cause cause - :cause-card (trim-cause-card cause-card) - :accessed accessed})) - (if suppress-checkpoint - (effect-completed state nil eid) - (checkpoint state nil eid {:duration trash-event})))))))))) + (wait-for (resolve-trash-prevention state side cards args) + (let [trashlist (:remaining async-result) + ;;_ (println "async result: " trashlist) + _ (update-current-ice-to-trash state (map :card trashlist))] + (wait-for + (trigger-event-sync state side :pre-trash-interrupt (map :card trashlist)) + (let [trash-event (get-trash-event side game-trash) + ;; No card should end up in the opponent's discard pile, so instead + ;; of using `side`, we use the card's `:side`. + move-card (fn [card dest] + (move state (to-keyword (:side card)) card dest {:keep-server-alive keep-server-alive})) + should-shuffle-rd? (some :shuffle-rd trashlist) + ;; If the trashed card is installed, update all of the indicies + ;; of the other installed cards in the same location + update-indicies (fn [card] + (when (installed? card) + (update-installed-card-indices state side (:zone card)))) + ;; Perform the move of the cards from their current location to + ;; the discard. At the same time, gather their `:trash-effect`s + ;; to be used in the simult event later. + moved-cards (reduce + (fn [acc {:keys [card destination]}] + (if-let [card (get-card? state card)] + (let [_ (set-duration-on-trash-events state card trash-event) + moved-card (move-card card destination) + trash-effect (get-trash-effect state side eid card args)] + (update-indicies card) + (conj acc {:moved-card moved-card + :trash-effect trash-effect + :old-card card})) + (conj acc {:old-card card}))) + [] trashlist)] + (when should-shuffle-rd? + ;; foiled by the circular dependency restriction once again + ;; see: shuffling.clj for clarification + (when (and (:access @state) + (:run @state)) + (swap! state assoc-in [:run :shuffled-during-access :rd] true)) + (swap! state update-in [:stats :corp :shuffle-count] (fnil + 0) 1) + (swap! state update-in [:corp :deck] shuffle) + (trigger-event state side :corp-shuffle-deck)) + (swap! state update-in [:trash :trash-list :card] dissoc eid) + (when (and side (seq (remove #{side} (map #(to-keyword (:side %)) (map :card trashlist))))) + (swap! state assoc-in [side :register :trashed-card] true)) + ;; Pseudo-shuffle archives. Keeps seen cards in play order and shuffles unseen cards. + (swap! state assoc-in [:corp :discard] + (vec (sort-by #(if (:seen %) -1 1) (get-in @state [:corp :discard])))) + (let [eid (make-result eid (vec (keep :moved-card moved-cards)))] + (doseq [{:keys [moved-card trash-effect]} moved-cards + :when trash-effect] + (register-pending-event state trash-event moved-card trash-effect)) + (doseq [{:keys [old-card moved-card]} moved-cards] + (queue-event state trash-event {:card old-card + :moved-card moved-card + :cause cause + :cause-card (trim-cause-card cause-card) + :accessed accessed})) + (if suppress-checkpoint + (effect-completed state nil eid) + (checkpoint state nil eid {:duration trash-event})))))))))) (defmethod engine/move* :trash-cards [state side eid _action cards args] (trash-cards state side eid cards args)) diff --git a/src/clj/game/core/payment.clj b/src/clj/game/core/payment.clj index 535061c2ef..02366c1cfb 100644 --- a/src/clj/game/core/payment.clj +++ b/src/clj/game/core/payment.clj @@ -11,11 +11,12 @@ (defn ->c ([type] (->c type 1)) ([type n] (->c type n nil)) - ([type n {:keys [additional stealth] :as args}] + ([type n {:keys [additional stealth maximum] :as args}] {:cost/type type :cost/amount n :cost/additional (boolean additional) :cost/stealth stealth + :cost/maximum maximum :cost/args (not-empty (dissoc args :stealth :additional))})) (defmulti value :cost/type) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj new file mode 100644 index 0000000000..2a9a758333 --- /dev/null +++ b/src/clj/game/core/prevention.clj @@ -0,0 +1,585 @@ +(ns game.core.prevention + (:require + [clojure.set :as set] + [clojure.string :as str] + [game.core.board :refer [all-active all-active-installed]] + [game.core.card :refer [get-card installed? resource? rezzed? same-card?]] + [game.core.card-defs :refer [card-def]] + [game.core.choose-one :refer [choose-one-helper]] + [game.core.cost-fns :refer [card-ability-cost]] + [game.core.eid :refer [complete-with-result effect-completed]] + [game.core.effects :refer [any-effects get-effects]] + [game.core.engine :refer [resolve-ability trigger-event-simult trigger-event-sync]] + [game.core.flags :refer [can-trash? untrashable-while-resources? untrashable-while-rezzed?]] + [game.core.payment :refer [->c can-pay?]] + [game.core.prompts :refer [clear-wait-prompt]] + [game.core.say :refer [enforce-msg]] + [game.core.to-string :refer [card-str]] + [game.utils :refer [dissoc-in enumerate-str quantify]] + [game.macros :refer [msg req wait-for]] + [jinteki.utils :refer [other-side]])) + +;; DOCUMENTATION FOR THE PREVENTION SYSTEM +;; Interrupts/prevention abilities in the CR are awkward, because: +;; 1) despite ncigs going, still have to obey ncigs (have to be relevant to the interrupt) +;; 2) may be a mix of triggered events, static abilities, and paid abilities +;; 3) Static abilities *should* be triggered first, but paid abilities and triggered events +;; may be resolved in any order +;; 4) Some effects interact with paid abilities, so we need to ensure we differentiate between +;; paid abilities and events +;; 5) Some of these may be floating effects from cards that are no longer in play (see leverage) +;; 6) Some effects may only be used a limited number of times, some are repeatable (see prana) +;; +;; Sow how does this system work? +;; * Cards may have a :prevention list in their cdef +;; * This is a list of all prevention abilities on the card +;; * A prevention ability looks like this: +;; +;; :prevention [{:prevents :tag +;; :type :ability +;; :prompt "Trash Decoy to avoid 1 tag?" +;; :ability {:async true +;; :cost [(->c :trash-can)] +;; :msg "avoid 1 tag" +;; :req (req (and (pos? (:remaining context)) +;; (not (:unpreventable context)))) +;; :effect (req (prevent-tag state :runner eid 1))}}]}) +;; +;; KEYS: +;; :prevents - key, what this prevention is for (ie :tag, :damage, :pre-damage, :jack-out) +;; :type - either :ability, or :event, for if this is a paid ability or a triggered event +;; :prompt - string - optional - a prompt to be displayed when you click the button +;; if this isn't supplied, it will instead jump straight into the ability resolution +;; otherwise, it will be an `:optional {:prompt prompt :yes-ability ability}` +;; :label - stribng - optional - the label for the button. If not supplied, it will just be the +;; name of the card (ie "Decoy"). I recommend only using this for handlers that +;; have ambiguity (ie feedback filter, dummy box, caldera, zaibatsu loyalty) as a way +;; of adding additional contextual information for the user. +;; :max-uses - int - optional - is there a maximum number of times this ability can trigger in one +;; instance? See: prana, muresh, cleaners, etc. If blank, +;; then this ability can be used any number of times so long as the req fn allows it +;; :ability - the ability that gets triggered when you chose the prevention. +;; Write it like any other ability. It will hav access to a context map which +;; will typically have: +;; 1) :remaining - either numeric, or sequential, depending on prevention type +;; 2) :count - initial quantity of the thing to prevent (numeric) +;; 3) :prevented - either a numeric count, or the :all key for everything +;; 4) :source-player - which player initiated this event (ie who did damage) +;; 5) :source-card - which card initiated this event (ie scorched earth) +;; 6) :unpreventable - sometimes true, is this unpreventable? +;; 7) :unboostable - almost never true, is this unboostable? +;; +;; FLOATING PREVENTIONS: +;; * register a lingering effect of :type :prevention +;; * :req should enforce the side - ie `:req (req (= :runner side))` +;; * :value is just a prevention map like normal +;; * in the :ability map, add `:condition :floating` so the engine will let us actually fire the +;; ability +;; +;; Note: The `:req` fn of the given :ability is used for computing if the button should +;; show up, as well as wether or not you can pay the cost. + + +(defn- relevant-prevention-abilities + "selects all prevention abilities which are: + 1) relevant to the context + 2) playable (navi mumbai + req) + 3) the player can afford to pay for + 4) haven't been used too many times (ie net shield, prana condenser)" + [state side eid key card] + (let [abs (filter #(= (:prevents %) key) (:prevention (card-def card))) + with-card (map #(assoc % :card card) abs) + playable? (filter #(let [cannot-play? (and (= (:type %) :ability) + (any-effects state side :prevent-paid-ability true? card [(:ability %) 0])) + payable? (can-pay? state side eid card nil (seq (card-ability-cost state side (:ability %) card []))) + ;; todo - account for card being disabled + not-used-too-many-times? (or (not (:max-uses %)) + (not (get-in @state [:prevent key :uses (:cid card)])) + (< (get-in @state [:prevent key :uses (:cid card)]) (:max-uses %))) + ability-req? (or (not (get-in % [:ability :req])) + ((get-in % [:ability :req]) state side eid card [(get-in @state [:prevent key])]))] + (and (not cannot-play?) payable? not-used-too-many-times? ability-req?)) + abs)] + (seq (map #(assoc % :card card) playable?)))) + +(defn- floating-prevention-abilities + [state side eid key] + (let [evs (get-effects state side :prevention) + abs (filter #(= (:prevents %) key) evs) + playable? (filter #(let [payable? (can-pay? state side eid (:card %) nil (seq (card-ability-cost state side (:ability %) (:card %) []))) + not-used-too-many-times? (or (not (:max-uses %)) + (not (get-in @state [:prevent key :uses (:cid (:card %))])) + (< (get-in @state [:prevent key :uses (:cid (:card %))]) (:max-uses %))) + ability-req? (or (not (get-in % [:ability :req])) + ((get-in % [:ability :req]) state side eid (:card %) [(get-in @state [:prevent key])]))] + (and payable? not-used-too-many-times? ability-req?)) + abs)] + (seq playable?))) + + +(defn- gather-prevention-abilities + [state side eid key] + (concat (mapcat #(relevant-prevention-abilities state side eid key %) (all-active state side)) + (floating-prevention-abilities state side eid key))) + +(defn prevent-numeric + [state side eid key n] + (if (get-in @state [:prevent key]) + (do (if (= n :all) + (swap! state update-in [:prevent key] merge {:prevented :all :remaining 0}) + (do (swap! state update-in [:prevent key :prevented] + n) + (swap! state update-in [:prevent key :remaining] #(max 0 (- % n))))) + (trigger-event-sync state side eid (if (= side :corp) :corp-prevent :runner-prevent) {:type key + :amount n})) + (do (println "tried to prevent " (name key) " outside of a " (name key) " prevention window (eid: " eid ")") + (effect-completed state side eid)))) + +(defn- fetch-and-clear! + "get the prevent map for a key and also dissoc it from the state" + [state key] + (let [res (get-in @state [:prevent key])] + (if (seq (:prevent-stack @state)) + (do (swap! state assoc :prevent (first (:prevent-stack @state))) + (swap! state update :prevent-stack rest)) + (swap! state dissoc :prevent)) + res)) + +(defn- push-prevention! + [state key map] + (when (:prevent @state) + (swap! state assoc :prevent-stack (concat [(:prevent @state)] (:prevent-stack @state)))) + (swap! state assoc-in [:prevent key] map)) + +(defn- trigger-prevention + "Triggers an ability as having prevented something" + [state side eid key prevention] + ;; this marks the player as having acted, so we can play the priority game + ;; Note that this requires the following concession: + ;; * All abilities should either use the prompt system set up here + ;; * Or if they do not, clicking the ability MUST act + ;; The consequence of ignoring this is the potential for a silly player to pretend to act, do nothing, and flip priority + (let [abi {:async true + :effect (req (swap! state assoc-in [:prevent key :priority-passes] 0) + (swap! state update-in [:prevent key :uses (->> prevention :card :cid)] (fnil inc 0)) + (resolve-ability state side eid (:ability prevention) card [(get-in @state [:prevent key])]))}] + (resolve-ability + state side (assoc eid :source (:card prevention) :source-type :ability) + (if (:prompt prevention) + {:optional {:prompt (:prompt prevention) + :yes-ability abi}} + abi) + (:card prevention) nil))) + +(defn- build-prevention-option + "Builds a menu item for firing a prevention ability" + [prevention key] + {:option (or (:label prevention) (->> prevention :card :printed-title)) + :ability {:async true + :effect (req (trigger-prevention state side eid key prevention))}}) + +(defn- resolve-keyed-prevention-for-side + [state side eid key {:keys [prompt waiting option data-type] :as args}] + (let [remainder (get-in @state [:prevent key :remaining]) + prompt (if (string? prompt) prompt (prompt state remainder)) + waiting (if (string? waiting) waiting (waiting state remainder)) + option (if (string? option) option (option state remainder))] + (if (or (if (= data-type :sequential) + (not (seq remainder)) + ;; only relevant for damage -> the CR says these numbers can go to (or below) 0 withoutout actually closing the interrupt, + ;; even though most abilities cannot interact with them - just brainchips and muresh bodysuit are relevant + (and (not= key :pre-damage) (not (pos? remainder)))) + (get-in @state [:prevent key :passed])) + (do (swap! state dissoc-in [:prevent key :passed]) + (effect-completed state side eid)) + (let [preventions (gather-prevention-abilities state side eid key)] + (if (empty? preventions) + (effect-completed state side eid) + (if (and (= 1 (count preventions)) + (:mandatory (first preventions))) + (wait-for (trigger-prevention state side key (first preventions)) + (resolve-keyed-prevention-for-side state side eid key args)) + (wait-for (resolve-ability + state side + (choose-one-helper + {:prompt prompt + :waiting-prompt waiting} + (concat (mapv #(build-prevention-option % key) preventions) + [(when-not (some :mandatory preventions) + {:option option + :ability {:effect (req (swap! state assoc-in [:prevent key :passed] true))}})])) + nil nil) + (resolve-keyed-prevention-for-side state side eid key args)))))))) + +(defn resolve-prevent-effects-with-priority + "Resolves prevention effects for a given key, automatically passing priority back and forth while doing so" + [state side eid key prev-fn] + (if (= 2 (get-in @state [:prevent key :priority-passes])) + (complete-with-result state side eid (fetch-and-clear! state key)) + (wait-for (prev-fn state side) + (swap! state update-in [:prevent key :priority-passes] (fnil inc 1)) + (resolve-prevent-effects-with-priority state (other-side side) eid key prev-fn)))) + +(defn preventable? + ([state key] (preventable? (get-in @state [:prevent key]))) + ([{:keys [remaining unpreventable] :as context}] + (and (if (sequential? remaining) + (seq remaining) + (pos? remaining)) + (not unpreventable)))) + +;; TRASH PREVENTION + +(defn prevent-trash-installed-by-type + [label types cost valid-context?] + (letfn [(relevant [state card] + (filter #(and (contains? types (:type %)) + ;; note that because of the way prompts work, you select before the cost is paid, so things like fall guy need this hack + (or (not= cost [(->c :trash-can)]) (not (same-card? card %))) + (installed? %)) + (map :card (get-in @state [:prevent :trash :remaining]))))] + {:prevents :trash + :type :ability + :label label + :ability {:req (req + (and (seq (relevant state card)) + (not (:unpreventable context)) + (valid-context? context) + (can-pay? state side eid card nil cost))) + :async true + :trash-icon (= cost [(->c :trash-can)]) + :effect (req + (wait-for (resolve-ability + state side + (if (= 1 (count (relevant state card))) + {:msg (msg "prevent " (->> (relevant state card) first :title) " from being trashed") + :cost cost + :effect (req (swap! state assoc-in [:prevent :trash :remaining] []))} + {:prompt (str "Choose a " (enumerate-str (map str/lower-case types) "or") " to save from being trashed") + :cost cost + :choices (req (relevant state card)) + :msg (msg "prevent " (:title target) " from being trashed") + :effect (req (swap! state update-in [:prevent :trash :remaining] (fn [s] (filterv #(not (same-card? (:card %) target)) s))))}) + card nil) + (swap! state update-in [:prevent :trash :remaining] (fn [ctx] (filterv #(get-card state (:card %)) ctx))) + (effect-completed state side eid)))}})) + +(defn resolve-trash-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :trash + {:data-type :sequential + :prompt (fn [state remainder] + (if (= side :runner) + (cond + (= 1 (count (get-in @state [:prevent :trash :remaining]))) + (str "Prevent " (->> (get-in @state [:prevent :trash :remaining]) :card :title) " from being trashed?") + (>= 5 (count (get-in @state [:prevent :trash :remaining]))) + (str "Prevent any of " (enumerate-str (sort (map #(->> % :card :title) (get-in @state [:prevent :trash :remaining]))) "or") " from being trashed?") + :else + (str "Prevent any of " (count (get-in @state [:prevent :trash :remaining])) " cards from being trashed?")) + "Choose an interrupt")) ;; note - for corp, this is only marilyn campaign + :waiting "your opponent to resolve trash prevention triggers" + :option (fn [state remainder] (str "Allow " (quantify (count (get-in @state [:prevent :trash :remaining])) "card") " to be trashed"))})) + +(defn resolve-trash-prevention + [state side eid targets {:keys [unpreventable game-trash cause cause-card] :as args}] + (let [untrashable (keep #(cond + (and (not game-trash) + (untrashable-while-rezzed? %)) + [% "cannot be trashed while installed"] + (and (= side :runner) + (not (can-trash? state side %))) + [% "cannot be trashed"] + (and (= side :corp) + (untrashable-while-resources? %) + (> (count (filter resource? (all-active-installed state :runner))) 1)) + [% "cannot be trashed while there are other resources installed"] + :else nil) + targets) + trashable (if untrashable + (vec (set/difference (set targets) (set (map first untrashable)))) + (vec targets)) + untrashable (mapv (fn [[c reason]] {:card c :destination :discard :reason reason}) untrashable) + trashable (mapv (fn [c] {:card c :destination :discard}) trashable)] + (doseq [{:keys [card reason]} untrashable] + (when reason + (enforce-msg state card reason))) + (push-prevention! state :trash + {:count (count trashable) :remaining trashable :untrashable untrashable :prevented 0 :source-player side :source-card cause-card :priority-passes 0 + :type type :unpreventable unpreventable :cause cause :game-trash game-trash :uses {}}) + (if (not (seq trashable)) + (complete-with-result state side eid (fetch-and-clear! state :trash)) + (resolve-prevent-effects-with-priority state (:active-player @state) eid :trash resolve-trash-for-side)))) + +;; DAMAGE PREVENTION + PRE-DAMAGE PREVENTION + +(defn- damage-key + "pre-damage and damage are different events (this is dumb, but it's a concession we have to make)" + [state] + (cond + (get-in @state [:prevent :pre-damage]) + :pre-damage + (get-in @state [:prevent :damage]) + :damage + :else + (do (println "attempt to pick damage key when no damage prevention is active") + nil))) + +(defn damage-type + [state] + (get-in @state [:prevent (damage-key state) :type])) + +(defn damage-pending + [state] + (get-in @state [:prevent (damage-key state) :remaining])) + +(defn damage-boost + [state side eid n] + (when (pos? (damage-pending state)) + (swap! state update-in [:prevent (damage-key state) :remaining] + n)) + (effect-completed state side eid)) + +(defn damage-name + [state] + (case (damage-type state) + :meat "meat" + :brain "core" + :core "core" + :net "net" + "neat")) + +(defn prevent-damage + [state side eid n] + (when (pos? (damage-pending state)) + (if (= n :all) + (swap! state update-in [:prevent (damage-key state)] merge {:remaining 0 :prevented :all}) + (do (swap! state update-in [:prevent (damage-key state) :remaining] #(max 0 (- % n))) + (swap! state update-in [:prevent (damage-key state) :prevented] (fnil #(+ n %) n))))) + (effect-completed state side eid)) + +(defn prevent-up-to-n-damage + [n types] + (letfn [(remainder [state] (get-in @state [:prevent (damage-key state) :remaining])) + (max-to-avoid [state n] (if (= n :all) (remainder state) (min (remainder state) n)))] + {:prompt (msg "Choose how much " (damage-name state) " damage prevent") + :req (req (and (preventable? state (damage-key state)) + (or (not types) + (contains? types (get-in @state [:prevent (damage-key state) :type]))))) + :choices {:number (req (max-to-avoid state n)) + :default (req (max-to-avoid state n))} + :async true + :msg (msg "prevent " target " " (damage-name state) " damage") + :effect (req (prevent-damage state side eid target)) + :cancel-effect (req (prevent-damage state side eid 0))})) + +(defn resolve-pre-damage-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :pre-damage + {:prompt (fn [state remainder] + (if (= side :runner) + (str "Prevent " (damage-pending state) " " (damage-name state) " damage?") + (str "There is " (damage-pending state) " pending " (damage-name state) " damage"))) + :waiting "your opponent to resolve pre-damage triggers" + :option "Pass priority"})) + +;; NOTE - PRE-DAMAGE EFFECTS HAPPEN BEFORE DAMAGE EFFECTS, AND ARE THE CONSTANT ABILITIES (IE GURU DAVINDER, MURESH BODYSUIT, THE CLEANERS, ETC) +;; AND MAY JUST CLOSE THE WINDOW ALL TOGETHER IF ALL DAMAGE IS PREVENTED + +(defn resolve-damage-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :damage + {:prompt (fn [state remainder] + (if (= side :runner) + (str "Prevent " (damage-pending state) " " (damage-name state) " damage?") + (str "There is " (damage-pending state) " pending " (damage-name state) " damage"))) + :waiting "your opponent to resolve damage triggers" + :option "Pass priority"})) + +(defn resolve-damage-prevention + [state side eid type n {:keys [unpreventable unboostable card] :as args}] + (push-prevention! state :pre-damage + {:count n :remaining n :prevented 0 :source-player side :source-card card :priority-passes 0 + :type type :unpreventable unpreventable :unboostable unboostable :uses {}}) + (wait-for (trigger-event-simult state side :pre-damage-flag nil {:card card :type type :count n}) + (wait-for (resolve-prevent-effects-with-priority state (:active-player @state) :pre-damage resolve-pre-damage-for-side) + (swap! state assoc-in [:prevent :damage] async-result) + (swap! state assoc-in [:prevent :damage :priority-passes] 0) + (resolve-prevent-effects-with-priority state (:active-player @state) eid :damage resolve-damage-for-side)))) + +;; ENCOUNTER PREVENTION +(def prevent-encounter + (fn [state side eid] (prevent-numeric state side eid :encounter 1))) + +(defn resolve-encounter-prevention-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :encounter + {:prompt (fn [state remainder] (str "Prevent " (get-in @state [:prevent :encounter :title]) " ability?")) + :waiting "your opponent to prevent a \"when encountered\" ability" + :option (fn [state remainder] (str "Allow " (get-in @state [:prevent :encounter :title])))})) + +(defn resolve-encounter-prevention + [state side eid {:keys [unpreventable card title] :as args}] + (push-prevention! state :encounter + {:count 1 :remaining 1 :title title :prevented 0 :source-player side :source-card card :uses {}}) + (if unpreventable + (complete-with-result state side eid (fetch-and-clear! state :encounter)) + (resolve-prevent-effects-with-priority state (:active-player @state) eid :encounter resolve-encounter-prevention-for-side))) + +;; END RUN PREVENTION +(def prevent-end-run + (fn [state side eid] (prevent-numeric state side eid :end-run 1))) + +(defn- resolve-end-run-prevention-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :end-run + {:prompt "Prevent the run from ending" + :waiting "your opponent to prevent the run from ending" + :option "Allow the run to end"})) + +(defn resolve-end-run-prevention + [state side eid {:keys [unpreventable card] :as args}] + (push-prevention! state :end-run + {:count 1 :remaining 1 :prevented 0 :source-player side :source-card card :uses {}}) + (wait-for + (trigger-event-simult state side :end-run-interrupt nil {:card card :source-eid eid}) + (if unpreventable + (complete-with-result state side eid (fetch-and-clear! state :end-run)) + (resolve-prevent-effects-with-priority state (:active-player @state) eid :end-run resolve-end-run-prevention-for-side)))) + +;; JACK OUT PREVENTION + +(def prevent-jack-out + (fn [state side eid] (prevent-numeric state side eid :jack-out 1))) + +(defn- resolve-jack-out-prevention-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :jack-out + {:prompt "Prevent the runner from jacking out" + :waiting "your opponent to prevent you from jacking out" + :option "Allow the Runner to jack out"})) + +(defn resolve-jack-out-prevention + [state side eid {:keys [unpreventable card] :as args}] + (push-prevention! state :jack-out + {:count 1 :remaining 1 :prevented 0 :source-player side :source-card card :uses {}}) + (if unpreventable + (complete-with-result state side eid (fetch-and-clear! state :jack-out)) + (wait-for + (resolve-jack-out-prevention-for-side state :corp) + (complete-with-result state side eid (fetch-and-clear! state :jack-out))))) + +;; EXPOSE PREVENTION + +(defn prevent-expose + [state side eid card] + (if (get-in @state [:prevent :expose]) + (if (<= (count (get-in @state [:prevent :expose :remaining])) 1) + (do (swap! state update-in [:prevent :expose] merge {:prevented :all :remaining []}) + (trigger-event-sync state side eid (if (= side :corp) :corp-prevent :runner-prevent) {:type :expose + :amount 1})) + (resolve-ability + state side eid + {:prompt "Prevent which card from being exposed?" + :choices (req (sort-by :title (get-in @state [:prevent :expose :remaining]))) + :effect (req (swap! state update-in [:prevent :expose :remaining] (fn [v] (filterv #(not (same-card? % target)) v))) + (swap! state update-in [:prevent :expose :prevented] (fnil inc 0)))} + card nil)) + (do (println "tried to prevent expose outside of an expose prevention window") + (effect-completed state side eid)))) + +(defn resolve-expose-prevention-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :expose + {:data-type :sequential + :prompt (fn [state remainder] (str "Prevent " (enumerate-str (map #(card-str state % {:visible (= side :corp)}) remainder) "or") " from being exposed?")) + :waiting "your opponent to prevent an Expose" + :option (fn [state remainder] (str "Allow " (quantify (count remainder) "card") " to be exposed"))})) + +(defn resolve-expose-prevention + [state side eid targets {:keys [unpreventable card] :as args}] + (push-prevention! state :expose + {:count (count targets) :remaining targets :prevented 0 :source-player side :source-card card :uses {}}) + (wait-for + (trigger-event-simult state side :expose-interrupt nil {:cards targets}) + (let [new-targets (filterv #(not (or (rezzed? %) (nil? %))) (map #(get-card state %) targets))] + (swap! state assoc-in [:prevent :expose :remaining] new-targets) + (swap! state assoc-in [:prevent :expose :count] (count new-targets)) + (if (or unpreventable (not (seq new-targets))) + (complete-with-result state side eid (fetch-and-clear! state :expose)) + (let [active-side (:active-player @state) + responding-side (other-side active-side)] + (wait-for + (resolve-expose-prevention-for-side state active-side) + (wait-for + (resolve-expose-prevention-for-side state responding-side) + (complete-with-result state side eid (fetch-and-clear! state :expose))))))))) + +;; BAD PUBLICITY PREVENTION + +(defn prevent-bad-publicity [state side eid n] (prevent-numeric state side eid :bad-publicity n)) + +(defn resolve-bad-pub-prevention-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :bad-publicity + {:prompt (fn [state remainder] (str "Prevent any of the " (get-in @state [:prevent :bad-publicity :count]) " bad publicity?" + (when-not (= (get-in @state [:prevent :bad-publicity :count]) remainder) + (str "(" remainder " remaining)")))) + :waiting "your opponent to prevent bad publicity" + :option (fn [state remainder] (str "Allow " remainder " bad publicity"))})) + +(defn resolve-bad-pub-prevention + [state side eid n {:keys [unpreventable card] :as args}] + (push-prevention! state :bad-publicity + {:count n :remaining n :prevented 0 :source-player side :source-card card :uses {}}) + (if (or unpreventable (not (pos? n))) + (complete-with-result state side eid (fetch-and-clear! state :bad-publicity)) + (resolve-prevent-effects-with-priority state (:active-player @state) eid :bad-publicity resolve-bad-pub-prevention-for-side))) + +;; TAG PREVENTION + +(defn prevent-tag [state side eid n] (prevent-numeric state side eid :tag n)) + +(defn prevent-up-to-n-tags + [n] + (letfn [(remainder [state] (get-in @state [:prevent :tag :remaining])) + (max-to-avoid [state n] (if (= n :all) (remainder state) (min (remainder state) n)))] + {:prompt "Choose how many tags to avoid" + :req (req (get-in @state [:prevent :tag])) + :choices {:number (req (max-to-avoid state n)) + :default (req (max-to-avoid state n))} + :async true + :msg (msg "avoid " (quantify target "tag")) + :effect (req (prevent-tag state side eid target)) + :cancel-effect (req (prevent-tag state side eid 0))})) + +(defn resolve-tag-prevention-for-side + [state side eid] + (resolve-keyed-prevention-for-side + state side eid :tag + {:prompt (fn [state remainder] (str "Prevent any of the " (get-in @state [:prevent :tag :count]) " tags?" + (when-not (= (get-in @state [:prevent :tag :count]) remainder) + (str "(" remainder " remaining)")))) + :waiting "your opponent to prevent tags" + :option (fn [state remainder] (str "Allow " (quantify remainder "remaining tag")))})) + +(defn resolve-tag-prevention + [state side eid n {:keys [unpreventable card] :as args}] + (push-prevention! state :tag + {:count n :remaining n :prevented 0 :source-player side :source-card card :uses {}}) + (if (or unpreventable (not (pos? n))) + (complete-with-result state side eid (fetch-and-clear! state :tag)) + (wait-for (trigger-event-simult state side :tag-interrupt nil card) + (let [active-side (:active-player @state) + responding-side (other-side active-side)] + (wait-for + (resolve-tag-prevention-for-side state active-side) + (wait-for + (resolve-tag-prevention-for-side state responding-side) + (complete-with-result state side eid (fetch-and-clear! state :tag)))))))) diff --git a/src/clj/game/core/runs.clj b/src/clj/game/core/runs.clj index 33ebcfe825..e2d0512ebf 100644 --- a/src/clj/game/core/runs.clj +++ b/src/clj/game/core/runs.clj @@ -8,11 +8,12 @@ [game.core.effects :refer [any-effects get-effects]] [game.core.eid :refer [complete-with-result effect-completed make-eid make-result]] [game.core.engine :refer [checkpoint end-of-phase-checkpoint register-pending-event pay queue-event resolve-ability trigger-event trigger-event-simult]] - [game.core.flags :refer [can-run? cards-can-prevent? clear-run-register! get-prevent-list prevent-jack-out]] + [game.core.flags :refer [can-run? clear-run-register!]] [game.core.gaining :refer [gain-credits]] [game.core.ice :refer [active-ice? break-subs-event-context get-current-ice get-run-ices update-ice-strength reset-all-ice reset-all-subs! set-current-ice]] [game.core.mark :refer [is-mark?]] [game.core.payment :refer [build-cost-string build-spend-msg can-pay? merge-costs ->c]] + [game.core.prevention :refer [resolve-encounter-prevention resolve-end-run-prevention resolve-jack-out-prevention]] [game.core.prompts :refer [clear-run-prompts clear-wait-prompt show-run-prompts show-prompt show-wait-prompt]] [game.core.say :refer [play-sfx system-msg]] [game.core.servers :refer [is-remote? target-server unknown->kw zone->name]] @@ -324,14 +325,13 @@ [abi ice] {:async true :interactive (req true) - :ability-name (or (:ability-name abi) (str (:title ice) " Ability")) - :effect (req (swap! state assoc-in [:run :prevent-encounter-ability] nil) - (wait-for (trigger-event-simult state :runner :prevent-encounter-ability nil {:ability-name (:ability-name abi)}) - (if (get-in @state [:run :prevent-encounter-ability]) - (effect-completed state side eid) + :ability-name (str (or (:ability-name abi) (:title ice)) " encounter") + :effect (req (wait-for (resolve-encounter-prevention state side {:title (str (or (:ability-name abi) (:title ice)) " encounter") :card ice}) + (if (pos? (:remaining async-result)) (do (register-pending-event state :resolve-ice-encounter-abi ice abi) (queue-event state :resolve-ice-encounter-abi {:ice ice}) - (checkpoint state side eid)))))}) + (checkpoint state side eid)) + (effect-completed state side eid))))}) (defn encounter-ice ;; note: as far as I can tell, this deliberately leaves on open eid (the run eid). @@ -691,10 +691,6 @@ (wait-for (register-successful-run state side (make-phase-eid state nil) (get-in @state [:run :server])) (complete-run state side)))) -(defn end-run-prevent - [state _] - (swap! state update-in [:end-run :end-run-prevent] (fnil inc 0))) - (defn- register-unsuccessful-run [state side eid] (let [run (:run @state)] @@ -712,77 +708,43 @@ (handle-end-run state side eid) (register-unsuccessful-run state side eid)))) -;; todo - ideally we should be able to know not just the card ending the run, but the cause as well -;; ie subroutine, card ability (like the trash on bc), or something else -;; this matters for cards like banner (defn end-run "After checking for prevents, end this run, and set it as UNSUCCESSFUL." ([state side eid card] (end-run state side eid card nil)) ([state side eid card {:keys [unpreventable] :as args}] (if (or (:run @state) (get-current-encounter state)) - (do (swap! state update-in [:end-run] dissoc :end-run-prevent) - (let [prevent (get-prevent-list state :runner :end-run) - auto-prevent (any-effects state side :auto-prevent-run-end true? card [card])] - (if auto-prevent - (do (end-run-prevent state side) - (system-msg state (other-side side) "prevents the run from ending") - (effect-completed state side eid)) - (if (and (not unpreventable) - (cards-can-prevent? state :runner prevent :end-run nil {:card-cause card})) - (do (system-msg state :runner "has the option to prevent the run from ending") - (show-wait-prompt state :corp "Runner to prevent the run from ending") - (show-prompt state :runner nil - (str "Prevent the run from ending?") ["Done"] - (fn [_] - (clear-wait-prompt state :corp) - (if-let [_ (get-in @state [:end-run :end-run-prevent])] - (effect-completed state side eid) - (do (system-msg state :runner "will not prevent the run from ending") - (resolve-end-run state side eid)))) - {:prompt-type :prevent})) - (resolve-end-run state side eid))))) + (wait-for (resolve-end-run-prevention state side (assoc args :card card)) + (if (pos? (:remaining async-result)) + (resolve-end-run state side eid) + (effect-completed state side eid))) (effect-completed state side eid)))) -(defn jack-out-prevent - [state side] - (swap! state update-in [:jack-out :jack-out-prevent] (fnil inc 0)) - (prevent-jack-out state side)) - (defn- resolve-jack-out [state side eid] (queue-event state :jack-out nil) (system-msg state side "jacks out") - (end-run state side eid {:unpreventable true})) + (end-run state side eid nil {:unpreventable true})) (defn jack-out "The runner decides to jack out." ([state side eid] - (swap! state update-in [:jack-out] dissoc :jack-out-prevent) - (let [cost (jack-out-cost state side)] - (if (can-pay? state side eid nil "jack out" cost) - (wait-for (pay state :runner nil cost) - (if-let [payment-str (:msg async-result)] - (let [prevent (get-prevent-list state :corp :jack-out)] - (if (cards-can-prevent? state :corp prevent :jack-out) - (do (system-msg state :runner (str (build-spend-msg payment-str "attempt to" "attempts to") "jack out")) - (system-msg state :corp "has the option to prevent the Runner from jacking out") - (show-wait-prompt state :runner "Corp to prevent the jack out") - (show-prompt state :corp nil - (str "Prevent the Runner from jacking out?") ["Done"] - (fn [_] - (clear-wait-prompt state :runner) - (if-let [_ (get-in @state [:jack-out :jack-out-prevent])] - (effect-completed state side (make-result eid false)) - (do (system-msg state :corp "will not prevent the Runner from jacking out") - (resolve-jack-out state side eid)))) - {:prompt-type :prevent})) - (do (when-not (string/blank? payment-str) - (system-msg state :runner (str payment-str " to jack out"))) - (resolve-jack-out state side eid)))) - (complete-with-result state side eid false))) - (do (system-msg state :runner (str "attempts to jack out but can't pay (" (build-cost-string cost) ")")) - (complete-with-result state side eid false)))))) + (if (any-effects state side :cannot-jack-out true?) + (do (system-msg state :runner "cannot jack out this run") + (complete-with-result state side eid false)) + (let [cost (jack-out-cost state side)] + (if (can-pay? state side eid nil "jack out" cost) + (wait-for (pay state :runner nil cost) + (if-let [payment-str (:msg async-result)] + (do (when-not (string/blank? payment-str) + (system-msg state :runner (str payment-str " to jack out"))) + (wait-for (resolve-jack-out-prevention state side nil) + (if (pos? (:remaining async-result)) + (resolve-jack-out state side eid) + (complete-with-result state side eid false)))) + (complete-with-result state side eid false))) + (do (system-msg state :runner (str "attempts to jack out but can't pay (" (build-cost-string cost) ")")) + (complete-with-result state side eid false))))))) (defn- run-end-fx [state side {:keys [eid successful unsuccessful]}] diff --git a/src/clj/game/core/shuffling.clj b/src/clj/game/core/shuffling.clj index 69d7358e10..0e144bd276 100644 --- a/src/clj/game/core/shuffling.clj +++ b/src/clj/game/core/shuffling.clj @@ -49,8 +49,8 @@ :effect (req (doseq [c targets] (move state side c :deck)) (shuffle! state side :deck)) - :cancel-effect (req - (system-msg state side (str " uses " (:title card) " to shuffle R&D")) + :cancel-effect (req + (system-msg state side (str " uses " (:title card) " to shuffle R&D")) (shuffle! state side :deck) (effect-completed state side eid))} card nil))) diff --git a/src/clj/game/core/tags.clj b/src/clj/game/core/tags.clj index 0f24756d43..19bf771f3d 100644 --- a/src/clj/game/core/tags.clj +++ b/src/clj/game/core/tags.clj @@ -3,8 +3,8 @@ [game.core.effects :refer [any-effects sum-effects]] [game.core.eid :refer [effect-completed make-eid]] [game.core.engine :refer [trigger-event trigger-event-simult trigger-event-sync queue-event checkpoint]] - [game.core.flags :refer [cards-can-prevent? get-prevent-list]] [game.core.gaining :refer [deduct gain]] + [game.core.prevention :refer [resolve-tag-prevention]] [game.core.prompts :refer [clear-wait-prompt show-prompt show-wait-prompt]] [game.core.say :refer [system-msg]] [game.core.toasts :refer [toast]] @@ -35,20 +35,6 @@ :is-tagged is-tagged?})) changed?))) -(defn tag-prevent - [state side eid n] - (swap! state update-in [:tag :tag-prevent] (fnil #(+ % n) 0)) - (trigger-event-sync state side eid (if (= side :corp) :corp-prevent :runner-prevent) {:type :tag - :amount n})) - -(defn- number-of-tags-to-gain - "Calculates the number of tags to give, taking into account prevention and boosting effects." - [state _ n {:keys [unpreventable unboostable]}] - (-> n - (+ (or (when-not unboostable (get-in @state [:tag :tag-bonus])) 0)) - (- (or (when-not unpreventable (get-in @state [:tag :tag-prevent])) 0)) - (max 0))) - (defn- resolve-tag "Resolve runner gain tags. Always gives `:base` tags." [state side eid {:keys [card n suppress-checkpoint]}] @@ -58,45 +44,22 @@ (update-tag-status state) (queue-event state :runner-gain-tag {:side side :cause-card (select-keys card [:cid :title]) - :amount n}) - (if suppress-checkpoint - (effect-completed state nil eid) - (checkpoint state eid))) - (effect-completed state nil eid))) + :amount n})) + (queue-event state :runner-prevents-all-tags {:side side + :cause-card card})) + (if suppress-checkpoint + (effect-completed state nil eid) + (checkpoint state eid))) (defn gain-tags "Attempts to give the runner n tags, allowing for boosting/prevention effects." ([state side eid n] (gain-tags state side eid n nil)) ([state side eid n {:keys [unpreventable card suppress-checkpoint] :as args}] - (swap! state update :tag dissoc :tag-bonus :tag-prevent) - (wait-for (trigger-event-simult state side :pre-tag nil card) - (let [n (number-of-tags-to-gain state side n args) - prevent (get-prevent-list state :runner :tag)] - (if (and (pos? n) - (not unpreventable) - (cards-can-prevent? state :runner prevent :tag)) - (do (system-msg state :runner "has the option to avoid tags") - (show-wait-prompt state :corp "Runner to prevent tags") - (swap! state assoc-in [:prevent :current] :tag) - (show-prompt - state :runner nil - (str "Avoid " (when (< 1 n) "any of the ") (quantify n "tag") "?") ["Done"] - (fn [_] - (let [prevent (get-in @state [:tag :tag-prevent]) - prevent-msg (if prevent - (str "avoids " - (if (= prevent Integer/MAX_VALUE) "all" prevent) - (pluralize prevent "tag")) - "will not avoid tags")] - (system-msg state :runner prevent-msg) - (clear-wait-prompt state :corp) - (resolve-tag state side eid {:suppress-checkpoint suppress-checkpoint - :card card - :n (max 0 (- n (or prevent 0)))}))) - {:prompt-type :prevent})) - (resolve-tag state side eid {:suppress-checkpoint suppress-checkpoint - :card card - :n n})))))) + (wait-for + (resolve-tag-prevention state side n args) + (resolve-tag state side eid {:suppress-checkpoint suppress-checkpoint + :card card + :n (:remaining async-result)})))) (defn lose-tags "Always removes `:base` tags" diff --git a/src/clj/game/core/turns.clj b/src/clj/game/core/turns.clj index 737308ea23..dba611bbaa 100644 --- a/src/clj/game/core/turns.clj +++ b/src/clj/game/core/turns.clj @@ -4,9 +4,9 @@ [game.core.board :refer [all-active all-active-installed all-installed all-installed-and-scored]] [game.core.card :refer [facedown? get-card has-subtype? in-hand? installed?]] [game.core.drawing :refer [draw]] - [game.core.effects :refer [unregister-lingering-effects any-effects]] + [game.core.effects :refer [unregister-lingering-effects update-lingering-effect-durations any-effects]] [game.core.eid :refer [effect-completed make-eid]] - [game.core.engine :refer [trigger-event trigger-event-simult unregister-floating-events]] + [game.core.engine :refer [trigger-event trigger-event-simult unregister-floating-events update-floating-event-durations]] [game.core.flags :refer [card-flag-fn? clear-turn-register!]] [game.core.gaining :refer [gain lose]] [game.core.hand-size :refer [hand-size]] @@ -41,6 +41,11 @@ (unregister-floating-events state side :start-of-turn) (unregister-lingering-effects state side (if (= side :corp) :until-corp-turn-begins :until-runner-turn-begins)) (unregister-floating-events state side (if (= side :corp) :until-corp-turn-begins :until-runner-turn-begins)) + (if (= side :corp) + (do (update-lingering-effect-durations state side :until-next-corp-turn-begins :until-corp-turn-begins) + (update-floating-event-durations state side :until-next-corp-turn-begins :until-corp-turn-begins)) + (do (update-lingering-effect-durations state side :until-next-runner-turn-begins :until-runner-turn-begins) + (update-floating-event-durations state side :until-next-runner-turn-begins :until-runner-turn-begins))) (if (= side :corp) (do (system-msg state side "makes [their] mandatory start of turn draw") (wait-for (draw state side 1 nil) @@ -147,6 +152,11 @@ (unregister-floating-events state side :end-of-next-run) (unregister-lingering-effects state side (if (= side :runner) :until-runner-turn-ends :until-corp-turn-ends)) (unregister-floating-events state side (if (= side :runner) :until-runner-turn-ends :until-corp-turn-ends)) + (if (= side :corp) + (do (update-lingering-effect-durations state side :until-next-corp-turn-ends :until-corp-turn-ends) + (update-floating-event-durations state side :until-next-corp-turn-ends :until-corp-turn-ends)) + (do (update-lingering-effect-durations state side :until-next-runner-turn-ends :until-runner-turn-ends) + (update-floating-event-durations state side :until-next-runner-turn-ends :until-runner-turn-ends))) (clean-set-aside! state side) (doseq [card (all-active-installed state :runner)] ;; Clear :installed :this-turn as turn has ended diff --git a/src/clj/game/utils.clj b/src/clj/game/utils.clj index 061e70d7d1..47325bbd13 100644 --- a/src/clj/game/utils.clj +++ b/src/clj/game/utils.clj @@ -117,10 +117,11 @@ "Joins a collection to a string, seperated by commas and 'and' in front of the last item. If collection only has one item, justs returns that item without seperators. Returns an empty string if coll is empty." - [strings] - (if (<= (count strings) 2) - (str/join " and " strings) - (str (apply str (interpose ", " (butlast strings))) ", and " (last strings)))) + ([strings] (enumerate-str strings "and")) + ([strings sep] + (if (<= (count strings) 2) + (str/join (str " " sep " ") strings) + (str (apply str (interpose ", " (butlast strings))) (str ", " sep " ") (last strings))))) (defn in-coll? "true if coll contains elm" diff --git a/test/clj/game/cards/agendas_test.clj b/test/clj/game/cards/agendas_test.clj index 52933229a7..80295730bd 100644 --- a/test/clj/game/cards/agendas_test.clj +++ b/test/clj/game/cards/agendas_test.clj @@ -2070,40 +2070,30 @@ (deftest labyrinthine-servers ;; Labyrinthine Servers (do-game - (new-game {:corp {:deck [(qty "Labyrinthine Servers" 2)]}}) - (play-and-score state "Labyrinthine Servers") + (new-game {:corp {:deck [(qty "Labyrinthine Servers" 1)]}}) (play-and-score state "Labyrinthine Servers") (take-credits state :corp) - (let [ls1 (get-scored state :corp 0) - ls2 (get-scored state :corp 1)] + (let [ls1 (get-scored state :corp 0)] (is (= 2 (get-counters (refresh ls1) :power))) - (is (= 2 (get-counters (refresh ls2) :power))) (testing "Don't use token" (run-on state "HQ") (run-jack-out state) (is (:run @state) "Jack out prevent prompt") - (click-prompt state :corp "Done") + (click-prompt state :corp "Allow the Runner to jack out") (is (not (:run @state)) "Corp does not prevent the jack out, run ends")) (testing "Use token" (run-on state "HQ") (run-jack-out state) - (card-ability state :corp ls1 0) - (card-ability state :corp ls2 0) - (card-ability state :corp ls1 0) - (click-prompt state :corp "Done") + (click-prompt state :corp "Labyrinthine Servers") (is (:run @state) "Jack out prevented, run is still ongoing") - (is (get-in @state [:run :cannot-jack-out]) "Cannot jack out flag is in effect") (run-continue state) (is (not (:run @state)))) - (testing "one Labyrinthine is empty but the other still has one token, ensure prompt still occurs" - (is (zero? (get-counters (refresh ls1) :power))) - (is (= 1 (get-counters (refresh ls2) :power))) + (testing "one counter left" + (is (= 1 (get-counters (refresh ls1) :power))) (run-on state "HQ") (run-jack-out state) (is (:run @state)) - (card-ability state :corp ls2 0) - (click-prompt state :corp "Done") - (is (get-in @state [:run :cannot-jack-out])) + (click-prompt state :corp "Labyrinthine Servers") (run-continue state) (is (not (:run @state)))) (testing "No more tokens" @@ -3582,6 +3572,23 @@ (click-card state :corp "Obokata Protocol") (is (= 5 (:agenda-point (get-corp))) "3+1+1 agenda points from obo + regen + regen"))) +(deftest regenesis-vs-marilyn-campaign-trash-replacement + ;; Regenesis - if no cards have been added to discard, reveal a face-down agenda + ;; and add it to score area + (do-game + (new-game {:corp {:deck ["Regenesis" "Marilyn Campaign"] + :discard ["Obokata Protocol"]}}) + (play-from-hand state :corp "Marilyn Campaign" "New remote") + (rez state :corp (get-content state :remote1 0)) + (dotimes [_ 4] + (take-credits state :corp) + (take-credits state :runner)) + ;; marilyn should pop now + (click-prompt state :corp "Shuffle Marilyn Campaign into R&D") + (play-and-score state "Regenesis") + (click-card state :corp "Obokata Protocol") + (is (= 4 (:agenda-point (get-corp))) "3+1 agenda points from obo + regen"))) + (deftest regenesis-not-affected-by-subliminal-messaging ;; Regenesis - Leaving Subliminal Messaging in Archives doesn't interfere (do-game @@ -4035,13 +4042,14 @@ (rez state :corp viktor) (run-continue state) (card-subroutine state :corp viktor 0) - (click-prompt state :runner "Done") ;; Don't prevent the brain damage + (click-prompt state :runner "Pass priority") ;; Don't prevent the brain damage (is (= 1 (count (:discard (get-runner))))) (is (= 1 (:brain-damage (get-runner)))) - (click-prompt state :runner "Done") ;; So we take the net, but don't prevent it either + (click-prompt state :runner "Pass priority") ;; So we take the net, but don't prevent it either (is (= 2 (count (:discard (get-runner))))) (card-subroutine state :corp viktor 0) - (card-ability state :runner ff 1) ;; Prevent the brain damage this time + (click-prompt state :runner "Feedback Filter (Core)") + (click-prompt state :runner "1") ;; Prevent the brain damage this time (is (= 3 (count (:discard (get-runner)))) "Feedback filter trashed, didn't take another net damage") (is (= 1 (:brain-damage (get-runner))))))) diff --git a/test/clj/game/cards/assets_test.clj b/test/clj/game/cards/assets_test.clj index 6e1d8f6809..48af43a9af 100644 --- a/test/clj/game/cards/assets_test.clj +++ b/test/clj/game/cards/assets_test.clj @@ -435,11 +435,9 @@ (play-from-hand state :runner "Feedback Filter") (take-credits state :runner) (let [ff (get-hardware state 0)] - (is (= 2 (count (:prompt (get-runner)))) "Runner has a single damage prevention prompt") - (card-ability state :runner ff 0) + (click-prompt state :runner "Feedback Filter (Net)") (is (zero? (count (:discard (get-runner)))) "Runner prevented damage") - (is (= 2 (count (:prompt (get-runner)))) "Runner has a next damage prevention prompt") - (click-prompt state :runner "Done") + (click-prompt state :runner "Pass priority") (is (= 1 (count (:discard (get-runner)))) "Runner took 1 net damage")))) (deftest bioroid-work-crew @@ -3353,21 +3351,13 @@ (play-from-hand state :corp "Marilyn Campaign" "New remote") (let [marilyn (get-content state :remote1 0)] (rez state :corp marilyn) - (is (= 8 (get-counters (refresh marilyn) :credit)) "Marilyn Campaign should start with 8 credits") (is (zero? (-> (get-corp) :deck count)) "R&D should be empty") - (take-credits state :corp) - (take-credits state :runner) - (is (= 6 (get-counters (refresh marilyn) :credit)) "Marilyn Campaign should lose 2 credits start of turn") - (take-credits state :corp) - (take-credits state :runner) - (is (= 4 (get-counters (refresh marilyn) :credit)) "Marilyn Campaign should lose 2 credits start of turn") - (take-credits state :corp) - (take-credits state :runner) - (is (= 2 (get-counters (refresh marilyn) :credit)) "Marilyn Campaign should lose 2 credits start of turn") - (take-credits state :corp) - (take-credits state :runner) - (is (zero? (get-counters (refresh marilyn) :credit)) "Marilyn Campaign should lose 2 credits start of turn") - (click-prompt state :corp "Yes") + (doseq [cr [8 6 4 2 0]] + (is (= cr (get-counters (refresh marilyn) :credit)) (str "Marilyn Campaign should have " cr " credits")) + (when-not (= 0 cr) + (take-credits state :corp) + (take-credits state :runner))) + (click-prompt state :corp "Shuffle Marilyn Campaign into R&D") (is (= 1 (-> (get-corp) :hand count)) "HQ should have 1 card in it, after mandatory draw") (is (= "Marilyn Campaign" (-> (get-corp) :hand first :title)) "Marilyn Campaign should be in HQ, after mandatory draw")))) @@ -3779,16 +3769,16 @@ (is (= 1 (count (:hand (get-corp)))) "Corp hand size is 1 before run") (run-empty-server state "Server 1") (click-prompt state :corp "Yes") ; Ghost Branch ability - (card-ability state :runner nach 0) + (click-prompt state :runner "New Angeles City Hall") + (click-prompt state :runner "Yes") (click-prompt state :corp "Yes") ; Draw from Net Analytics - (click-prompt state :runner "Done") (click-prompt state :runner "No action") (is (no-prompt? state :runner) "Runner waiting prompt is cleared") (is (zero? (count-tags state)) "Avoided 1 Ghost Branch tag") (is (= 2 (count (:hand (get-corp)))) "Corp draw from NA") ; tag removal (gain-tags state :runner 1) - (click-prompt state :runner "Done") ; Don't prevent the tag + (click-prompt state :runner "Allow 1 remaining tag") ; Don't prevent the tag (remove-tag state :runner) (click-prompt state :corp "Yes") ; Draw from Net Analytics (is (= 3 (count (:hand (get-corp)))) "Corp draw from NA")))) @@ -4160,18 +4150,18 @@ (play-from-hand state :corp "Neural EMP") (let [corp-credits (:credit (get-corp))] (is (= 5 (count (:hand (get-runner)))) "No damage dealt") - (card-ability state :corp (refresh pc) 0) + (click-prompt state :corp "Prāna Condenser") (is (= 1 (get-counters (refresh pc) :power)) "Added 1 power counter") (is (= (+ 3 corp-credits) (:credit (get-corp))) "Gained 3 credits") (play-from-hand state :corp "Neural EMP") (is (= 5 (count (:hand (get-runner)))) "No damage dealt") - (card-ability state :corp pc 0) + (click-prompt state :corp "Prāna Condenser") (is (= 2 (get-counters (refresh pc) :power)) "Added another power counter") (is (= (+ 4 corp-credits) (:credit (get-corp))) "Gained another 3 credits (and paid 2 for EMP)") (is (= 5 (count (:hand (get-runner)))) "No damage dealt")) (take-credits state :corp) (take-credits state :runner) - (card-ability state :corp pc 1) + (card-ability state :corp pc 0) (is (= 3 (count (:hand (get-runner)))) "2 damage dealt")))) (deftest prana-condenser-refuse-to-prevent-damage @@ -4188,7 +4178,7 @@ (play-from-hand state :corp "Neural EMP") (let [corp-credits (:credit (get-corp))] (is (= 5 (count (:hand (get-runner)))) "No damage dealt") - (click-prompt state :corp "Done") + (click-prompt state :corp "Pass priority") (is (= 4 (count (:hand (get-runner)))) "1 net damage dealt") (is (= 0 (get-counters (refresh pc) :power)) "No power counter added") (is (= corp-credits (:credit (get-corp))) "No credits gained"))))) @@ -4206,7 +4196,7 @@ (play-from-hand state :runner "Caldera") (is (= 4 (count (:hand (get-runner)))) "Runner starts with 4 cards in grip") (run-empty-server state :hq) - (card-ability state :runner (get-resource state 0) 0) + (click-prompt state :runner "Caldera") (is (= 4 (count (:hand (get-runner)))) "Runner took no damage") (is (no-prompt? state :corp) "No Prana prompt for Corp")))) @@ -4260,7 +4250,7 @@ (play-from-hand state :runner "PAD Tap") (play-from-hand state :runner "PAD Tap") (take-credits state :runner) - (card-ability state :corp (refresh pc) 0) + (click-prompt state :corp "Prāna Condenser") (is (= 9 (:credit (get-runner))) "Runner gained 3 credits from Prana")))) (deftest primary-transmission-dish @@ -4559,7 +4549,7 @@ (end-phase-12 state :corp) (is (= 2 (-> (prompt-map :corp) :choices count)) "Corp should have two abilities to trigger") (click-prompt state :corp "Marilyn Campaign") - (click-prompt state :corp "Yes") + (click-prompt state :corp "Shuffle Marilyn Campaign into R&D") (is (find-card "Marilyn Campaign" (:deck (get-corp)))) (is (zero? (-> (get-corp) :hand count)) "Corp should have 3 cards in hand") (click-prompt state :corp "Yes") @@ -6702,15 +6692,14 @@ (is (some? (prompt-map :corp)) "Corp should get the option to rez Zaibatsu Loyalty before expose") (click-prompt state :corp "Yes") (is (rezzed? (refresh zai)) "Zaibatsu Loyalty should be rezzed") - (let [credits (:credit (get-corp))] - (card-ability state :corp zai 0) - (is (= (dec credits) (:credit (get-corp))) "Corp should lose 1 credit for stopping the expose") - (click-prompt state :corp "Done")) + (is (changed? [(:credit (get-corp)) -1] + (click-prompt state :corp "1 [Credit]: Zaibatsu Loyalty"))) (card-ability state :runner code 0) (click-card state :runner (refresh iw)) (is (some? (prompt-map :corp)) "Corp should be prompted to prevent") (is (zero? (-> (get-corp) :discard count)) "No trashed cards") - (card-ability state :corp zai 1) + (is (changed? [(:credit (get-corp)) 0] + (click-prompt state :corp "[trash]: Zaibatsu Loyalty"))) (is (= 1 (-> (get-corp) :discard count)) "Zaibatsu Loyalty should be in discard after using ability")))) (deftest zealous-judge diff --git a/test/clj/game/cards/events_test.clj b/test/clj/game/cards/events_test.clj index bc8b029b4d..e94547c9a6 100644 --- a/test/clj/game/cards/events_test.clj +++ b/test/clj/game/cards/events_test.clj @@ -52,11 +52,10 @@ (play-run-event state "Account Siphon" :hq) (click-prompt state :runner "Account Siphon") (is (= 4 (:credit (get-runner))) "Runner still has 4 credits due to BP") - (card-ability state :runner nach 0) + (click-prompt state :runner "New Angeles City Hall") + (click-prompt state :runner "Yes") (is (= 2 (:credit (get-runner))) "Runner has 2 credits left") - (card-ability state :runner nach 0) - (is (zero? (:credit (get-runner))) "Runner has no credits left") - (click-prompt state :runner "Done")) + (click-prompt state :runner "Yes")) (is (zero? (count-tags state)) "Runner did not take any tags") (is (= 10 (:credit (get-runner))) "Runner gained 10 credits") (is (= 3 (:credit (get-corp))) "Corp lost 5 credits"))) @@ -2935,13 +2934,12 @@ (play-from-hand state :runner "Falsified Credentials") (click-prompt state :runner "Agenda") (click-card state :runner atl) - (click-prompt state :corp "Done") + (click-prompt state :corp "Allow 1 card to be exposed") (is (= 9 (:credit (get-runner))) "An unprevented expose gets credits") (play-from-hand state :runner "Falsified Credentials") (click-prompt state :runner "Agenda") (click-card state :runner atl) - (card-ability state :corp (refresh zaibatsu) 0) ; prevent the expose! - (click-prompt state :corp "Done") + (click-prompt state :corp "1 [Credit]: Zaibatsu Loyalty") (is (= 8 (:credit (get-runner))) "A prevented expose does not")))) (deftest fear-the-masses @@ -3731,7 +3729,7 @@ (play-from-hand state :runner "Infiltration") (click-prompt state :runner "Expose a card") (click-card state :runner "Ice Wall") - (is (last-log-contains? state "Runner exposes Ice Wall protecting HQ at position 0") + (is (last-log-contains? state "Runner uses Infiltration to expose Ice Wall protecting HQ at position 0") "Infiltration properly exposes the ice"))) (deftest information-sifting-hudson-interaction-max-access @@ -5014,8 +5012,9 @@ (play-from-hand state :corp "SEA Source") (click-prompt state :corp "0") (click-prompt state :runner "0") - (card-ability state :runner (-> (get-resource state 0) :hosted first) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "On the Lam") + (click-prompt state :runner "Yes") + (click-prompt state :runner "1") (is (zero? (count-tags state)) "Runner should avoid tag") (is (= 1 (-> (get-runner) :discard count)) "Runner should have 1 card in Heap"))) @@ -5031,7 +5030,9 @@ (click-card state :runner (get-resource state 0)) (take-credits state :runner) (play-and-score state "Show of Force") - (card-ability state :runner (-> (get-resource state 0) :hosted first) 1) + (click-prompt state :runner "On the Lam") + (click-prompt state :runner "Yes") + (click-prompt state :runner "2") (is (zero? (count-tags state)) "Runner should avoid all meat damage") (is (= 1 (-> (get-runner) :discard count)) "Runner should have 1 card in Heap"))) @@ -5060,8 +5061,9 @@ (play-from-hand state :corp "SEA Source") (click-prompt state :corp "0") (click-prompt state :runner "0") - (card-ability state :runner (-> (get-runner-facedown state 0) :hosted first) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "On the Lam") + (click-prompt state :runner "Yes") + (click-prompt state :runner "1") (is (zero? (count-tags state)) "Runner should avoid tag"))) (deftest out-of-the-ashes-happy-path @@ -6719,7 +6721,7 @@ (take-credits state :corp) (play-from-hand state :runner "Spot the Prey") (click-card state :runner "Hostile Takeover") - (is (last-log-contains? state "Runner exposes Hostile Takeover")) + (is (last-log-contains? state "Runner uses Spot the Prey to expose Hostile Takeover")) (click-prompt state :runner "HQ") (is (:run @state) "Run should be initiated"))) @@ -7132,25 +7134,21 @@ (deftest the-noble-path ;; The Noble Path - Prevents damage during run (do-game - (new-game {:runner {:deck ["The Noble Path" (qty "Sure Gamble" 2)]}}) - (let [hand-count #(count (:hand (get-runner)))] - (starting-hand state :runner ["The Noble Path" "Sure Gamble"]) - (take-credits state :corp) - ;; Play The Noble Path and confirm it trashes remaining cards in hand - (is (= 2 (hand-count)) "Start with 2 cards") - (play-from-hand state :runner "The Noble Path") - (is (zero? (hand-count)) "Playing Noble Path trashes the remaining cards in hand") - ;; Put a card into hand so I can confirm it's not discarded by damage - ;; Don't want to dealing with checking damage on a zero card hand - (starting-hand state :runner ["Sure Gamble"]) - (damage state :runner :net 1) - (is (= 1 (hand-count)) "Damage was prevented") + (new-game {:runner {:hand ["The Noble Path" "Sports Hopper" "Sure Gamble"] + :deck [(qty "Sure Gamble" 2)]}}) + (take-credits state :corp) + (play-from-hand state :runner "Sports Hopper") + (play-from-hand state :runner "The Noble Path") + (is (zero? (count (:hand (get-runner)))) "Playing Noble Path trashes the remaining cards in hand") + (click-prompt state :runner "HQ") + (card-ability state :runner (get-hardware state 0) 0) + (damage state :runner :net 2) + (is (= 2 (count (:hand (get-runner)))) "Damage was prevented") ;; Finish the run and check that damage works again - (click-prompt state :runner "HQ") - (run-continue state) - (click-prompt state :runner "No action") - (damage state :runner :net 1) - (is (zero? (hand-count)) "Damage works again after run")))) + (run-continue state) + (click-prompt state :runner "No action") + (damage state :runner :net 2) + (is (zero? (count (:hand (get-runner)))) "Damage works again after run"))) (deftest the-price ;; trash the top 4, install one paying 3 less diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index 1e3e53259b..b61643d0e6 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -32,18 +32,14 @@ (play-from-hand state :runner "LLDS Energy Regulator") (core/add-counter state :runner (core/make-eid state) (get-program state 0) :virus 3) (take-credits state :runner) - (let [llds (get-program state 1)] - (is (changed? [(:credit (get-runner)) 0] - (purge state :corp) - (click-prompt state :runner "Yes")) - "Runner didn't get credits before deciding on LLDS") - (is (changed? [(:credit (get-runner)) -3] - (card-ability state :runner (refresh llds) 0)) - "Runner pays 3 for LLDS") - (is (changed? [(:credit (get-runner)) 3] - (click-prompt state :runner "Done")) - "Runner got Acacia credits") - (is (zero? (count (:discard (get-runner)))) "Acacia has not been trashed")))) + (is (changed? [(:credit (get-runner)) 0] + (purge state :corp) + (click-prompt state :runner "Yes")) + "Runner didn't get credits before deciding on LLDS") + (is (changed? [(:credit (get-runner)) 0] + (click-prompt state :runner "3 [Credits]: LLDS Energy Regulator")) + "Runner pays 3 for LLDS, then gets acacia credits") + (is (zero? (count (:discard (get-runner)))) "Acacia has not been trashed"))) (deftest acacia-effect-counts-both-runner-and-corp-virus-counters ;; Effect counts both Runner and Corp virus counters @@ -112,7 +108,7 @@ (run-on state :hq) (rez state :corp (get-ice state :hq 1)) (run-continue state) - (click-prompt state :runner "Yes") + (click-prompt state :runner "AirbladeX (JSRF Ed.)") (is (no-prompt? state :runner) "No Funhouse prompt") (is (= 2 (get-counters (refresh airbladex) :power)) "Spent 1 hosted power counter") (run-continue state) @@ -121,7 +117,7 @@ (run-continue state) (fire-subs state (get-ice state :hq 0)) (is (changed? [(count (:hand (get-runner))) 0] - (card-ability state :runner airbladex 0)) + (click-prompt state :runner "AirbladeX (JSRF Ed.)")) "1 net damage prevented") (is (= 1 (get-counters (refresh airbladex) :power)) "Spent 1 hosted power counter")))) @@ -382,8 +378,9 @@ (play-from-hand state :corp "SEA Source") (click-prompt state :corp "0") (click-prompt state :runner "0") - (card-ability state :runner (-> (get-resource state 0) :hosted first) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "On the Lam") + (click-prompt state :runner "Yes") + (click-prompt state :runner "1") (is (zero? (count-tags state)) "Runner should avoid tag") (is (= 1 (-> (get-runner) :discard count)) "Runner should have 1 card in Heap") (is (zero? (count (:hand (get-runner)))) "Runner doesn't draw from Aniccam"))) @@ -1940,22 +1937,21 @@ (play-from-hand state :runner "Sure Gamble") (play-from-hand state :runner "Feedback Filter") (is (= 7 (:credit (get-runner)))) - (let [ff (get-hardware state 0)] - (run-on state "Server 1") - (rez state :corp dm) - (run-continue state) - (card-subroutine state :corp dm 0) - (card-ability state :runner ff 0) - (is (= 3 (count (:hand (get-runner)))) "1 net damage prevented") - (is (= 4 (:credit (get-runner)))) - (run-continue state) - (click-prompt state :corp "Yes") ; pay 3 to fire Overwriter - (card-ability state :runner ff 1) - (click-prompt state :runner "Done") - (click-prompt state :runner "Pay 0 [Credits] to trash") ; trash Overwriter for 0 - (is (= 1 (:brain-damage (get-runner))) "2 of the 3 core damage prevented") - (is (= 2 (count (:hand (get-runner))))) - (is (empty? (get-hardware state)) "Feedback Filter trashed"))))) + (run-on state "Server 1") + (rez state :corp dm) + (run-continue state) + (card-subroutine state :corp dm 0) + (click-prompt state :runner "Feedback Filter (Net)") + (is (= 3 (count (:hand (get-runner)))) "1 net damage prevented") + (is (= 4 (:credit (get-runner)))) + (run-continue state) + (click-prompt state :corp "Yes") ; pay 3 to fire Overwriter + (click-prompt state :runner "Feedback Filter (Core)") + (click-prompt state :runner "2") + (click-prompt state :runner "Pay 0 [Credits] to trash") ; trash Overwriter for 0 + (is (= 1 (:brain-damage (get-runner))) "2 of the 3 core damage prevented") + (is (= 2 (count (:hand (get-runner))))) + (is (empty? (get-hardware state)) "Feedback Filter trashed")))) (deftest flame-out-basic-behavior ;; Basic behavior @@ -2366,7 +2362,7 @@ ;; expose and jack out (run-on state :hq) (card-ability state :runner gpi 0) - (is (last-log-contains? state "exposes Ice Wall") "Expose approached ice") + (is (last-log-contains? state "expose Ice Wall") "Expose approached ice") (is (= "Jack out?" (:msg (prompt-map :runner))) "Runner offered to jack out") (click-prompt state :runner "Yes") (is (nil? (get-run)) "Run has ended") @@ -2401,30 +2397,20 @@ (play-from-hand state :runner "Heartbeat") (is (= 5 (core/available-mu state)) "Gained 1 MU") (play-from-hand state :runner "Cache") - (let [hb (get-hardware state 0) - cache (get-program state 0) + (let [cache (get-program state 0) hbdown (get-runner-facedown state 0)] (damage state :corp :net 1) - (is (= (:msg (prompt-map :runner)) - "Prevent 1 net damage?") - "Damage prevention message correct.") - (card-ability state :runner hb 0) + (click-prompt state :runner "Heartbeat") (click-card state :runner cache) (is (= 1 (count (:discard (get-runner)))) "Prevented 1 net damage") (is (= 2 (count (:hand (get-runner))))) - (is (second-last-log-contains? state "Runner trashes 1 installed card \\(Cache\\) to use Heartbeat to prevent 1 damage\\.")) + (is (last-log-contains? state "Runner trashes 1 installed card \\(Cache\\) to use Heartbeat to prevent 1 net damage\\.")) (damage state :corp :net 3) - (is (= (:msg (prompt-map :runner)) - "Prevent any of the 3 net damage?") - "Damage prevention message correct.") - (card-ability state :runner hb 0) + (click-prompt state :runner "Heartbeat") (click-card state :runner hbdown) - (is (= (:msg (prompt-map :runner)) - "Prevent any of the 3 net damage? (1/3 prevented)") - "Damage prevention message correct.") - (click-prompt state :runner "Done") + (click-prompt state :runner "Pass priority") (is (= 4 (count (:discard (get-runner)))) "Prevented 1 of 3 net damage; used facedown card") - (is (last-n-log-contains? state 2 "Runner trashes 1 installed card \\(a facedown card\\) to use Heartbeat to prevent 1 damage\\."))))) + (is (last-n-log-contains? state 1 "Runner trashes 1 installed card \\(a facedown card\\) to use Heartbeat to prevent 1 net damage\\."))))) (deftest hermes (do-game @@ -2991,9 +2977,7 @@ (click-prompt state :corp "Yes") (click-card state :corp "IPO") (click-card state :corp "Extract") - (is (:run @state) "Run not ended yet") - (card-ability state :runner (get-hardware state 0) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "Lucky Charm") (is (:run @state) "Run prevented from ending"))) (deftest lucky-charm-no-interference-with-runs-ending-successfully-or-by-jacking-out-and-batty-normal-etr-border-control-interaction @@ -3039,7 +3023,7 @@ (card-subroutine state :corp (refresh iw) 0) (is (:run @state) "Run not ended yet") (is (not (no-prompt? state :runner)) "Runner prompted to ETR") - (click-prompt state :runner "Done") + (click-prompt state :runner "Allow the run to end") (is (not (:run @state)) "Run ended yet") (is (no-prompt? state :runner) "Prevent prompt gone") ;; run into border control, have its subroutine ETR, do use lucky charm @@ -3048,8 +3032,7 @@ (card-subroutine state :corp (refresh bc) 1) (is (:run @state) "Run not ended yet") (is (not (no-prompt? state :runner)) "Runner prompted to ETR") - (card-ability state :runner (get-hardware state 0) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "Lucky Charm") (is (= 1 (count (:rfg (get-runner)))) "Lucky Charm RFGed") (is (:run @state) "Run prevented from ending") (is (no-prompt? state :runner) "Prevent prompt gone") @@ -3062,8 +3045,7 @@ (is (= 1 (count (:discard (get-corp)))) "Border Control trashed") (is (:run @state) "Run not ended yet") (is (not (no-prompt? state :runner)) "Runner prompted to ETR") - (card-ability state :runner (get-hardware state 0) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "Lucky Charm") (is (= 2 (count (:rfg (get-runner)))) "2nd Lucky Charm RFGed") (is (:run @state) "Run prevented from ending") ;; win batty psi game and fire ice wall sub @@ -3078,8 +3060,7 @@ (click-prompt state :corp "End the run") (is (:run @state) "Run not ended yet") (is (not (no-prompt? state :runner)) "Runner prompted to ETR") - (card-ability state :runner (get-hardware state 0) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "Lucky Charm") (is (:run @state) "Run prevented from ending")))) (deftest mache @@ -4067,10 +4048,10 @@ (take-credits state :runner) (gain-tags state :runner 1) (play-from-hand state :corp "Scorched Earth") - (card-ability state :runner plas 0) - (card-ability state :runner plas 0) - (card-ability state :runner plas 0) - (card-ability state :runner plas 0) + (click-prompt state :runner "Plascrete Carapace") + (click-prompt state :runner "Plascrete Carapace") + (click-prompt state :runner "Plascrete Carapace") + (click-prompt state :runner "Plascrete Carapace") (is (= 1 (count (:hand (get-runner)))) "All meat damage prevented") (is (empty? (get-hardware state)) "Plascrete depleted and trashed")))) @@ -4423,58 +4404,51 @@ (deftest ramujan-reliant-550-bmi ;; Prevent up to X net or brain damage. (do-game - (new-game {:corp {:deck ["Data Mine" "Snare!"]} - :runner {:deck [(qty "Sure Gamble" 5)] - :hand [(qty "Ramujan-reliant 550 BMI" 4) "Sure Gamble"]}}) - (play-from-hand state :corp "Data Mine" "New remote") - (play-from-hand state :corp "Snare!" "Server 1") - (let [dm (get-ice state :remote1 0)] - (take-credits state :corp) - (play-from-hand state :runner "Ramujan-reliant 550 BMI") - (play-from-hand state :runner "Ramujan-reliant 550 BMI") - (play-from-hand state :runner "Ramujan-reliant 550 BMI") - (let [rr1 (get-hardware state 0) - rr2 (get-hardware state 1)] - (run-on state "Server 1") - (rez state :corp dm) - (run-continue state) - (card-subroutine state :corp dm 0) - (card-ability state :runner rr1 0) - (click-prompt state :runner "1") - (is (last-n-log-contains? state 1 "Sure Gamble") - "Ramujan did log trashed card names") - (is (= 2 (count (:hand (get-runner)))) "1 net damage prevented") - (run-continue state) - (click-prompt state :corp "No") - (click-prompt state :runner "No action") - (take-credits state :runner) - (take-credits state :corp) - (play-from-hand state :runner "Ramujan-reliant 550 BMI") - (run-empty-server state "Server 1") - (click-prompt state :corp "Yes") - (card-ability state :runner rr2 0) - (click-prompt state :runner "3") - (is (second-last-log-contains? state "Sure Gamble, Sure Gamble, and Sure Gamble") - "Ramujan did log trashed card names") - (is (= 1 (count (:hand (get-runner)))) "3 net damage prevented"))))) + (new-game {:corp {:deck ["Data Mine" "Snare!"]} + :runner {:deck [(qty "Sure Gamble" 5)] + :hand [(qty "Ramujan-reliant 550 BMI" 4) "Sure Gamble"]}}) + (play-from-hand state :corp "Data Mine" "New remote") + (play-from-hand state :corp "Snare!" "Server 1") + (let [dm (get-ice state :remote1 0)] + (take-credits state :corp) + (play-from-hand state :runner "Ramujan-reliant 550 BMI") + (play-from-hand state :runner "Ramujan-reliant 550 BMI") + (play-from-hand state :runner "Ramujan-reliant 550 BMI") + (run-on state "Server 1") + (rez state :corp dm) + (run-continue state) + (card-subroutine state :corp dm 0) + (click-prompt state :runner "Ramujan-reliant 550 BMI") + (click-prompt state :runner "1") + (is (= 2 (count (:hand (get-runner)))) "1 net damage prevented") + (run-continue state) + (click-prompt state :corp "No") + (click-prompt state :runner "No action") + (take-credits state :runner) + (take-credits state :corp) + (play-from-hand state :runner "Ramujan-reliant 550 BMI") + (run-empty-server state "Server 1") + (click-prompt state :corp "Yes") + (click-prompt state :runner "Ramujan-reliant 550 BMI") + (click-prompt state :runner "3") + (is (= 1 (count (:hand (get-runner)))) "3 net damage prevented")))) (deftest ramujan-reliant-550-bmi-prevent-up-to-x-net-or-brain-damage-empty-stack - ;; Prevent up to X net or brain damage. Empty stack - (do-game - (new-game {:corp {:deck ["Data Mine"]} - :runner {:deck ["Ramujan-reliant 550 BMI" "Sure Gamble"]}}) - (play-from-hand state :corp "Data Mine" "New remote") - (let [dm (get-ice state :remote1 0)] - (take-credits state :corp) - (play-from-hand state :runner "Ramujan-reliant 550 BMI") - (let [rr1 (get-hardware state 0)] - (run-on state "Server 1") - (rez state :corp dm) - (run-continue state) - (card-subroutine state :corp dm 0) - (card-ability state :runner rr1 0) - (click-prompt state :runner "Done") - (is (zero? (count (:hand (get-runner)))) "Not enough cards in Stack for Ramujan to work"))))) + ;; Prevent up to X net or brain damage. Empty stack + (do-game + (new-game {:corp {:deck ["Data Mine"]} + :runner {:deck ["Ramujan-reliant 550 BMI" "Sure Gamble"]}}) + (play-from-hand state :corp "Data Mine" "New remote") + (let [dm (get-ice state :remote1 0)] + (take-credits state :corp) + (play-from-hand state :runner "Ramujan-reliant 550 BMI") + (run-on state "Server 1") + (rez state :corp dm) + (run-continue state) + (card-subroutine state :corp dm 0) + (click-prompt state :runner "Ramujan-reliant 550 BMI") + (click-prompt state :runner "1") + (is (= 1 (count (:hand (get-runner)))) "Cards in stack don't actually matter")))) (deftest recon-drone ;; trash and pay X to prevent that much damage from a card you are accessing @@ -4501,18 +4475,14 @@ (play-from-hand state :runner "Recon Drone") (play-from-hand state :runner "Recon Drone") (play-from-hand state :runner "Recon Drone") - (let [rd1 (get-hardware state 0) - rd2 (get-hardware state 1) - rd3 (get-hardware state 2) - rd4 (get-hardware state 3) - hok (get-scored state :corp 0)] + (let [hok (get-scored state :corp 0)] (run-empty-server state "Server 2") (is (waiting? state :runner) "Runner has prompt to wait for Snare!") (click-prompt state :corp "Yes") - (card-ability state :runner rd1 0) + (click-prompt state :runner "Recon Drone") (click-prompt state :runner "3") - (click-prompt state :runner "No action") (is (= 5 (count (:hand (get-runner)))) "Runner took no net damage") + (click-prompt state :runner "No action") (run-empty-server state "Server 2") (is (waiting? state :runner) "Runner has prompt to wait for Snare!") (click-prompt state :corp "No") @@ -4523,27 +4493,26 @@ (run-empty-server state "Server 2") (is (waiting? state :runner) "Runner has prompt to wait for Snare!") (click-prompt state :corp "Yes") - (card-ability state :runner rd2 0) + (click-prompt state :runner "Recon Drone") (is (= 1 (:credit (get-runner))) "Runner has 1 credit") (is (= 1 (:number (:choices (prompt-map :runner)))) "Recon Drone choice limited to runner credits") (click-prompt state :runner "1") - (click-prompt state :runner "Done") + (click-prompt state :runner "Pass priority") (click-prompt state :runner "Pay 0 [Credits] to trash") (is (= 3 (count (:hand (get-runner)))) "Runner took 2 net damage from Snare!") (core/gain state :runner :credit 100) (run-empty-server state "Server 3") (is (waiting? state :runner) "Runner has prompt to wait for Prisec") (click-prompt state :corp "Yes") - (card-ability state :runner rd3 0) + (click-prompt state :runner "Recon Drone") (is (= 100 (:credit (get-runner))) "Runner has 100 credits") - (is (= 100 (:number (:choices (prompt-map :runner)))) "Recon Drone choice is not limited to 1 meat") (click-prompt state :runner "1") (click-prompt state :runner "Pay 3 [Credits] to trash") (is (= 3 (count (:hand (get-runner)))) "Runner took no meat damage") (run-empty-server state "Server 4") (is (waiting? state :runner) "Runner has prompt to wait for Cerebral Overwriter") (click-prompt state :corp "Yes") - (card-ability state :runner rd4 0) + (click-prompt state :runner "Recon Drone") (click-prompt state :runner "1") (is (= 3 (count (:hand (get-runner)))) "Runner took no core damage")))) @@ -5754,6 +5723,8 @@ (play-from-hand state :corp "Ice Wall" "Archives") (take-credits state :corp) (play-from-hand state :runner "Zamba") + (card-ability state :runner (get-hardware state 0) 0) + (click-prompt state :runner "Always") (is (= 6 (core/available-mu state)) "Gain 2 memory") (is (= 1 (:credit (get-runner))) "At 1 credit") (play-from-hand state :runner "Infiltration") diff --git a/test/clj/game/cards/ice_test.clj b/test/clj/game/cards/ice_test.clj index 1d6c386264..51466b750f 100644 --- a/test/clj/game/cards/ice_test.clj +++ b/test/clj/game/cards/ice_test.clj @@ -736,11 +736,10 @@ "Runner lost 2 credits") (is (changed? [(count (:hand (get-runner))) 0] (click-prompt state :runner "Take 1 net damage") - (card-ability state :runner (get-resource state 0) 0)) + (click-prompt state :runner "Caldera")) "Runner prevented 1 net damage") (is (changed? [(count (:hand (get-runner))) -1] - (click-prompt state :runner "Take 1 net damage") - (click-prompt state :runner "Done")) + (click-prompt state :runner "Take 1 net damage")) "Runner got 1 damage")))) (deftest attini-threat-ability @@ -799,7 +798,7 @@ (fire-subs state (refresh att)) (dotimes [_ 3] (is (changed? [(:credit (get-corp)) 3] - (card-ability state :corp (get-content state :remote1 0) 0)) + (click-prompt state :corp "Prāna Condenser")) "prevented 1 net with prana"))))) (deftest attini-threat-ability-cannot-spend-credits @@ -818,12 +817,7 @@ (is (changed? [(count (:hand (get-runner))) -3] (fire-subs state (refresh att)) - (dotimes [_ 3] - (is (changed? - [(:credit (get-runner)) 0] - (card-ability state :runner (get-resource state 0) 0) - (click-prompt state :runner "Done")) - "couldn't spend on caldera"))) + (is (no-prompt? state :runner) "Not prompted :)")) "Runner took 3 damage and couldn't prevent any of them by spending credits")))) (deftest authenticator-encounter-decline-to-take-tag @@ -3740,14 +3734,16 @@ (rez state :corp inazuma) (run-continue state) (fire-subs state (refresh inazuma)) - (run-continue state :movement) - (is (not (= nil (get-in @state [:run :cannot-jack-out]))) "Runner cannot jack out") + (run-continue-until state :movement) + (run-jack-out state) + (is (:run @state) "Runner cannot jack out") (run-continue state :approach-ice) (rez state :corp cl) (run-continue state) (fire-subs state cl) - (run-continue state) - (is (not (get-in @state [:run :cannot-jack-out])) "Runner can jack out")))) + (run-continue-until state :movement) + (run-jack-out state) + (is (not (:run @state)) "Runner jacked out")))) (deftest inazuma-cannot-break-subroutines-of-next-piece-of-ice ;; Cannot break subroutines of next piece of ice @@ -4231,9 +4227,7 @@ (is (changed? [(:credit (get-runner)) 2] (click-card state :corp nfl1)) "Runner gained 2 Credits thanks to Klevetnik's on-rez ability") - (is (changed? [(:credit (get-runner)) 0] - (card-ability state :runner (refresh nfl1) 0)) - "No Free Lunch was blanked") + (is (not (:playable (first (:abilities (refresh nfl1))))) "NFL abilities are not playable") (is (changed? [(:credit (get-runner)) 3] (card-ability state :runner (refresh nfl2) 0)) "Other No Free Lunch was not blanked") @@ -4241,10 +4235,9 @@ (card-subroutine state :corp klev 0) (is (not (:run @state)) "The run should have ended") (take-credits state :runner) - (is (changed? [(:credit (get-runner)) 0] - (card-ability state :runner (refresh nfl1) 0)) - "No Free Lunch still blank") + (is (not (:playable (first (:abilities (refresh nfl1))))) "NFL abilities are still not playable") (take-credits state :corp) + (take-credits state :runner) (is (changed? [(:credit (get-runner)) 3] (card-ability state :runner (refresh nfl1) 0)) "No Free Lunch unblanked")))) @@ -4289,15 +4282,11 @@ (is (changed? [(:credit (get-runner)) 2] (click-card state :corp nfl)) "Runner gained 2 Credits thanks to Klevetnik's on-rez ability") - (is (changed? [(:credit (get-runner)) 0] - (card-ability state :runner (refresh nfl) 0)) - "No Free Lunch was blanked") + (is (not (:playable (first (:abilities (refresh nfl))))) "NFL abilities are not playable") (run-continue state) (card-subroutine state :corp klev 0) (take-credits state :corp) ;; End of the Corp current turn - (is (changed? [(:credit (get-runner)) 0] - (card-ability state :runner (refresh nfl) 0)) - "No Free Lunch still blank") + (is (not (:playable (first (:abilities (refresh nfl))))) "NFL abilities are still not playable") (take-credits state :runner) (take-credits state :corp) ;; End of the Corp next turn (is (changed? [(:credit (get-runner)) 3] @@ -6429,16 +6418,16 @@ (click-prompt state :corp "Event") (fire-subs state (refresh sai)) (is (changed? [(count (:hand (get-runner))) -1] - (click-prompt state :runner "Done")) + (click-prompt state :runner "Pass priority")) "Let through first sub damage") (is (changed? [(count (:hand (get-runner))) 0] - (card-ability state :runner cal 0)) + (click-prompt state :runner "Caldera")) "Prevent special damage") (is (changed? [(count (:hand (get-runner))) 0] - (card-ability state :runner cal 0)) + (click-prompt state :runner "Caldera")) "Prevent second sub damage") (is (changed? [(count (:hand (get-runner))) 0] - (card-ability state :runner cal 0)) + (click-prompt state :runner "Caldera")) "Prevent third sub damage") (is (no-prompt? state :runner) "No more damage prevention triggers")))) @@ -7265,12 +7254,12 @@ (run-continue state) (fire-subs state susanoo) (is (= [:archives] (get-in @state [:run :server])) "Deflected to archives") - (is (get-in @state [:run :cannot-jack-out]) "Runner cannot jack out") (rez state :corp cl) (run-continue-until state :encounter-ice cl) (fire-subs state cl) (run-continue state :movement) - (is (not (get-in @state [:run :cannot-jack-out])) "Runner can jack out again")))) + (run-jack-out state) + (is (not (:run @state)) "Runner can jack out")))) (deftest susanoo-no-mikoto-redirection-does-not-occur-during-a-forced-encounter ;; Redirection does not occur during a forced encounter @@ -7295,7 +7284,6 @@ (fire-subs state susanoo) (is (= [:rd] (get-in @state [:run :server])) "Run still on R&D") (run-continue state :encounter-ice) - (is (get-in @state [:run :cannot-jack-out]) "Runner cannot jack out") (rez state :corp cl) (run-continue-until state :encounter-ice cl) (is (not (get-in @state [:run :cannot-jack-out])) "Runner can jack out again")))) @@ -8434,7 +8422,8 @@ (rez state :corp wp) (run-continue state) (fire-subs state wp) - (is (get-in @state [:run :cannot-jack-out])) + (run-jack-out state) + (is (:run @state) "Runner cannot jack out") (is (nil? (refresh wp)) "Whirlpool is trashed")))) (deftest whirlpool-on-hq @@ -8451,7 +8440,8 @@ (rez state :corp wp) (run-continue state) (fire-subs state wp) - (is (get-in @state [:run :cannot-jack-out])) + (run-jack-out state) + (is (:run @state) "Runner cannot jack out") (is (nil? (refresh wp)) "Whirlpool is trashed")))) (deftest whirlpool-whirlpool-not-trashed-when-broken diff --git a/test/clj/game/cards/identities_test.clj b/test/clj/game/cards/identities_test.clj index 69ebfea095..3ff3ff5e0c 100644 --- a/test/clj/game/cards/identities_test.clj +++ b/test/clj/game/cards/identities_test.clj @@ -15,106 +15,106 @@ FourHundredAndNineTeen-amoral-scammer ;; 419 (do-game - (new-game {:corp {:id "Weyland Consortium: Builder of Nations" - :deck ["PAD Campaign" "The Cleaners" (qty "Pup" 3) "Oaktown Renovation"]} - :runner {:id "419: Amoral Scammer"}}) - (is (= 5 (:credit (get-corp))) "Starts with 5 credits") - (play-from-hand state :corp "Pup" "HQ") - (click-prompt state :runner "Yes") - (click-prompt state :corp "Yes") - (is (= 4 (:credit (get-corp))) "Pays 1 credit to not expose card") - (play-from-hand state :corp "Pup" "HQ") - (is (no-prompt? state :runner) "No option on second install") - (take-credits state :corp) - (take-credits state :runner) - (play-from-hand state :corp "Pup" "Archives") - (click-prompt state :runner "No") - (is (no-prompt? state :corp) "No prompt if Runner chooses No") - (take-credits state :corp) - (take-credits state :runner) - (play-from-hand state :corp "The Cleaners" "New remote") - (click-prompt state :runner "Yes") - (click-prompt state :corp "No") - (is (last-log-contains? state "exposes The Cleaners") "Installed card was exposed") - (take-credits state :corp) - (take-credits state :runner) - (play-from-hand state :corp "Oaktown Renovation" "New remote") - (is (no-prompt? state :corp) "Cannot expose faceup agendas") - (take-credits state :corp) - (take-credits state :runner) - (core/lose state :corp :credit (:credit (get-corp))) - (is (zero? (:credit (get-corp))) "Corp has no credits") - (play-from-hand state :corp "PAD Campaign" "New remote") - (click-prompt state :runner "Yes") - (is (no-prompt? state :corp) "No prompt if Corp has no credits") - (is (last-log-contains? state "exposes PAD Campaign") "Installed card was exposed"))) + (new-game {:corp {:id "Weyland Consortium: Builder of Nations" + :deck ["PAD Campaign" "The Cleaners" (qty "Pup" 3) "Oaktown Renovation"]} + :runner {:id "419: Amoral Scammer"}}) + (is (= 5 (:credit (get-corp))) "Starts with 5 credits") + (play-from-hand state :corp "Pup" "HQ") + (click-prompt state :runner "Yes") + (click-prompt state :corp "Yes") + (is (= 4 (:credit (get-corp))) "Pays 1 credit to not expose card") + (play-from-hand state :corp "Pup" "HQ") + (is (no-prompt? state :runner) "No option on second install") + (take-credits state :corp) + (take-credits state :runner) + (play-from-hand state :corp "Pup" "Archives") + (click-prompt state :runner "No") + (is (no-prompt? state :corp) "No prompt if Runner chooses No") + (take-credits state :corp) + (take-credits state :runner) + (play-from-hand state :corp "The Cleaners" "New remote") + (click-prompt state :runner "Yes") + (click-prompt state :corp "No") + (is (last-log-contains? state "expose The Cleaners") "Installed card was exposed") + (take-credits state :corp) + (take-credits state :runner) + (play-from-hand state :corp "Oaktown Renovation" "New remote") + (is (no-prompt? state :corp) "Cannot expose faceup agendas") + (take-credits state :corp) + (take-credits state :runner) + (core/lose state :corp :credit (:credit (get-corp))) + (is (zero? (:credit (get-corp))) "Corp has no credits") + (play-from-hand state :corp "PAD Campaign" "New remote") + (click-prompt state :runner "Yes") + (is (no-prompt? state :corp) "No prompt if Corp has no credits") + (is (last-log-contains? state "expose PAD Campaign") "Installed card was exposed"))) (deftest FourHundredAndNineTeen-amoral-scammer-verify-expose-can-be-blocked - ;; Verify expose can be blocked - (do-game - (new-game {:corp {:id "Weyland Consortium: Builder of Nations" - :deck ["Underway Grid" "Pup"]} - :runner {:id "419: Amoral Scammer"}}) - (play-from-hand state :corp "Underway Grid" "New remote") - (click-prompt state :runner "No") - (take-credits state :corp) - (take-credits state :runner) - (play-from-hand state :corp "Pup" "Server 1") - (click-prompt state :runner "Yes") - (let [ug (get-in @state [:corp :servers :remote1 :content 0])] - (rez state :corp ug) - (click-prompt state :corp "No") - (is (last-log-contains? state "uses Underway Grid to prevent 1 card from being exposed") "Exposure was prevented")))) + ;; Verify expose can be blocked + (do-game + (new-game {:corp {:id "Weyland Consortium: Builder of Nations" + :deck ["Underway Grid" "Pup"]} + :runner {:id "419: Amoral Scammer"}}) + (play-from-hand state :corp "Underway Grid" "New remote") + (click-prompt state :runner "No") + (take-credits state :corp) + (take-credits state :runner) + (play-from-hand state :corp "Pup" "Server 1") + (click-prompt state :runner "Yes") + (let [ug (get-in @state [:corp :servers :remote1 :content 0])] + (rez state :corp ug) + (click-prompt state :corp "No") + (is (not (last-log-contains? state "expose Pup")) "Exposure was prevented")))) (deftest FourHundredAndNineTeen-amoral-scammer-ixodidae-shouldn-t-trigger-off-419-s-ability - ;; Ixodidae shouldn't trigger off 419's ability - (do-game - (new-game {:corp {:deck ["PAD Campaign"]} - :runner {:id "419: Amoral Scammer" - :deck ["Ixodidae"]}}) - (take-credits state :corp) - (play-from-hand state :runner "Ixodidae") - (take-credits state :runner) - (play-from-hand state :corp "PAD Campaign" "New remote") - (let [corp-credits (:credit (get-corp)) - runner-credits (:credit (get-runner))] - (click-prompt state :runner "Yes") - (click-prompt state :corp "Yes") - (is (= 1 (- corp-credits (:credit (get-corp)))) "Should lose 1 credit from 419 ability") - (is (zero? (- runner-credits (:credit (get-runner)))) "Should not gain any credits from Ixodidae")))) + ;; Ixodidae shouldn't trigger off 419's ability + (do-game + (new-game {:corp {:deck ["PAD Campaign"]} + :runner {:id "419: Amoral Scammer" + :deck ["Ixodidae"]}}) + (take-credits state :corp) + (play-from-hand state :runner "Ixodidae") + (take-credits state :runner) + (play-from-hand state :corp "PAD Campaign" "New remote") + (let [corp-credits (:credit (get-corp)) + runner-credits (:credit (get-runner))] + (click-prompt state :runner "Yes") + (click-prompt state :corp "Yes") + (is (= 1 (- corp-credits (:credit (get-corp)))) "Should lose 1 credit from 419 ability") + (is (zero? (- runner-credits (:credit (get-runner)))) "Should not gain any credits from Ixodidae")))) (deftest FourHundredAndNineTeen-amoral-scammer-419-vs-asa-group-double-install-corp-s-turn - ;; 419 vs Asa Group double install, Corp's turn - (do-game - (new-game {:corp {:id "Asa Group: Security Through Vigilance" - :deck [(qty "Hedge Fund" 5)] - :hand ["PAD Campaign" "Ice Wall"]} - :runner {:id "419: Amoral Scammer"}}) - (play-from-hand state :corp "PAD Campaign" "New remote") - (click-card state :corp "Ice Wall") - (click-prompt state :runner "Yes") - (click-prompt state :corp "No") - (is (last-log-contains? state "exposes PAD Campaign in Server 1") "Installed card was exposed"))) + ;; 419 vs Asa Group double install, Corp's turn + (do-game + (new-game {:corp {:id "Asa Group: Security Through Vigilance" + :deck [(qty "Hedge Fund" 5)] + :hand ["PAD Campaign" "Ice Wall"]} + :runner {:id "419: Amoral Scammer"}}) + (play-from-hand state :corp "PAD Campaign" "New remote") + (click-card state :corp "Ice Wall") + (click-prompt state :runner "Yes") + (click-prompt state :corp "No") + (is (last-log-contains? state "expose PAD Campaign in Server 1") "Installed card was exposed"))) (deftest FourHundredAndNineTeen-amoral-scammer-419-vs-asa-group-double-install-runner-s-turn - ;; 419 vs Asa Group double install, Runner's turn - (do-game - (new-game {:corp {:id "Asa Group: Security Through Vigilance" - :deck [(qty "Hedge Fund" 5)] - :hand ["PAD Campaign" "Ice Wall" "Advanced Assembly Lines"]} - :runner {:id "419: Amoral Scammer"}}) - (play-from-hand state :corp "Advanced Assembly Lines" "New remote") - (click-prompt state :corp "Done") - (click-prompt state :runner "No") - (take-credits state :corp) - (rez state :corp (get-content state :remote1 0)) - (card-ability state :corp (get-content state :remote1 0) 0) - (click-card state :corp "PAD Campaign") - (click-prompt state :corp "New remote") - (click-prompt state :runner "Yes") - (click-prompt state :corp "No") - (is (last-log-contains? state "exposes PAD Campaign in Server 2") "Installed card was exposed") - (is (prompt-is-type? state :corp :select) "Corp should still have select prompt"))) + ;; 419 vs Asa Group double install, Runner's turn + (do-game + (new-game {:corp {:id "Asa Group: Security Through Vigilance" + :deck [(qty "Hedge Fund" 5)] + :hand ["PAD Campaign" "Ice Wall" "Advanced Assembly Lines"]} + :runner {:id "419: Amoral Scammer"}}) + (play-from-hand state :corp "Advanced Assembly Lines" "New remote") + (click-prompt state :corp "Done") + (click-prompt state :runner "No") + (take-credits state :corp) + (rez state :corp (get-content state :remote1 0)) + (card-ability state :corp (get-content state :remote1 0) 0) + (click-card state :corp "PAD Campaign") + (click-prompt state :corp "New remote") + (click-prompt state :runner "Yes") + (click-prompt state :corp "No") + (is (last-log-contains? state "expose PAD Campaign in Server 2") "Installed card was exposed") + (is (prompt-is-type? state :corp :select) "Corp should still have select prompt"))) (deftest FourHundredAndNineTeen-amoral-scammer-interation-with-install-and-rez-effects-issue-4485 ;; interation with 'install and rez' effects. Issue #4485 @@ -130,27 +130,27 @@ (is (not (no-prompt? state :runner)) "419 does trigger on installed and rezzed cards"))) (deftest FourHundredAndNineTeen-amoral-scammer-419-vs-sportsmetal-jinja-grid-issue-3806 - ;; 419 vs Sportsmetal Jinja Grid. Issue #3806 - (do-game - (new-game {:corp {:id "Sportsmetal: Go Big or Go Home" - :deck [(qty "Ice Wall" 5)] - :hand ["Domestic Sleepers" "Jinja City Grid"]} - :runner {:id "419: Amoral Scammer"}}) - (play-from-hand state :corp "Jinja City Grid" "New remote") - (rez state :corp (get-content state :remote1 0)) - (click-prompt state :runner "No") - (take-credits state :corp) - (run-empty-server state :hq) - (click-prompt state :runner "Steal") - (click-prompt state :corp "Draw 2 cards") - (is (waiting? state :runner) "During Jinja, Runner should wait") - (click-prompt state :corp (first (prompt-buttons :corp))) - (is (= 2 (count (prompt-buttons :runner))) "419 can trigger ability with 2 options") - (is (waiting? state :corp) "Corp is waiting for runner ability") - (click-prompt state :runner "Yes") - (click-prompt state :corp "No") - (is (= 2 (count (prompt-buttons :corp))) "Corp should have prompt back with 2 options") - (is (waiting? state :runner) "Runner should wait again"))) + ;; 419 vs Sportsmetal Jinja Grid. Issue #3806 + (do-game + (new-game {:corp {:id "Sportsmetal: Go Big or Go Home" + :deck [(qty "Ice Wall" 5)] + :hand ["Domestic Sleepers" "Jinja City Grid"]} + :runner {:id "419: Amoral Scammer"}}) + (play-from-hand state :corp "Jinja City Grid" "New remote") + (rez state :corp (get-content state :remote1 0)) + (click-prompt state :runner "No") + (take-credits state :corp) + (run-empty-server state :hq) + (click-prompt state :runner "Steal") + (click-prompt state :corp "Draw 2 cards") + (is (waiting? state :runner) "During Jinja, Runner should wait") + (click-prompt state :corp (first (prompt-buttons :corp))) + (is (= 2 (count (prompt-buttons :runner))) "419 can trigger ability with 2 options") + (is (waiting? state :corp) "Corp is waiting for runner ability") + (click-prompt state :runner "Yes") + (click-prompt state :corp "No") + (is (= 2 (count (prompt-buttons :corp))) "Corp should have prompt back with 2 options") + (is (waiting? state :runner) "Runner should wait again"))) (deftest FourHundredAndNineTeen-amoral-scammer-419-vs-expose-timing-rules-change ;; 419 vs Sportsmetal Jinja Grid. Issue #3806 @@ -2690,6 +2690,7 @@ (take-credits state :corp) (run-on state :remote1) (run-continue-until state :movement) + (click-prompt state :corp "Tori Hanzō") (click-prompt state :corp "Yes") (is (= ["Sure Gamble"] (map :title (:discard (get-runner)))) "Gamble trashed, easy mark not milled") (is (= 1 (:brain-damage (get-runner)))))) @@ -4676,25 +4677,20 @@ (take-credits state :corp) (play-from-hand state :runner "Feedback Filter") (is (= 3 (:credit (get-runner))) "Runner has 3 credits") - (let [psychic (get-content state :remote1 0) - ff (get-hardware state 0)] + (let [psychic (get-content state :remote1 0)] (run-empty-server state :hq) (is (:run @state) "On successful run trigger effects") (click-card state :runner psychic) (is (= 1 (count (:hand (get-runner)))) "Runner has 1 card in hand") (click-prompt state :corp "2 [Credits]") (click-prompt state :runner "0 [Credits]") - (card-ability state :runner ff 0) + (click-prompt state :runner "Feedback Filter (Net)") (is (zero? (:credit (get-runner))) "Runner has no more credits left") - (is (= 1 (count (:hand (get-runner)))) "Prevented 1 net damage") - (is (empty? (:discard (get-runner))) "No cards discarded") - (is (:run @state) "On run access phase") - (click-prompt state :runner "Done") (is (empty? (:hand (get-runner))) "Suffered 1 net damage due to accessing Fetal AI") (is (= 1 (count (:discard (get-runner)))) "Discarded 1 card due to net damage") (is (:run @state) "Resolving access triggers") - (click-prompt state :runner "No action") (is (zero? (count (:scored (get-runner)))) "Runner has no credits to be able to steal Fetal AI") + (click-prompt state :runner "No action") (is (not (:run @state)) "Run has now ended") (is (= "Flatline" (:reason @state)) "Win condition reports flatline")))) diff --git a/test/clj/game/cards/operations_test.clj b/test/clj/game/cards/operations_test.clj index 7c9b673607..ea26ad9f2f 100644 --- a/test/clj/game/cards/operations_test.clj +++ b/test/clj/game/cards/operations_test.clj @@ -254,7 +254,9 @@ (click-prompt state :corp "R&D") (click-prompt state :runner "Yes") (is (:run @state) "Run started") - (is (get-in @state [:run :cannot-jack-out]) "Runner cannot jack out") + (run-continue-until state :movement) + (run-jack-out state) + (is (:run @state) "Did not jack out") (is (not (find-card "An Offer You Can't Refuse" (:scored (get-corp)))) "Offer isn't in score area"))) (deftest anonymous-tip @@ -1035,7 +1037,7 @@ (is (= (inc corp-creds) (:credit (get-corp))) "Corp gained 1 when runner installed Aumakua") (play-from-hand state :runner "Fall Guy") (is (= (+ 2 corp-creds) (:credit (get-corp))) "Corp gained 1 when runner installed Fall Guy") - (card-ability state :runner (get-resource state 0) 1) + (card-ability state :runner (get-resource state 0) 0) (is (= (+ 3 corp-creds) (:credit (get-corp))) "Corp gained 1 when runner trashed Fall Guy") (run-empty-server state :remote1) (click-prompt state :runner "Pay 4 [Credits] to trash") @@ -3218,12 +3220,10 @@ (is (changed? [(count-tags state) 2] (play-from-hand state :corp "Oppo Research") (is (not (no-prompt? state :runner)) "Runner prompted to avoid tag") - (card-ability state :runner (get-resource state 0) 0) - (click-prompts state :corp - "0" - {:side :runner :choice "0"} - {:side :runner :choice "Done"} - "Yes")) + (click-prompts state :runner "No One Home" "Yes") + (click-prompt state :corp "0") + (click-prompts state :runner "0" "2") + (click-prompt state :corp "Yes")) "Runner prevented 2 tag"))) (deftest oversight-ai-rez-at-no-cost @@ -4557,7 +4557,7 @@ (click-card state :corp (find-card "Scorched Earth" (:hand (get-corp)))) (is (waiting? state :corp) "Corp does not have Subcontract prompt until damage prevention completes") - (click-prompt state :runner "Done") + (click-prompt state :runner "Pass priority") (is (not (no-prompt? state :corp)) "Corp can now play second Subcontract operation"))) (deftest subcontract-interaction-with-terminal-operations @@ -4981,9 +4981,8 @@ (is (= 1 (count (:hand (get-corp)))) "Corp could not play All Seeing I when runner was not tagged") (gain-tags state :runner 1) (play-from-hand state :corp "The All-Seeing I") - (let [fall-guy (get-resource state 1)] - (card-ability state :runner fall-guy 0)) - (click-prompt state :runner "Done") + (click-prompt state :runner "Fall Guy") + (click-prompt state :runner "Same Old Thing") (is (= 1 (res)) "One installed resource saved by Fall Guy") (is (= 2 (count (:discard (get-runner)))) "Two cards in heap")))) @@ -5002,14 +5001,12 @@ (gain-tags state :runner 1) (take-credits state :runner) (play-from-hand state :corp "The All-Seeing I") - (is (= "Prevent the trashing of Off-Campus Apartment?" - (:msg (prompt-map :runner)))) - (let [fall-guy (find-card "Fall Guy" (core/all-active-installed state :runner))] - (card-ability state :runner fall-guy 0)) - (click-prompt state :runner "Done") - (is (= "Prevent the trashing of Fall Guy?" + (is (= "Prevent any of Fall Guy, Fall Guy, or Off-Campus Apartment from being trashed?" (:msg (prompt-map :runner)))) - (click-prompt state :runner "Done") + (click-prompt state :runner "Fall Guy") + (click-prompt state :runner "Off-Campus Apartment") + ;; no more valid targets, since fall guy can't target itself and can't target the + ;; OCA that is already off the trash list :) (is (= 1 (count (core/all-active-installed state :runner))) "One installed card (Off-Campus)") (is (= 2 (count (:discard (get-runner)))) "Two cards in heap"))) diff --git a/test/clj/game/cards/programs_test.clj b/test/clj/game/cards/programs_test.clj index a13de82360..8322207740 100644 --- a/test/clj/game/cards/programs_test.clj +++ b/test/clj/game/cards/programs_test.clj @@ -2874,7 +2874,8 @@ (run-empty-server state "Server 1") (click-prompt state :runner "Pay 5 [Credits] to trash") (let [dx (get-program state 0)] - (card-ability state :runner dx 1) + (click-prompt state :runner "Deus X") + (click-prompt state :runner "1") (is (= 2 (count (:hand (get-runner)))) "Deus X prevented one Hostile net damage")))) (deftest deus-x-vs-multiple-sources-of-net-damage @@ -2889,7 +2890,8 @@ (play-from-hand state :runner "Deus X") (run-empty-server state "Server 1") (let [dx (get-program state 0)] - (card-ability state :runner dx 1) + (click-prompt state :runner "Deus X") + (click-prompt state :runner "2") (click-prompt state :runner "Pay to steal") (is (= 3 (count (:hand (get-runner)))) "Deus X prevented net damage from accessing Fetal AI, but not from Personal Evolution") (is (= 1 (count (:scored (get-runner)))) "Fetal AI stolen")))) @@ -4780,7 +4782,7 @@ (run-continue-until state :approach-ice) (rez state :corp (get-ice state :rd 0)) (run-continue state) - (click-prompt state :runner "Yes") + (click-prompt state :runner "Hunting Grounds") (card-ability state :runner inv 1) (card-ability state :runner inv 1) (card-ability state :runner inv 0) @@ -6017,6 +6019,7 @@ (is (= 1 (get-strength (refresh nanotk))) "Default strength") (run-on state "HQ") (rez state :corp susanoo) + (rez state :corp (get-ice state :archives 0)) (run-continue state) (is (= 3 (get-strength (refresh nanotk))) "2 ice on HQ") (card-subroutine state :corp (refresh susanoo) 0) @@ -7200,11 +7203,11 @@ (play-from-hand state :runner "Fall Guy") (is (zero? (count (:hand (get-runner)))) "No cards in hand") ; No draw from Fall Guy trash as Reaver already fired this turn - (card-ability state :runner (get-resource state 0) 1) + (card-ability state :runner (get-resource state 0) 0) (is (zero? (count (:hand (get-runner)))) "No cards in hand") (take-credits state :runner) ; Draw from Fall Guy trash on corp turn - (card-ability state :runner (get-resource state 0) 1) + (card-ability state :runner (get-resource state 0) 0) (is (= 1 (count (:hand (get-runner)))) "One card in hand"))) (deftest reaver-not-triggering-on-non-installed-cards diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 320dd62bdc..18a202f6dc 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -868,7 +868,8 @@ (gain-tags state :runner 1) (play-from-hand state :corp "Scorched Earth") (is (zero? (count (:discard (get-runner)))) "No cards have been discarded or trashed yet") - (card-ability state :runner (get-resource state 0) 0) + (click-prompt state :runner "Citadel Sanctuary") + (click-prompt state :runner "Yes") (is (= 3 (count (:discard (get-runner)))) "CS and all cards in grip are trashed"))) (deftest citadel-sanctuary-end-of-turn-trace @@ -1689,7 +1690,8 @@ (click-prompt state :corp "0") (click-prompt state :runner "0") (is (not (no-prompt? state :runner)) "Runner prompted to avoid tag") - (card-ability state :runner (get-resource state 0) 0) + (click-prompt state :runner "Decoy") + (click-prompt state :runner "Yes") (is (= 1 (count (:discard (get-runner)))) "Decoy trashed") (is (zero? (count-tags state)) "Tag avoided"))) @@ -2216,11 +2218,10 @@ (play-from-hand state :runner "Dummy Box") (play-from-hand state :runner "Cache") (take-credits state :runner) - (trash state :runner (get-program state 0)) + (trash state :corp (get-program state 0)) (is (not (no-prompt? state :runner)) "Dummy Box prompting to prevent program trash") - (card-ability state :runner (get-resource state 0) 1) + (click-prompt state :runner "Dummy Box (Program)") (click-card state :runner (find-card "Clot" (:hand (get-runner)))) - (click-prompt state :runner "Done") (is (= 1 (count (:discard (get-runner)))) "Clot trashed") (is (empty? (:hand (get-runner))) "Card trashed from hand") (is (= 1 (count (get-program state))) "Cache still installed") @@ -2397,8 +2398,7 @@ (play-from-hand state :runner "Environmental Testing") (play-from-hand state :runner "Fall Guy") (dotimes [_ 4] (play-from-hand state :runner "Ika")) - (card-ability state :runner (get-resource state 1) 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "Fall Guy") (is (= 20 (:credit (get-runner))) "Got paid out twice") (is (= 2 (count (:discard (get-runner)))) "Env and fall guy both trashed"))) @@ -2774,7 +2774,7 @@ (click-prompt state :runner "Pay 3 [Credits] to trash") (is (waiting? state :runner) "Runner has prompt to wait for Corp to shuffle Marilyn") - (is (= "Shuffle Marilyn Campaign into R&D?" (:msg (prompt-map :corp))) "Now Corp gets shuffle choice") + (is (= "Choose an interrupt" (:msg (prompt-map :corp))) "Now Corp gets shuffle choice") (is (= 2 (:credit (get-runner)))) #_ trashed_marilyn)) (deftest friend-of-a-friend @@ -3211,7 +3211,7 @@ (run-on state "Server 1") (let [credits (:credit (get-runner))] (run-continue state) - (click-prompt state :runner "Yes") + (click-prompt state :runner "Hunting Grounds") (is (= credits (:credit (get-runner))) "Runner doesn't lose any credits to Tollbooth") (is (:run @state) "Run hasn't ended from not paying Tollbooth")))) @@ -3227,7 +3227,7 @@ (play-from-hand state :runner "Hunting Grounds") (run-on state "Server 1") (run-continue state) - (click-prompt state :runner "No") + (click-prompt state :runner "Allow Tollbooth encounter") (is (zero? (:credit (get-runner))) "Runner loses credits to Tollbooth") (is (:run @state) "Run hasn't ended when paying Tollbooth"))) @@ -3248,7 +3248,7 @@ (run-on state "Server 1") (let [credits (:credit (get-runner))] (run-continue state) - (click-prompt state :runner "Yes") + (click-prompt state :runner "Hunting Grounds") (run-continue-until state :encounter-ice) (is (= (- credits 3) (:credit (get-runner))) "Runner loses 3 credits to Tollbooth 2 ")))) @@ -3265,7 +3265,7 @@ (let [credits (:credit (get-runner))] (run-on state "Server 1") (run-continue state) - (click-prompt state :runner "Yes") + (click-prompt state :runner "Hunting Grounds") (is (= credits (:credit (get-runner))) "Runner doesn't lose any credits to Tollbooth") (run-continue state :movement) (run-jack-out state)) @@ -4458,24 +4458,22 @@ (is (no-prompt? state :runner) "No more prompt"))) (deftest net-mercur-prevention-prompt-issue-4464 - ;; Prevention prompt. Issue #4464 - (do-game - (new-game {:runner {:hand ["Feedback Filter" "Net Mercur"] - :credits 10}}) - (take-credits state :corp) - (play-from-hand state :runner "Feedback Filter") - (play-from-hand state :runner "Net Mercur") - (let [nm (get-resource state 0) - ff (get-hardware state 0)] - (core/add-counter state :runner (make-eid state) (refresh nm) :credit 4) - (damage state :corp :net 2) - (card-ability state :runner ff 0) - (click-card state :runner nm) - (click-card state :runner nm) - (click-card state :runner nm) - (card-ability state :runner ff 0) - (click-prompt state :runner "Done") - (is (= 1 (get-counters (refresh nm) :credit)) "Net Mercur has lost 3 credits")))) + ;; Prevention prompt. Issue #4464 + (do-game + (new-game {:runner {:hand ["Feedback Filter" "Net Mercur"] + :credits 10}}) + (take-credits state :corp) + (play-from-hand state :runner "Feedback Filter") + (play-from-hand state :runner "Net Mercur") + (let [nm (get-resource state 0)] + (core/add-counter state :runner (make-eid state) (refresh nm) :credit 4) + (damage state :corp :net 2) + (click-prompt state :runner "Feedback Filter (Net)") + (click-card state :runner nm) + (click-card state :runner nm) + (click-card state :runner nm) + (click-prompt state :runner "Pass priority") + (is (= 1 (get-counters (refresh nm) :credit)) "Net Mercur has lost 3 credits")))) (deftest network-exchange ;; ice install costs 1 more except for inner most @@ -4567,8 +4565,8 @@ (play-from-hand state :corp "SEA Source") (click-prompt state :corp "0") ; default trace (click-prompt state :runner "0") ; Runner won't match - (card-ability state :runner nach 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "New Angeles City Hall") + (click-prompt state :runner "Yes") (is (zero? (count-tags state)) "Avoided SEA Source tag") (is (= 4 (:credit (get-runner))) "Paid 2 credits") (take-credits state :corp) @@ -4600,9 +4598,9 @@ (click-prompt state :runner "Account Siphon") (let [nach (get-resource state 0)] (is (= 4 (:credit (get-runner))) "Have not gained Account Siphon credits until tag avoidance window closes") - (card-ability state :runner nach 0) - (card-ability state :runner nach 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "New Angeles City Hall") + (click-prompt state :runner "Yes") + (click-prompt state :runner "Yes") (is (zero? (count-tags state)) "Tags avoided") (is (= 10 (:credit (get-runner))) "10 credits siphoned") (is (= 3 (:credit (get-corp))) "Corp lost 5 credits")))) @@ -4638,15 +4636,16 @@ (take-credits state :corp) (play-from-hand state :runner "Sure Gamble") (play-from-hand state :runner "No One Home") - (let [dm (get-ice state :archives 0) - noh (get-resource state 0)] + (let [dm (get-ice state :archives 0)] (run-on state "Archives") (rez state :corp dm) (run-continue state) (card-subroutine state :corp dm 0) - (card-ability state :runner noh 0) + (click-prompt state :runner "No One Home") + (click-prompt state :runner "Yes") (click-prompt state :corp "0") (click-prompt state :runner "0") + (click-prompt state :runner "1") (is (= 3 (count (:hand (get-runner)))) "1 net damage prevented") (run-continue state) (play-from-hand state :runner "No One Home") @@ -4655,10 +4654,11 @@ (click-prompt state :corp "0") (click-prompt state :runner "0") (is (not (no-prompt? state :runner)) "Runner prompted to avoid tag") - (card-ability state :runner (get-resource state 0) 0) + (click-prompt state :runner "No One Home") + (click-prompt state :runner "Yes") (click-prompt state :corp "0") (click-prompt state :runner "0") - (click-prompt state :runner "Done") + (click-prompt state :runner "1") (is (= 3 (count (:discard (get-runner)))) "Two NOH trashed, 1 gamble played") (is (zero? (count-tags state)) "Tags avoided") (take-credits state :corp) @@ -4666,7 +4666,7 @@ (take-credits state :runner) (gain-tags state :runner 1) (is (not (no-prompt? state :runner)) "Runner prompted to avoid tag") - (click-prompt state :runner "Done") + (click-prompt state :runner "Allow 1 remaining tag") (core/gain state :corp :credit 4) (play-from-hand state :corp "Scorched Earth") (is (no-prompt? state :runner) "Runner not prompted to avoid meat damage")) @@ -4685,11 +4685,67 @@ (rez state :corp dm) (run-continue state) (card-subroutine state :corp dm 0) - (card-ability state :runner noh 0) + (click-prompt state :runner "No One Home") + (click-prompt state :runner "Yes") (click-prompt state :corp "0") (click-prompt state :runner "0") + (click-prompt state :runner "1") (is (= 2 (count (:hand (get-runner)))) "1 net damage prevented")))))) +(deftest no-one-home-only-on-first-event + (doseq [[scenario-a scenario-b] [[:net :net] [:net :tag] [:tag :tag] [:tag :net]]] + (do-game + (new-game {:runner {:hand [(qty "Sure Gamble" 5) "No One Home" "No One Home"]} + :corp {:hand [(qty "Public Trail" 2)]}}) + (take-credits state :corp) + (play-from-hand state :runner "No One Home") + (play-from-hand state :runner "No One Home") + (run-empty-server state :hq) + (click-prompt state :runner "No action") + (take-credits state :runner) + (if (= scenario-a :net) + (damage state :corp :net 1) + (do (play-from-hand state :corp "Public Trail") + (click-prompt state :runner "Take 1 tag"))) + ;; avoid damage/tag + (click-prompts state :runner "No One Home" "Yes") + (click-prompt state :corp "0") + (click-prompts state :runner "0" "1") + (is (no-prompt? state :runner) "No lingering prompt") + (is (= 1 (count (:discard (get-runner)))) "Trashed NOH, no damage") + (is (= 0 (count-tags state)) "Not tagged") + (if (= scenario-b :net) + (damage state :corp :net 1) + (do (play-from-hand state :corp "Public Trail") + (click-prompt state :runner "Take 1 tag"))) + (is (no-prompt? state :runner) (str "No prompt because it's already passed: (" scenario-a " -> " scenario-b ")"))))) + +(deftest no-one-home-snare-can-prevent-either-trigger + (doseq [scenario [:net :tag :both]] + (do-game + (new-game {:runner {:hand [(qty "Sure Gamble" 5) "No One Home" "No One Home"]} + :corp {:hand ["Snare!"]}}) + (take-credits state :corp) + (play-from-hand state :runner "No One Home") + (play-from-hand state :runner "No One Home") + (run-empty-server state :hq) + (click-prompt state :corp "Yes") + ;; tag first + (if (not= scenario :damage) + (do (click-prompts state :runner "No One Home" "Yes") + (click-prompt state :corp "0") + (click-prompts state :runner "0" "1") + (is (= 0 (count-tags state)) "0 tags")) + (do (click-prompt state :runner "Allow 1 remaining tag") + (is (= 1 (count-tags state)) "1 tag"))) + (if (not= scenario :tag) + (do (click-prompts state :runner "No One Home" "Yes") + (click-prompt state :corp "0") + (click-prompts state :runner "0" "3") + (is (>= 2 (count (:discard (get-runner)))) "0 damage")) + (do (click-prompt state :runner "Pass priority") + (is (<= 3 (count (:discard (get-runner)))) "Took 3 damage")))))) + (deftest off-campus-apartment-ability-shows-a-simultaneous-resolution-prompt-when-appropriate ;; ability shows a simultaneous resolution prompt when appropriate (do-game @@ -4963,8 +5019,7 @@ (take-credits state :runner) (click-card state :runner (refresh misd)) (is (not (no-prompt? state :runner)) "Prompt to prevent trashing with Sacrificial Construct") - (card-ability state :runner sac 0) - (click-prompt state :runner "Done") + (click-prompt state :runner "Sacrificial Construct") (take-credits state :corp) (is (changed? [(:credit (get-runner)) 0] (play-from-hand state :runner "Corroder") @@ -5554,11 +5609,11 @@ (trash state :runner (get-resource state 2)) (is (no-prompt? state :runner) "Sac Con not prompting to prevent resource trash") (trash state :runner (get-program state 0)) - (card-ability state :runner (get-resource state 0) 0) + (click-prompt state :runner "Sacrificial Construct") (is (= 2 (count (:discard (get-runner)))) "Sac Con trashed") (is (= 1 (count (get-program state))) "Cache still installed") (trash state :runner (get-hardware state 0)) - (card-ability state :runner (get-resource state 0) 0) + (click-prompt state :runner "Sacrificial Construct") (is (= 3 (count (:discard (get-runner)))) "Sac Con trashed") (is (= 1 (count (get-hardware state))) "Astrolabe still installed"))) @@ -6270,7 +6325,7 @@ (play-from-hand state :runner "Fall Guy") (is (= 4 (:credit (get-runner)))) (let [fall (get-resource state 1)] - (card-ability state :runner fall 1) + (card-ability state :runner fall 0) (is (= 7 (:credit (get-runner))))))) (deftest technical-writer @@ -7181,7 +7236,8 @@ (take-credits state :runner) (is (= 3 (:credit (get-runner))) "Runner is now at 3 credits") (gain-tags state :corp 1) - (card-ability state :runner (get-resource state 1) 0) + (click-prompt state :runner "New Angeles City Hall") + (click-prompt state :runner "Yes") (click-card state :runner "Corroder") (is (zero? (:credit (get-runner))) "Runner paid one less to install") (is (= "Corroder" (:title (get-program state 0))) "Corroder is installed"))) @@ -7606,14 +7662,14 @@ (play-from-hand state :runner "Fall Guy") (play-from-hand state :runner "Fall Guy") (play-from-hand state :runner "Fall Guy") - (card-ability state :runner (get-resource state 1) 1) + (card-ability state :runner (get-resource state 1) 0) (is (= 2 (count (:discard (get-runner)))) "Fall Guy trashed") (is (= 3 (:credit (get-runner))) "Gained 2c from Fall Guy and 1c from Wasteland") (take-credits state :runner) - (card-ability state :runner (get-resource state 1) 1) + (card-ability state :runner (get-resource state 1) 0) (is (= 3 (count (:discard (get-runner)))) "Fall Guy trashed") (is (= 6 (:credit (get-runner))) "Gained 2c from Fall Guy and 1c from Wasteland") - (card-ability state :runner (get-resource state 1) 1) + (card-ability state :runner (get-resource state 1) 0) (is (= 4 (count (:discard (get-runner)))) "Fall Guy trashed") (is (= 8 (:credit (get-runner))) "Gained 2c from Fall Guy but no credits from Wasteland"))) diff --git a/test/clj/game/cards/upgrades_test.clj b/test/clj/game/cards/upgrades_test.clj index c98a362b08..f89cb1f163 100644 --- a/test/clj/game/cards/upgrades_test.clj +++ b/test/clj/game/cards/upgrades_test.clj @@ -4180,18 +4180,16 @@ (play-from-hand state :runner "Net Shield") (run-on state "HQ") (let [pup (get-ice state :hq 0) - tori (get-content state :hq 0) - nshld (get-program state 0)] + tori (get-content state :hq 0)] (rez state :corp pup) (rez state :corp tori) (run-continue state) (card-subroutine state :corp pup 0) (click-prompt state :runner "Suffer 1 net damage") - (card-ability state :runner nshld 0) + (click-prompt state :runner "Net Shield") (is (empty? (:discard (get-runner))) "1 net damage prevented") (card-subroutine state :corp pup 0) (click-prompt state :runner "Suffer 1 net damage") - (click-prompt state :runner "Done") ; decline to prevent (is (= 1 (count (:discard (get-runner)))) "1 net damage; previous prevention stopped Tori ability") (run-continue state :movement) (run-jack-out state) @@ -4199,7 +4197,7 @@ (run-continue state) (card-subroutine state :corp pup 0) (click-prompt state :runner "Suffer 1 net damage") - (click-prompt state :runner "Done") + (click-prompt state :corp "Tori Hanzō") (click-prompt state :corp "Yes") (is (= 2 (count (:discard (get-runner)))) "1 core damage suffered") (is (= 1 (:brain-damage (get-runner))))))) @@ -4218,14 +4216,15 @@ (rez state :corp hg) (rez state :corp tori) (run-continue state) - (click-prompt state :corp "No") ; Tori prompt to pay 2c to replace 1 net with 1 brain + (click-prompt state :corp "Pass priority") ;; Tori prompt to pay 2c to replace 1 net with 1 brain (is (= 1 (count (:discard (get-runner)))) "1 net damage suffered") (click-prompt state :runner "Hokusai Grid") (click-prompt state :runner "No action") (click-prompt state :runner "No action") (is (no-prompt? state :corp) "No prompts, run ended") (run-empty-server state "Archives") - (click-prompt state :corp "Yes") ; Tori prompt to pay 2c to replace 1 net with 1 brain + (click-prompt state :corp "Tori Hanzō") + (click-prompt state :corp "Yes") ;; Tori prompt to pay 2c to replace 1 net with 1 brain (is (= 2 (count (:discard (get-runner))))) (is (= 1 (:brain-damage (get-runner))) "1 core damage suffered") (click-prompt state :runner "Hokusai Grid") @@ -4249,6 +4248,7 @@ (run-continue state) (card-subroutine state :corp pup 0) (click-prompt state :runner "Suffer 1 net damage") + (click-prompt state :corp "Tori Hanzō") (click-prompt state :corp "Yes") ; pay 2c to replace 1 net with 1 brain (is (= 1 (count (:discard (get-runner)))) "1 core damage suffered") (is (= 1 (:brain-damage (get-runner)))) @@ -4807,13 +4807,13 @@ (rez state :corp (get-ice state :remote1 0)) (rez state :corp (get-content state :remote1 0)) (run-continue state :encounter-ice) - (click-prompt state :corp "ZATO Ability") + (click-prompt state :corp "ZATO City Grid encounter") (click-prompt state :corp "No") (click-prompt state :runner "End the run") (is (not (:run @state)) "Run ended") (run-on state :remote1) (run-continue state :encounter-ice) - (click-prompt state :corp "Funhouse Ability") + (click-prompt state :corp "Funhouse encounter") (click-prompt state :runner "End the run") (is (not (:run @state)) "Run ended") (is (no-prompt? state :corp)))) diff --git a/test/clj/game/core/abilities_test.clj b/test/clj/game/core/abilities_test.clj index 46b672f618..d61e7a0485 100644 --- a/test/clj/game/core/abilities_test.clj +++ b/test/clj/game/core/abilities_test.clj @@ -59,7 +59,8 @@ (doseq [card (->> (vals @all-cards) (filter #(re-find #"(?i)\[trash\].*:" (:text % "")))) :when (not-empty (card-def card))] - (is (core/has-trash-ability? card) (str (:title card) " needs either :cost [(->c :trash-can)] or :trash-icon true")))) + (is (core/has-trash-ability? card) + (str (:title card) " needs either :cost [(->c :trash-can)] or :trash-icon true")))) (defn- x-has-labels [x-key x-name] diff --git a/test/clj/game/core/rules_test.clj b/test/clj/game/core/rules_test.clj index fec4dfbf41..e574b21d9d 100644 --- a/test/clj/game/core/rules_test.clj +++ b/test/clj/game/core/rules_test.clj @@ -700,11 +700,13 @@ (click-prompt state :runner "No action") (run-empty-server state "HQ") (play-from-hand state :runner "Apocalypse") - (is (= #{"Hostile Infrastructure" "Marilyn Campaign" "Calvin B4L3Y"} + (is (= #{"Shuffle Marilyn Campaign into R&D" "Allow 3 cards to be trashed"} + (into #{} (prompt-titles :corp))) + "Corp only has the marilyn interrupt") + (click-prompt state :corp "Shuffle Marilyn Campaign into R&D") + (is (= #{"Hostile Infrastructure""Calvin B4L3Y"} (into #{} (prompt-titles :corp))) "Corp has the simultaneous prompt") - (click-prompt state :corp "Marilyn Campaign") - (click-prompt state :corp "Yes") (click-prompt state :corp "Calvin B4L3Y") (click-prompt state :corp "Yes"))) diff --git a/test/clj/game/core/scenarios_test.clj b/test/clj/game/core/scenarios_test.clj index d24a4fdf67..695cffa69d 100644 --- a/test/clj/game/core/scenarios_test.clj +++ b/test/clj/game/core/scenarios_test.clj @@ -455,9 +455,8 @@ (take-credits state :runner) (is (= 6 (:credit (get-runner)))) (play-from-hand state :corp "Neural EMP") - (let [ns (get-program state 0) - fg (first (:hosted (refresh apt)))] - (card-ability state :runner ns 0) + (let [fg (first (:hosted (refresh apt)))] + (click-prompt state :runner "Net Shield") (is (= 5 (:credit (get-runner))) "Runner paid 1c to survive Neural EMP") (play-from-hand state :corp "SEA Source") (click-prompt state :corp "3") ; boost trace to 6 @@ -467,7 +466,7 @@ (trash-resource state) (click-card state :corp "Off-Campus Apartment") (is (= 3 (:credit (get-corp))) "WNP increased cost to trash a resource by 2") - (card-ability state :runner fg 0) ; Trash Fall Guy to save the Apartment! + (click-prompt state :runner "Fall Guy") ;; Trash Fall Guy to save the Apartment! (is (= (:title (get-resource state 0)) "Off-Campus Apartment") "Apartment still standing") (is (= (:title (last (:discard (get-runner)))) "Fall Guy") "Fall Guy trashed"))))))