From 1e48a1a50853adad71e79a89b3842ad64a0b6ce4 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 12:03:36 +1300 Subject: [PATCH 01/38] first steps on making a prevention framework --- src/clj/game/cards/hardware.clj | 60 +++++++++++++----------- src/clj/game/cards/ice.clj | 4 +- src/clj/game/cards/identities.clj | 13 ++++-- src/clj/game/core/effects.clj | 17 ++++++- src/clj/game/core/engine.clj | 9 ++++ src/clj/game/core/prevention.clj | 77 +++++++++++++++++++++++++++++++ src/clj/game/core/tags.clj | 32 +++---------- src/clj/game/core/turns.clj | 9 +++- 8 files changed, 160 insertions(+), 61 deletions(-) create mode 100644 src/clj/game/core/prevention.clj diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index f465efa40a..7521126049 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -17,7 +17,7 @@ reorder-choice 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]] + 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,6 +42,7 @@ [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 [prevent-tag]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal]] @@ -742,8 +743,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)) @@ -752,14 +762,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 @@ -1843,24 +1846,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)]}) diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index 45600cd3fd..d7d08600a5 100644 --- a/src/clj/game/cards/ice.clj +++ b/src/clj/game/cards/ice.clj @@ -19,7 +19,7 @@ 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-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 @@ -1843,7 +1843,7 @@ (decapitalize target))) :player :runner :prompt "Choose one" - :choices (req [(when-not (forced-to-avoid-tags? state side) + :choices (req [(when-not (any-effects state :runner :forced-to-avoid-tag) "Take 1 tag") "End the run"]) :waiting-prompt true diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index ed96cba991..fca1c689de 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -38,6 +38,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]] @@ -1028,12 +1029,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 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 714d2d63fc..42a14294e3 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] diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj new file mode 100644 index 0000000000..38fd9fa264 --- /dev/null +++ b/src/clj/game/core/prevention.clj @@ -0,0 +1,77 @@ +(ns game.core.prevention + (:require + [game.core.eid :refer [complete-with-result effect-completed]] + [game.core.engine :refer [trigger-event-simult trigger-event-sync]] + [game.utils :refer [dissoc-in]] + [game.macros :refer [wait-for]])) + +;; so how is this going to work? +;; each player, starting with the active player, gets a chance to prevent effects +;; we get a list of all cards that have prevention effects, and create a prompt with all the specific prevention abilities, along with: +;; * are they repeatable? +;; * the source card +;; * is it an ability, an interrupt, or a triggered event? +;; +;; Relevant Cards: +;; Jesminder, Quianju PT (forced interrupts) - these are pre-tag events, we can skip them +;; Forger (interrupt, ability) - move this to the pre-tag event, then we can skip it +;; No One Home (event, avoid any number of tags) +;; On the lam (ability, avoid up to three tags) +;; Decoy (ability, avoid up to 1 tags) +;; New Angeles City Hall (ability, avoid 1 tag, repeatable) +;; Dorm Computer (aura/event, avoid all tags) +;; +;; Plan of attack: +;; * Rework Forger to be an interrupt +;; * Rework NOH, On the Lam, Decoy, NACH, Dorm Computer to have prevention +;; abilities I can scry the state for, like: +;; * Rework the 'forced-to-avoid-tag' flag as a static-ability (see jesminder) +;; +;; :prevention [{:type tag +;; :type :ability +;; :label "(No One Home) Avoid any number of tags" +;; :ability {:optional true +;; :cost [(->c :trash-can 1)] +;; :req (req (and (no-event state side :tag) +;; (no-event state side :net-damage))) +;; :optional true +;; :yes-ability {:async true +;; :effect (req (do-whatever-trace))}] +;; +;; :prevention [{:prevent tag +;; :type :effect +;; :mandatory true +;; :max-uses 1 +;; :label "(Dorm Computer) Avoid all tags" +;; :ability {:msg "avoid all tags" +;; :req (req (this-card-is-run-source state)) ;; - dorm computer +;; :effect (req (prevent state side :tag :all))}}] +(defn prevent-tag + [state side eid n] + (if (get-in @state [:prevent :tags]) + (do (if (= n :all) + (swap! state update-in [:prevent :tags] merge {:prevented :all :remaining 0}) + (do (swap! state update-in [:prevent :tags :prevented] + n) + (swap! state update-in [:prevent :tags :remaining] #(max 0 (- % n))))) + (trigger-event-sync state side eid (if (= side :corp) :corp-prevent :runner-prevent) {:type :tag + :amount n})) + (do (println "tried to prevent tags outside of a tag prevention window") + (effect-completed state side eid)))) + +(defn- fetch-and-clear! + [state key] + (let [res (get-in @state [:prevent key])] + (swap! state dissoc-in [:prevent key]) + res)) + +(defn resolve-tag-prevention + [state side eid n {:keys [unpreventable card] :as args}] + (swap! state assoc-in [:prevent :tags] + {:count n :remaining n :prevented 0 :source-player side :source-card card}) + (if (or unpreventable (not (pos? n))) + (complete-with-result state side eid (fetch-and-clear! state :tags)) + ;; this should hit forger, jesminder, quianju pt + ;; then we check if remaining is > 0 + (wait-for (trigger-event-simult state side :tag-interrupt nil card) + (println (get-in @state [:prevent])) + (complete-with-result state side eid (fetch-and-clear! state :tags))))) diff --git a/src/clj/game/core/tags.clj b/src/clj/game/core/tags.clj index 35fbdfbce8..3adbc64a96 100644 --- a/src/clj/game/core/tags.clj +++ b/src/clj/game/core/tags.clj @@ -5,6 +5,7 @@ [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,12 +36,14 @@ :is-tagged is-tagged?})) changed?))) +;; this can also be cut (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})) +;; this can actually be cut entirely (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]}] @@ -70,33 +73,12 @@ ([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})) + (let [n (number-of-tags-to-gain state side n args)] + (wait-for + (resolve-tag-prevention state side n args) (resolve-tag state side eid {:suppress-checkpoint suppress-checkpoint :card card - :n n})))))) + :n (:count 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..51b8893890 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) From 76edc439755071848aa4273c371305ae7763edf8 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 12:24:32 +1300 Subject: [PATCH 02/38] forger reworked as an interrupt --- src/clj/game/cards/hardware.clj | 37 ++++++++++++++++++++------------- src/clj/game/core/tags.clj | 2 +- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 7521126049..3dd6cd066e 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -13,10 +13,10 @@ [game.core.cost-fns :refer [install-cost rez-additional-cost-bonus rez-cost trash-cost]] [game.core.damage :refer [chosen-damage damage damage-prevent enable-runner-damage-choice runner-can-choose-damage?]] - [game.core.def-helpers :refer [breach-access-bonus defcard offer-jack-out + [game.core.def-helpers :refer [breach-access-bonus choose-one-helper defcard offer-jack-out reorder-choice trash-on-empty get-x-fn]] [game.core.drawing :refer [draw]] - [game.core.effects :refer [register-lingering-effect + [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 @@ -911,19 +911,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 :tags :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)) diff --git a/src/clj/game/core/tags.clj b/src/clj/game/core/tags.clj index 3adbc64a96..2ceb88df50 100644 --- a/src/clj/game/core/tags.clj +++ b/src/clj/game/core/tags.clj @@ -78,7 +78,7 @@ (resolve-tag-prevention state side n args) (resolve-tag state side eid {:suppress-checkpoint suppress-checkpoint :card card - :n (:count async-result)})))))) + :n (:remaining async-result)})))))) (defn lose-tags "Always removes `:base` tags" From 1b137f674220196514b6575179ef49d59f97e2f3 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 16:21:09 +1300 Subject: [PATCH 03/38] tag prevention is updated --- src/clj/game/cards/events.clj | 17 ++--- src/clj/game/cards/ice.clj | 5 +- src/clj/game/cards/resources.clj | 66 ++++++++++++++---- src/clj/game/core/costs.clj | 2 +- src/clj/game/core/prevention.clj | 112 +++++++++++++++++++++++++++---- 5 files changed, 162 insertions(+), 40 deletions(-) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index fd6a8cf780..983f132783 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -49,6 +49,7 @@ swap-ice trash trash-cards]] [game.core.payment :refer [can-pay? ->c]] [game.core.play-instants :refer [play-instant]] + [game.core.prevention :refer [prevent-up-to-n-tags]] [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]] @@ -2684,21 +2685,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 + :label "On the Lam" + :choice "Trash On the Lam to avoid up to 3 tags?" + :ability (assoc (prevent-up-to-n-tags 3) :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} + :interactions {:prevent [{:type #{:net :brain :meat} :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" + :abilities [{:label "Prevent up to 3 damage" :msg "prevent up to 3 damage" :cost [(->c :trash-can)] :effect (effect (damage-prevent :net 3) diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index d7d08600a5..9288d50fae 100644 --- a/src/clj/game/cards/ice.clj +++ b/src/clj/game/cards/ice.clj @@ -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 @@ -1843,7 +1842,7 @@ (decapitalize target))) :player :runner :prompt "Choose one" - :choices (req [(when-not (any-effects state :runner :forced-to-avoid-tag) + :choices (req [(when-not (forced-to-avoid-tags? state :runner) "Take 1 tag") "End the run"]) :waiting-prompt true diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index a2eb769e24..abd83738d7 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -58,6 +58,7 @@ [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 [prevent-tag prevent-up-to-n-tags]] [game.core.prompts :refer [cancellable]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal reveal-loud]] @@ -1126,12 +1127,14 @@ :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" + :choice "Trash Decoy to avoid 1 tag?" + :ability {:async true + :cost [(->c :trash-can)] + :msg "avoid 1 tag" + :effect (req (prevent-tag state :runner eid 1))}}]}) (defcard "District 99" (letfn [(eligible-cards [runner] @@ -2335,16 +2338,32 @@ :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 :tags :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 :tags :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" + :choice "Pay 2 [Credits] to avoid a tag?" + :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)))}}] :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]" @@ -2376,7 +2395,26 @@ (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} + {:prevention [{:prevents :tag + :type :event + :label "No One Home" + :choice "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) + ;; note that the checkpoints are suppressed for both damage and tag when resolving a snare, + ;; (or at least they will be after the costs merge), so this should work + (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)))}}] + :interactions {:prevent [{:type #{:net} :req (req (first-chance? state side))}]} :abilities [{:msg "force the Corp to trace" :async true diff --git a/src/clj/game/core/costs.clj b/src/clj/game/core/costs.clj index 770a8e8261..3dc65af79e 100644 --- a/src/clj/game/core/costs.clj +++ b/src/clj/game/core/costs.clj @@ -256,7 +256,7 @@ [cost state side eid card] (wait-for (trash state side card {:cause :ability-cost :unpreventable 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/prevention.clj b/src/clj/game/core/prevention.clj index 38fd9fa264..d60fe6f626 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -1,9 +1,16 @@ (ns game.core.prevention (:require + [game.core.board :refer [all-installed]] + [game.core.card-defs :refer [card-def]] + [game.core.cost-fns :refer [card-ability-cost]] + [game.core.def-helpers :refer [choose-one-helper]] [game.core.eid :refer [complete-with-result effect-completed]] - [game.core.engine :refer [trigger-event-simult trigger-event-sync]] - [game.utils :refer [dissoc-in]] - [game.macros :refer [wait-for]])) + [game.core.effects :refer [any-effects]] + [game.core.engine :refer [resolve-ability trigger-event-simult trigger-event-sync]] + [game.core.payment :refer [can-pay?]] + [game.utils :refer [dissoc-in quantify]] + [game.macros :refer [msg req wait-for]] + [jinteki.utils :refer [other-side]])) ;; so how is this going to work? ;; each player, starting with the active player, gets a chance to prevent effects @@ -14,18 +21,18 @@ ;; ;; Relevant Cards: ;; Jesminder, Quianju PT (forced interrupts) - these are pre-tag events, we can skip them -;; Forger (interrupt, ability) - move this to the pre-tag event, then we can skip it -;; No One Home (event, avoid any number of tags) -;; On the lam (ability, avoid up to three tags) -;; Decoy (ability, avoid up to 1 tags) +;; Forger (interrupt, ability) - move this to the pre-tag event/interrupt, then we can skip it +;; No One Home (event, avoid any number of tags) (done) +;; On the lam (ability, avoid up to three tags) (done) +;; Decoy (ability, avoid up to 1 tags) - done ;; New Angeles City Hall (ability, avoid 1 tag, repeatable) ;; Dorm Computer (aura/event, avoid all tags) ;; ;; Plan of attack: -;; * Rework Forger to be an interrupt +;; * Rework Forger to be an interrupt (done) +;; * Rework the 'forced-to-avoid-tag' flag as a static-ability (see jesminder) (done) ;; * Rework NOH, On the Lam, Decoy, NACH, Dorm Computer to have prevention ;; abilities I can scry the state for, like: -;; * Rework the 'forced-to-avoid-tag' flag as a static-ability (see jesminder) ;; ;; :prevention [{:type tag ;; :type :ability @@ -46,6 +53,29 @@ ;; :ability {:msg "avoid all tags" ;; :req (req (this-card-is-run-source state)) ;; - dorm computer ;; :effect (req (prevent state side :tag :all))}}] + +(defn- relevant-prevention-abilities + [state side eid key card] + (let [abs (filter #(= (:prevents %) key) (:prevention (card-def card))) + with-card (map #(assoc % :card card) abs) + ;; filter only to ones that are playable and can be played + 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 :tags :uses (:cid card)])) + (< (get-in @state [:prevent :tags :uses (:cid card)]) (:max-uses %))) + ability-req? (or (not (get-in % [:ability :req])) + ((get-in % [:ability :req]) state side eid card nil))] + (and (not cannot-play?) payable? not-used-too-many-times? ability-req?)) + abs)] + (seq (map #(assoc % :card card) playable?)))) + +(defn- gather-prevention-abilities + [state side eid key] + (mapcat #(relevant-prevention-abilities state side eid key %) (all-installed state side))) + (defn prevent-tag [state side eid n] (if (get-in @state [:prevent :tags]) @@ -58,20 +88,74 @@ (do (println "tried to prevent tags outside of a tag prevention window") (effect-completed state side eid)))) +(defn prevent-up-to-n-tags + [n] + (letfn [(remainder [state] (get-in @state [:prevent :tags :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 :tags])) + :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- 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])] (swap! state dissoc-in [:prevent key]) res)) +(defn- build-prevention-option + [prevention] + {:option (:label prevention) + :ability {:async true + :req (:req (:ability prevention)) + :effect (req + (swap! state update-in [:prevent :tags :uses (->> prevention :card :cid)] (fnil inc 0)) + (resolve-ability + state side (assoc eid :source (:card prevention) :source-type :ability) + (if (:choice prevention) + {:optional {:prompt (:choice prevention) + :yes-ability (:ability prevention)}} + (:ability prevention)) + (:card prevention) nil))}}) + +(defn- resolve-tag-prevention-for-side + [state side eid] + (let [remainder (get-in @state [:prevent :tags :remaining])] + (if (or (not (pos? remainder)) (get-in @state [:prevent :tags :passed])) + (do (swap! state dissoc-in [:prevent :tags :passed]) + (effect-completed state side eid)) + (let [preventions (gather-prevention-abilities state side eid :tag)] + (if (empty? preventions) + (effect-completed state side eid) + (wait-for (resolve-ability + state side + (choose-one-helper + {:prompt (str "Prevent any of the " (get-in @state [:prevent :tags :count]) " tags?" + (when-not (= (get-in @state [:prevent :tags :count]) remainder) + (str "(" remainder " remaining)")))} + (concat (mapv build-prevention-option preventions) + [{:option (str "Allow " (quantify remainder "remaining tag")) + :ability {:effect (req (swap! state assoc-in [:prevent :tags :passed] true))}}])) + nil nil) + (resolve-tag-prevention-for-side state side eid))))))) + (defn resolve-tag-prevention [state side eid n {:keys [unpreventable card] :as args}] (swap! state assoc-in [:prevent :tags] - {:count n :remaining n :prevented 0 :source-player side :source-card card}) + {: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 :tags)) - ;; this should hit forger, jesminder, quianju pt - ;; then we check if remaining is > 0 + ;; interrupts and static abilities happen first (wait-for (trigger-event-simult state side :tag-interrupt nil card) - (println (get-in @state [:prevent])) - (complete-with-result state side eid (fetch-and-clear! state :tags))))) + (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 :tags)))))))) From 6ec18464ff788e6054ed562a1a3ba1cca53a88b0 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 17:18:04 +1300 Subject: [PATCH 04/38] cleaned up prevents --- src/clj/game/cards/events.clj | 3 +-- src/clj/game/cards/resources.clj | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 983f132783..a57c85ac12 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -2687,8 +2687,7 @@ (defcard "On the Lam" {:prevention [{:prevents :tag :type :ability - :label "On the Lam" - :choice "Trash On the Lam to avoid up to 3 tags?" + :prompt "Trash On the Lam to avoid up to 3 tags?" :ability (assoc (prevent-up-to-n-tags 3) :cost [(->c :trash-can)])}] :on-play {:prompt "Choose a resource to host On the Lam on" :choices {:card #(and (resource? %) diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index abd83738d7..42965288f4 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -1127,10 +1127,11 @@ :type :credit}}}) (defcard "Decoy" - {:prevention [{:prevents :tag + {:trash-icon true + :prevention [{:prevents :tag :type :ability :label "Decoy" - :choice "Trash Decoy to avoid 1 tag?" + :prompt "Trash Decoy to avoid 1 tag?" :ability {:async true :cost [(->c :trash-can)] :msg "avoid 1 tag" @@ -2354,7 +2355,7 @@ {:prevention [{:prevents :tag :type :ability :label "New Angeles City Hall" - :choice "Pay 2 [Credits] to avoid a tag?" + :prompt "Pay 2 [Credits] to avoid a tag?" :ability {:async true :cost [(->c :credit 2)] :msg "avoid 1 tag" @@ -2398,7 +2399,7 @@ {:prevention [{:prevents :tag :type :event :label "No One Home" - :choice "Trash No One Home to force the Corp to trace" + :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) From 81c8ddbfb427b74f95700dcd0814662bd30454fc Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 17:18:18 +1300 Subject: [PATCH 05/38] allow specify waiting msg --- src/clj/game/core/def_helpers.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/game/core/def_helpers.clj b/src/clj/game/core/def_helpers.clj index 0a092d1b9e..d27a5eafd5 100644 --- a/src/clj/game/core/def_helpers.clj +++ b/src/clj/game/core/def_helpers.clj @@ -327,7 +327,7 @@ (merge base-map {:choices (req (into [] (map #(choices-fn % state side eid card targets) xs))) - :waiting-prompt (not no-wait-msg) + :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)"))) From 06dbb38960a05d187ee84eab153f587bd176472c Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 17:18:59 +1300 Subject: [PATCH 06/38] waiting promp, pick printed title if no label --- src/clj/game/core/prevention.clj | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index d60fe6f626..54f6bc5b20 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -110,15 +110,15 @@ (defn- build-prevention-option [prevention] - {:option (:label prevention) + {:option (or (:label prevention) (->> prevention :card :printed-title)) :ability {:async true :req (:req (:ability prevention)) :effect (req (swap! state update-in [:prevent :tags :uses (->> prevention :card :cid)] (fnil inc 0)) (resolve-ability state side (assoc eid :source (:card prevention) :source-type :ability) - (if (:choice prevention) - {:optional {:prompt (:choice prevention) + (if (:prompt prevention) + {:optional {:prompt (:prompt prevention) :yes-ability (:ability prevention)}} (:ability prevention)) (:card prevention) nil))}}) @@ -137,7 +137,8 @@ (choose-one-helper {:prompt (str "Prevent any of the " (get-in @state [:prevent :tags :count]) " tags?" (when-not (= (get-in @state [:prevent :tags :count]) remainder) - (str "(" remainder " remaining)")))} + (str "(" remainder " remaining)"))) + :waiting-prompt "your opponent to prevent tags"} (concat (mapv build-prevention-option preventions) [{:option (str "Allow " (quantify remainder "remaining tag")) :ability {:effect (req (swap! state assoc-in [:prevent :tags :passed] true))}}])) From 75715d2c202eb83e1390fe3be6bea3739913d134 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 17:19:19 +1300 Subject: [PATCH 07/38] fix unit tests for updated card behaviour --- test/clj/game/cards/assets_test.clj | 6 +++--- test/clj/game/cards/events_test.clj | 19 ++++++++++--------- test/clj/game/cards/hardware_test.clj | 5 +++-- test/clj/game/cards/operations_test.clj | 5 +++-- test/clj/game/cards/resources_test.clj | 23 +++++++++++++---------- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/test/clj/game/cards/assets_test.clj b/test/clj/game/cards/assets_test.clj index 561ba73f4e..d73178b2d5 100644 --- a/test/clj/game/cards/assets_test.clj +++ b/test/clj/game/cards/assets_test.clj @@ -3778,16 +3778,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")))) diff --git a/test/clj/game/cards/events_test.clj b/test/clj/game/cards/events_test.clj index 513128492a..2ea27c6277 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"))) @@ -5015,8 +5014,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"))) @@ -5032,7 +5032,7 @@ (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) + (card-ability state :runner (-> (get-resource state 0) :hosted first) 0) (is (zero? (count-tags state)) "Runner should avoid all meat damage") (is (= 1 (-> (get-runner) :discard count)) "Runner should have 1 card in Heap"))) @@ -5061,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 diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index ef4ae3c59e..d7cdfdf3de 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -382,8 +382,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"))) diff --git a/test/clj/game/cards/operations_test.clj b/test/clj/game/cards/operations_test.clj index 38a3309dcd..5d832c9e55 100644 --- a/test/clj/game/cards/operations_test.clj +++ b/test/clj/game/cards/operations_test.clj @@ -3222,10 +3222,11 @@ (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-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 "2") (click-prompt state :corp "Yes")) "Runner prevented 2 tag"))) diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 66e0789af2..b8f2e2225a 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -1692,7 +1692,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"))) @@ -4557,8 +4558,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) @@ -4590,9 +4591,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")))) @@ -4645,10 +4646,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) @@ -4656,7 +4658,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")) @@ -7173,7 +7175,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"))) From 6919e2592bff76f2ffd101bbfae6622e2afac1f8 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 17:19:35 +1300 Subject: [PATCH 08/38] if there aren't any abilities, ignore the trash icon thing --- test/clj/game/core/abilities_test.clj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/clj/game/core/abilities_test.clj b/test/clj/game/core/abilities_test.clj index 46b672f618..ff2e3484f4 100644 --- a/test/clj/game/core/abilities_test.clj +++ b/test/clj/game/core/abilities_test.clj @@ -59,7 +59,9 @@ (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 (or (core/has-trash-ability? card) + (zero? (count (:abilities card)))) + (str (:title card) " needs either :cost [(->c :trash-can)] or :trash-icon true")))) (defn- x-has-labels [x-key x-name] From 3c6695e74d9f85ff9df5d286f6971b17aca46a9e Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 18:12:41 +1300 Subject: [PATCH 09/38] simplified tags file --- src/clj/game/core/tags.clj | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/clj/game/core/tags.clj b/src/clj/game/core/tags.clj index 2ceb88df50..a5b77c28de 100644 --- a/src/clj/game/core/tags.clj +++ b/src/clj/game/core/tags.clj @@ -36,22 +36,6 @@ :is-tagged is-tagged?})) changed?))) -;; this can also be cut -(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})) - -;; this can actually be cut entirely -(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]}] @@ -71,14 +55,11 @@ "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)] - (wait-for - (resolve-tag-prevention state side n args) - (resolve-tag state side eid {:suppress-checkpoint suppress-checkpoint - :card card - :n (:remaining async-result)})))))) + (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" From bc47a0ed999dfb16f32391bc7bbd3ec399d7298c Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 18:12:56 +1300 Subject: [PATCH 10/38] removed dead references to tag-prevent --- src/clj/game/cards/events.clj | 2 +- src/clj/game/cards/hardware.clj | 2 +- src/clj/game/cards/identities.clj | 2 +- src/clj/game/cards/programs.clj | 12 +++++------- src/clj/game/cards/resources.clj | 8 +++----- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index a57c85ac12..fee2c77d50 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -64,7 +64,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]] diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 3dd6cd066e..34aa4cd41a 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -53,7 +53,7 @@ [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]] diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index fca1c689de..3fcc72b4a0 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -52,7 +52,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!]] diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index 237ddcf03a..a9bcdafcbd 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -1506,13 +1506,11 @@ (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 (add-counter state side card :virus 2) - (system-msg state side - (str "takes 1 tag to place 2 virus counters on God of War")) - (effect-completed state side eid)) - (effect-completed state side eid))))}] + :effect (req (wait-for (gain-tags state :runner 1 {:unpreventable true}) + (add-counter state side card :virus 2) + (system-msg state side + (str "takes 1 tag to place 2 virus counters on God of War")) + (effect-completed state side eid)))}] {:flags {:runner-phase-12 (req true)} :events [(choose-one-helper {:event :runner-turn-begins diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 42965288f4..77e53db470 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -75,7 +75,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]] @@ -2392,10 +2392,8 @@ :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)))}}}))] + :effect (req (do (damage-prevent state :runner :net Integer/MAX_VALUE) + (effect-completed state side eid)))}}}))] {:prevention [{:prevents :tag :type :event :label "No One Home" From 5d6c3bdb5b41e7dcafbe0aaf02eae6c2f3c5e845 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 18:13:25 +1300 Subject: [PATCH 11/38] cleanup --- src/clj/game/core.clj | 3 +-- src/clj/game/core/prevention.clj | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/clj/game/core.clj b/src/clj/game/core.clj index 7effe0352d..d1858d9fe1 100644 --- a/src/clj/game/core.clj +++ b/src/clj/game/core.clj @@ -803,8 +803,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/prevention.clj b/src/clj/game/core/prevention.clj index 54f6bc5b20..4682fd8fe0 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -55,10 +55,14 @@ ;; :effect (req (prevent state side :tag :all))}}] (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) - ;; filter only to ones that are playable and can be played 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 []))) From ce754898810a357f92dfb897cab61a6e3519db04 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 18:24:40 +1300 Subject: [PATCH 12/38] cleanup/sorting for re-use --- src/clj/game/core/prevention.clj | 93 ++++++++++++-------------------- 1 file changed, 33 insertions(+), 60 deletions(-) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 4682fd8fe0..2ec26a4945 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -18,41 +18,6 @@ ;; * are they repeatable? ;; * the source card ;; * is it an ability, an interrupt, or a triggered event? -;; -;; Relevant Cards: -;; Jesminder, Quianju PT (forced interrupts) - these are pre-tag events, we can skip them -;; Forger (interrupt, ability) - move this to the pre-tag event/interrupt, then we can skip it -;; No One Home (event, avoid any number of tags) (done) -;; On the lam (ability, avoid up to three tags) (done) -;; Decoy (ability, avoid up to 1 tags) - done -;; New Angeles City Hall (ability, avoid 1 tag, repeatable) -;; Dorm Computer (aura/event, avoid all tags) -;; -;; Plan of attack: -;; * Rework Forger to be an interrupt (done) -;; * Rework the 'forced-to-avoid-tag' flag as a static-ability (see jesminder) (done) -;; * Rework NOH, On the Lam, Decoy, NACH, Dorm Computer to have prevention -;; abilities I can scry the state for, like: -;; -;; :prevention [{:type tag -;; :type :ability -;; :label "(No One Home) Avoid any number of tags" -;; :ability {:optional true -;; :cost [(->c :trash-can 1)] -;; :req (req (and (no-event state side :tag) -;; (no-event state side :net-damage))) -;; :optional true -;; :yes-ability {:async true -;; :effect (req (do-whatever-trace))}] -;; -;; :prevention [{:prevent tag -;; :type :effect -;; :mandatory true -;; :max-uses 1 -;; :label "(Dorm Computer) Avoid all tags" -;; :ability {:msg "avoid all tags" -;; :req (req (this-card-is-run-source state)) ;; - dorm computer -;; :effect (req (prevent state side :tag :all))}}] (defn- relevant-prevention-abilities "selects all prevention abilities which are: @@ -80,31 +45,18 @@ [state side eid key] (mapcat #(relevant-prevention-abilities state side eid key %) (all-installed state side))) -(defn prevent-tag - [state side eid n] - (if (get-in @state [:prevent :tags]) +(defn prevent-numeric + [state side eid key ev n] + (if (get-in @state [:prevent key]) (do (if (= n :all) - (swap! state update-in [:prevent :tags] merge {:prevented :all :remaining 0}) - (do (swap! state update-in [:prevent :tags :prevented] + n) - (swap! state update-in [:prevent :tags :remaining] #(max 0 (- % n))))) - (trigger-event-sync state side eid (if (= side :corp) :corp-prevent :runner-prevent) {:type :tag + (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 ev :amount n})) - (do (println "tried to prevent tags outside of a tag prevention window") + (do (println "tried to prevent " (name key) " outside of a " (name key) " prevention window") (effect-completed state side eid)))) -(defn prevent-up-to-n-tags - [n] - (letfn [(remainder [state] (get-in @state [:prevent :tags :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 :tags])) - :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- fetch-and-clear! "get the prevent map for a key and also dissoc it from the state" [state key] @@ -113,12 +65,13 @@ res)) (defn- build-prevention-option - [prevention] + "Builds a menu item for firing a prevention ability" + [prevention key] {:option (or (:label prevention) (->> prevention :card :printed-title)) :ability {:async true :req (:req (:ability prevention)) :effect (req - (swap! state update-in [:prevent :tags :uses (->> prevention :card :cid)] (fnil inc 0)) + (swap! state update-in [:prevent key :uses (->> prevention :card :cid)] (fnil inc 0)) (resolve-ability state side (assoc eid :source (:card prevention) :source-type :ability) (if (:prompt prevention) @@ -127,6 +80,27 @@ (:ability prevention)) (:card prevention) nil))}}) +;; BAD PUBLICITY PREVENTION + +(defn prevent-bad-publicity [state side eid n] (prevent-numeric state side eid :bad-publicity :bad-publicity n)) + +;; TAG PREVENTION + +(defn prevent-tag [state side eid n] (prevent-numeric state side eid :tags :tag n)) + +(defn prevent-up-to-n-tags + [n] + (letfn [(remainder [state] (get-in @state [:prevent :tags :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 :tags])) + :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] (let [remainder (get-in @state [:prevent :tags :remaining])] @@ -143,7 +117,7 @@ (when-not (= (get-in @state [:prevent :tags :count]) remainder) (str "(" remainder " remaining)"))) :waiting-prompt "your opponent to prevent tags"} - (concat (mapv build-prevention-option preventions) + (concat (mapv #(build-prevention-option % :tags) preventions) [{:option (str "Allow " (quantify remainder "remaining tag")) :ability {:effect (req (swap! state assoc-in [:prevent :tags :passed] true))}}])) nil nil) @@ -155,7 +129,6 @@ {: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 :tags)) - ;; interrupts and static abilities happen first (wait-for (trigger-event-simult state side :tag-interrupt nil card) (let [active-side (:active-player @state) responding-side (other-side active-side)] From 8695ca06f529acb724ce99d8617b81a23f04e101 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 19:20:23 +1300 Subject: [PATCH 13/38] bad publicity prevention + more cleanup --- src/clj/game/cards/assets.clj | 17 +++-- src/clj/game/cards/hardware.clj | 2 +- src/clj/game/cards/resources.clj | 4 +- src/clj/game/core.clj | 1 - src/clj/game/core/bad_publicity.clj | 41 +----------- src/clj/game/core/prevention.clj | 99 +++++++++++++++++++++-------- 6 files changed, 88 insertions(+), 76 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index 43bac169c2..c06067237a 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? @@ -46,6 +45,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 [prevent-bad-publicity]] [game.core.revealing :refer [reveal]] [game.core.rezzing :refer [can-pay-to-rez? derez rez]] [game.core.runs :refer [end-run]] @@ -435,11 +435,14 @@ (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 {: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] diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 34aa4cd41a..a981f88ffc 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -918,7 +918,7 @@ :effect (effect (prevent-tag :runner eid 1))}] {:events [(choose-one-helper {:event :tag-interrupt - :req (req (and (pos? (get-in @state [:prevent :tags :remaining])) + :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)} diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 77e53db470..52c8b3e9a2 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -2341,9 +2341,9 @@ (defcard "New Angeles City Hall" (letfn [(prevent-another-tag [] {:optional - {:req (req (and (pos? (get-in @state [:prevent :tags :remaining])) + {: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 :tags :remaining]) " remaining)") + :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" diff --git a/src/clj/game/core.clj b/src/clj/game/core.clj index d1858d9fe1..a03fa73298 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]) diff --git a/src/clj/game/core/bad_publicity.clj b/src/clj/game/core/bad_publicity.clj index 9397a4bec4..10f17d3770 100644 --- a/src/clj/game/core/bad_publicity.clj +++ b/src/clj/game/core/bad_publicity.clj @@ -5,16 +5,11 @@ [game.core.flags :refer [cards-can-prevent? get-prevent-list]] [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] (if (pos? n) @@ -23,43 +18,13 @@ (trigger-event-sync state side (make-result eid n) :corp-gain-bad-publicity {:amount n})) (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)))))))) - (resolve-bad-publicity state side eid n)))))) + (wait-for (resolve-bad-pub-prevention state side n args) + (resolve-bad-publicity state side eid (:remaining async-result))))) (defn lose-bad-publicity ([state side n] (lose-bad-publicity state side (make-eid state) n)) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 2ec26a4945..7f62d6bb61 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -33,8 +33,8 @@ 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 :tags :uses (:cid card)])) - (< (get-in @state [:prevent :tags :uses (:cid card)]) (: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 nil))] (and (not cannot-play?) payable? not-used-too-many-times? ability-req?)) @@ -46,13 +46,13 @@ (mapcat #(relevant-prevention-abilities state side eid key %) (all-installed state side))) (defn prevent-numeric - [state side eid key ev n] + [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 ev + (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") (effect-completed state side eid)))) @@ -64,36 +64,81 @@ (swap! state dissoc-in [:prevent key]) res)) +(defn- trigger-prevention + [state side eid key prevention] + (swap! state update-in [:prevent key :uses (->> prevention :card :cid)] (fnil inc 0)) + (resolve-ability + state side (assoc eid :source (:card prevention) :source-type :ability) + (if (:prompt prevention) + {:optional {:prompt (:prompt prevention) + :yes-ability (:ability prevention)}} + (:ability prevention)) + (: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 :req (:req (:ability prevention)) - :effect (req - (swap! state update-in [:prevent key :uses (->> prevention :card :cid)] (fnil inc 0)) - (resolve-ability - state side (assoc eid :source (:card prevention) :source-type :ability) - (if (:prompt prevention) - {:optional {:prompt (:prompt prevention) - :yes-ability (:ability prevention)}} - (:ability prevention)) - (:card prevention) nil))}}) + :effect (req (trigger-prevention state side eid key prevention))}}) ;; BAD PUBLICITY PREVENTION -(defn prevent-bad-publicity [state side eid n] (prevent-numeric state side eid :bad-publicity :bad-publicity n)) +(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] + (let [remainder (get-in @state [:prevent :bad-publicity :remaining])] + (if (or (not (pos? remainder)) (get-in @state [:prevent :bad-publicity :passed])) + (do (swap! state dissoc-in [:prevent :bad-publicity :passed]) + (effect-completed state side eid)) + (let [preventions (gather-prevention-abilities state side eid :bad-publicity)] + (if (empty? preventions) + (effect-completed state side eid) + ;; TODO - if there's exactly ONE choice, and it's also mandatory, just rip that choice + (if (and (= 1 (count preventions)) + (:mandatory (first preventions))) + (wait-for (trigger-prevention state side :bad-publicity (first preventions)) + (resolve-bad-pub-prevention-for-side state side eid)) + (wait-for (resolve-ability + state side + (choose-one-helper + {:prompt (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-prompt "your opponent to prevent bad publicity"} + (concat (mapv #(build-prevention-option % :bad-publicity) preventions) + [(when-not (some :mandatory preventions) + {:option (str "Allow " remainder " remaining bad publicity") + :ability {:effect (req (swap! state assoc-in [:prevent :bad-publicity :passed] true))}})])) + nil nil) + (resolve-bad-pub-prevention-for-side state side eid)))))))) + +(defn resolve-bad-pub-prevention + [state side eid n {:keys [unpreventable card] :as args}] + (swap! state assoc-in [:prevent :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)) + (let [active-side (:active-player @state) + responding-side (other-side active-side)] + (wait-for + (resolve-bad-pub-prevention-for-side state active-side) + (wait-for + (resolve-bad-pub-prevention-for-side state responding-side) + (complete-with-result state side eid (fetch-and-clear! state :bad-publicity))))))) ;; TAG PREVENTION -(defn prevent-tag [state side eid n] (prevent-numeric state side eid :tags :tag n)) +(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 :tags :remaining])) + (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 :tags])) + :req (req (get-in @state [:prevent :tag])) :choices {:number (req (max-to-avoid state n)) :default (req (max-to-avoid state n))} :async true @@ -103,9 +148,9 @@ (defn- resolve-tag-prevention-for-side [state side eid] - (let [remainder (get-in @state [:prevent :tags :remaining])] - (if (or (not (pos? remainder)) (get-in @state [:prevent :tags :passed])) - (do (swap! state dissoc-in [:prevent :tags :passed]) + (let [remainder (get-in @state [:prevent :tag :remaining])] + (if (or (not (pos? remainder)) (get-in @state [:prevent :tag :passed])) + (do (swap! state dissoc-in [:prevent :tag :passed]) (effect-completed state side eid)) (let [preventions (gather-prevention-abilities state side eid :tag)] (if (empty? preventions) @@ -113,22 +158,22 @@ (wait-for (resolve-ability state side (choose-one-helper - {:prompt (str "Prevent any of the " (get-in @state [:prevent :tags :count]) " tags?" - (when-not (= (get-in @state [:prevent :tags :count]) remainder) + {:prompt (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-prompt "your opponent to prevent tags"} - (concat (mapv #(build-prevention-option % :tags) preventions) + (concat (mapv #(build-prevention-option % :tag) preventions) [{:option (str "Allow " (quantify remainder "remaining tag")) - :ability {:effect (req (swap! state assoc-in [:prevent :tags :passed] true))}}])) + :ability {:effect (req (swap! state assoc-in [:prevent :tag :passed] true))}}])) nil nil) (resolve-tag-prevention-for-side state side eid))))))) (defn resolve-tag-prevention [state side eid n {:keys [unpreventable card] :as args}] - (swap! state assoc-in [:prevent :tags] + (swap! state assoc-in [:prevent :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 :tags)) + (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)] @@ -136,4 +181,4 @@ (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 :tags)))))))) + (complete-with-result state side eid (fetch-and-clear! state :tag)))))))) From 06bfaedb3fbe0fc6e9ecd24afb17fc48e5f59f56 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Mon, 24 Feb 2025 22:29:03 +1300 Subject: [PATCH 14/38] half-way through expose handler --- src/clj/game/cards/assets.clj | 34 +++++++----- src/clj/game/cards/events.clj | 17 +++--- src/clj/game/cards/hardware.clj | 47 ++++++++++++---- src/clj/game/cards/identities.clj | 7 +-- src/clj/game/cards/programs.clj | 6 +- src/clj/game/cards/resources.clj | 2 +- src/clj/game/cards/upgrades.clj | 7 +-- src/clj/game/core.clj | 3 +- src/clj/game/core/engine.clj | 1 + src/clj/game/core/expose.clj | 61 ++++++++------------- src/clj/game/core/prevention.clj | 91 ++++++++++++++++++++++++++++--- src/clj/game/utils.clj | 9 +-- 12 files changed, 184 insertions(+), 101 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index c06067237a..d17a010da4 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -26,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]] @@ -45,7 +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 [prevent-bad-publicity]] + [game.core.prevention :refer [prevent-bad-publicity prevent-expose]] [game.core.revealing :refer [reveal]] [game.core.rezzing :refer [can-pay-to-rez? derez rez]] [game.core.runs :refer [end-run]] @@ -3258,27 +3257,32 @@ (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)] + :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)] + :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 fee2c77d50..9eba262a58 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -1060,7 +1060,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" @@ -1201,6 +1201,7 @@ :effect (req (wait-for (breach-server state side [:hq] {:no-root true}) (breach-server state side eid [:rd] {:no-root true})))}]}) +;; TODO - fix this card (defcard "Drive By" {:on-play {:choices {:card #(let [topmost (get-nested-host %)] @@ -1208,7 +1209,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]) (if-let [target async-result] (if (or (asset? target) (upgrade? target)) @@ -1483,6 +1484,7 @@ card [(breach-access-bonus :hq 1 {:duration :end-of-run})]) (effect-completed state side eid)))}]}) +;; TODO - fix this card (defcard "Falsified Credentials" {:on-play {:prompt "Choose one" @@ -1497,7 +1499,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 @@ -1929,7 +1931,7 @@ {:choices {:card #(and (installed? %) (not (rezzed? %)))} :async true - :effect (effect (expose eid target))} + :effect (effect (expose eid [target] {:card card}))} {:msg "gain 2 [Credits]" :async true :effect (effect (gain-credits eid 2))}) @@ -3498,10 +3500,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" @@ -3699,7 +3698,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" diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index a981f88ffc..4ef9a9a2e2 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -46,7 +46,7 @@ [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.rezzing :refer [can-pay-to-rez? derez rez]] [game.core.runs :refer [bypass-ice end-run end-run-prevent get-current-encounter jack-out make-run successful-run-replace-breach total-cards-accessed]] @@ -239,16 +239,19 @@ :effect (effect (damage eid :brain 2 {:card card}))} :in-play [:click-per-turn 1]}) +;; TODO - if there are multiple cards exposed! (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 +265,26 @@ :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) + :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? @@ -1112,7 +1134,7 @@ :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" @@ -1269,7 +1291,7 @@ :cost [(->c :click 1) (->c :credit 1)] :req (req (some #{:hq} (:successful-run runner-reg))) :choices {:card installed?} - :effect (effect (expose eid target)) + :effect (effect (expose eid [target])) :msg "expose 1 card"}]}) (defcard "LilyPAD" @@ -2582,6 +2604,7 @@ :msg "draw 1 card from the bottom of the stack" :effect (effect (move (last (:deck runner)) :hand))}]}) +;; TODO - add an autoresolve to this, make it work when multiple cards are exposed (defcard "Zamba" {:implementation "Credit gain is automatic" :static-abilities [(mu+ 2)] diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index 3fcc72b4a0..dbb9b2e541 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -110,7 +110,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)] {:card card})) (continue-ability state side {:optional @@ -119,7 +119,7 @@ :player :corp :no-ability {:async true - :effect (effect (expose :runner eid (:card context)))} + :effect (effect (expose :runner eid [(:card context)] {:card card}))} :yes-ability {:async true :effect @@ -1911,8 +1911,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] {:card card}))}]}) (defcard "Skorpios Defense Systems: Persuasive Power" {:implementation "Manually triggered, no restriction on which cards in Heap can be targeted. Cannot use on in progress run event" diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index a9bcdafcbd..9b603e4514 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -529,6 +529,7 @@ :effect (effect (add-counter :runner card :virus 1)) :msg "place 1 virus counter on itself"}]})) +;; TODO - fix this card (defcard "Aumakua" (auto-icebreaker {:implementation "[Erratum] Whenever you finish breaching a server, if you did not steal or trash any accessed cards, place 1 virus counter on this program." :abilities [(break-sub 1 1) @@ -3096,7 +3097,7 @@ {: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" @@ -3440,6 +3441,7 @@ {:req (req (and (= 1 (count (:subroutines current-ice))) (<= (get-strength current-ice) (get-strength card))))})) +;; TODO - fix this card (defcard "Wari" (letfn [(prompt-for-subtype [] {:prompt "Choose one" @@ -3455,7 +3457,7 @@ (not (rezzed? %)))} :async true :msg (str "name " chosen-subtype) - :effect (req (wait-for (expose state side target) + :effect (req (wait-for (expose state side [target]) (when (has-subtype? async-result chosen-subtype) (do (move state :corp async-result :hand) (system-msg state :runner diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 52c8b3e9a2..fd9f4bd423 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -2761,7 +2761,7 @@ :choices {:card installed?} :async true :cost [(->c :trash-can)] - :effect (effect (expose eid target))}]}) + :effect (effect (expose eid [target]))}]}) (defcard "Reclaim" {:abilities diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index 54e886aa64..e932ac0ab0 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!]] @@ -1826,11 +1825,9 @@ :cost [(->c :trash-can)] :msg (msg "prevent a subroutine on " (:title current-ice) " from being broken")}]}) +;; TODO - fix this card (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))}] + {:events [] :static-abilities [{:type :bypass-ice :req (req (same-server? card target)) :value false}]}) diff --git a/src/clj/game/core.clj b/src/clj/game/core.clj index a03fa73298..66a58244e7 100644 --- a/src/clj/game/core.clj +++ b/src/clj/game/core.clj @@ -387,8 +387,7 @@ (expose-vars [game.core.expose - expose - expose-prevent]) + expose]) (expose-vars [game.core.finding diff --git a/src/clj/game/core/engine.clj b/src/clj/game/core/engine.clj index 42a14294e3..9e32cdb4b2 100644 --- a/src/clj/game/core/engine.clj +++ b/src/clj/game/core/engine.clj @@ -623,6 +623,7 @@ :active (active? card) :derezzed (and (installed? card) (not (rezzed? card))) + :installed (installed? card) :facedown (and (installed? card) (facedown? card)) :faceup (and (installed? card) diff --git a/src/clj/game/core/expose.clj b/src/clj/game/core/expose.clj index 838695f87a..4e34edc204 100644 --- a/src/clj/game/core/expose.clj +++ b/src/clj/game/core/expose.clj @@ -3,49 +3,34 @@ [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.engine :refer [checkpoint queue-event register-pending-event resolve-ability trigger-event-sync]] [game.core.flags :refer [cards-can-prevent? get-prevent-list]] + [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 + ;; but if it dies to drive-by, the effect SHOULD fizzle + (register-pending-event state :expose t (assoc ability :condition :installed)))) + (queue-event state :expose {:cards targets}) + (checkpoint state side eid {:duration :expose})))) (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 card] :as args}] + (let [targets (filterv #(not (or (rezzed? %) (nil? %))) 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/prevention.clj b/src/clj/game/core/prevention.clj index 7f62d6bb61..dbade6652b 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -1,6 +1,7 @@ (ns game.core.prevention (:require [game.core.board :refer [all-installed]] + [game.core.card :refer [get-card rezzed? same-card?]] [game.core.card-defs :refer [card-def]] [game.core.cost-fns :refer [card-ability-cost]] [game.core.def-helpers :refer [choose-one-helper]] @@ -8,7 +9,8 @@ [game.core.effects :refer [any-effects]] [game.core.engine :refer [resolve-ability trigger-event-simult trigger-event-sync]] [game.core.payment :refer [can-pay?]] - [game.utils :refer [dissoc-in quantify]] + [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]])) @@ -65,6 +67,7 @@ res)) (defn- trigger-prevention + "Triggers an ability as having prevented something" [state side eid key prevention] (swap! state update-in [:prevent key :uses (->> prevention :card :cid)] (fnil inc 0)) (resolve-ability @@ -83,6 +86,76 @@ :req (:req (:ability prevention)) :effect (req (trigger-prevention state side eid key prevention))}}) +;; 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)))) + + ;; (if ( + ;; [n] + ;; {:prompt (str "Prevent " (quantify n "card") " from being exposed") + ;; :label (str "prevent " (quantify n "card") " from being exposed") + ;; :choices {:req (req (vec-contains-card? (get-in @state [:prevent :expose :remaining]) target)) + ;; :max n + ;; :all true} + ;; :msg (msg "prevent " (enumerate-str (map #(card-str state %) targets)) " from being exposed") + ;; :effect (req (let [remainder (get-in @state [:prevent :expose :remaining]) + ;; remainder (filterv #(not (vec-contains-card? targets %)) remainder)] + ;; (swap! state assoc-in [:prevent :expose :remaining] remainder)))}) + +(defn resolve-expose-prevention-for-side + [state side eid] + (let [remainder (get-in @state [:prevent :expose :remaining])] + (if (or (not (seq remainder)) (get-in @state [:prevent :expose :passed])) + (do (swap! state dissoc-in [:prevent :expose :passed]) + (effect-completed state side eid)) + (let [preventions (gather-prevention-abilities state side eid :expose)] + (if (empty? preventions) + (effect-completed state side eid) + (wait-for (resolve-ability + state side + (choose-one-helper + {:prompt (str "Prevent " (enumerate-str (map #(card-str state % {:visible (= side :corp)}) remainder) "or") " from being exposed?") + :waiting-prompt "your opponent to prevent an Expose"} + (concat (mapv #(build-prevention-option % :expose) preventions) + [{:option (str "Allow " (quantify (count remainder) "card") " to be exposed") + :ability {:effect (req (swap! state assoc-in [:prevent :expose :passed] true))}}])) + nil nil) + (resolve-expose-prevention-for-side state side eid))))))) + +(defn resolve-expose-prevention + [state side eid targets {:keys [unpreventable card] :as args}] + (swap! state assoc-in [:prevent :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 :counnt] (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)) @@ -119,15 +192,15 @@ [state side eid n {:keys [unpreventable card] :as args}] (swap! state assoc-in [:prevent :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)) - (let [active-side (:active-player @state) - responding-side (other-side active-side)] + (if (or unpreventable (not (pos? n))) + (complete-with-result state side eid (fetch-and-clear! state :bad-publicity)) + (let [active-side (:active-player @state) + responding-side (other-side active-side)] + (wait-for + (resolve-bad-pub-prevention-for-side state active-side) (wait-for - (resolve-bad-pub-prevention-for-side state active-side) - (wait-for - (resolve-bad-pub-prevention-for-side state responding-side) - (complete-with-result state side eid (fetch-and-clear! state :bad-publicity))))))) + (resolve-bad-pub-prevention-for-side state responding-side) + (complete-with-result state side eid (fetch-and-clear! state :bad-publicity))))))) ;; TAG PREVENTION 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" From 654e09dac2bd2be0689fa7d3914bced3af0c5584 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 11:29:06 +1300 Subject: [PATCH 15/38] expose prevention is good to go --- src/clj/game/cards/events.clj | 14 +- src/clj/game/cards/hardware.clj | 26 ++- src/clj/game/cards/programs.clj | 15 +- src/clj/game/cards/resources.clj | 5 +- src/clj/game/cards/upgrades.clj | 7 +- src/clj/game/core/expose.clj | 12 +- test/clj/game/cards/assets_test.clj | 9 +- test/clj/game/cards/events_test.clj | 9 +- test/clj/game/cards/hardware_test.clj | 4 +- test/clj/game/cards/identities_test.clj | 226 ++++++++++++------------ 10 files changed, 167 insertions(+), 160 deletions(-) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 9eba262a58..0ffa3e56b9 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -1060,7 +1060,7 @@ :choices {:card #(and (installed? %) (ice? %))} :async true - :effect (req (wait-for (expose state side [target]) + :effect (req (wait-for (expose state side [target] {:card card}) (continue-ability state side {:prompt "Choose a server" @@ -1201,7 +1201,6 @@ :effect (req (wait-for (breach-server state side [:hq] {:no-root true}) (breach-server state side eid [:rd] {:no-root true})))}]}) -;; TODO - fix this card (defcard "Drive By" {:on-play {:choices {:card #(let [topmost (get-nested-host %)] @@ -1209,8 +1208,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] {:card card}) + (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))) @@ -1484,7 +1483,6 @@ card [(breach-access-bonus :hq 1 {:duration :end-of-run})]) (effect-completed state side eid)))}]}) -;; TODO - fix this card (defcard "Falsified Credentials" {:on-play {:prompt "Choose one" @@ -1499,7 +1497,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] {:card card}) (continue-ability state :runner (when (and async-result ;; expose was successful @@ -3500,7 +3498,7 @@ :async true :change-in-game-state (req (some (complement faceup?) (all-installed state :corp))) :effect (req (if (pos? (count targets)) - (expose state side eid targets) + (expose state side eid targets {:card card}) (effect-completed state side eid)))}}) (defcard "Scavenge" @@ -3698,7 +3696,7 @@ (not (ice? %)) (corp? %))} :async true - :effect (req (wait-for (expose state side [target]) + :effect (req (wait-for (expose state side [target] {:card card}) (continue-ability state side {:prompt "Choose a server" diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 4ef9a9a2e2..31b929dcbe 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -239,7 +239,6 @@ :effect (effect (damage eid :brain 2 {:card card}))} :in-play [:click-per-turn 1]}) -;; TODO - if there are multiple cards exposed! (defcard "Blackguard" (letfn [(force-a-rez [c] {:msg (msg "attempt to force the rez of " (:title c)) @@ -272,6 +271,7 @@ {: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) @@ -1132,9 +1132,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] {:card card}) (continue-ability state side (offer-jack-out) card nil)))}]}) (defcard "Grimoire" @@ -1291,8 +1290,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 @@ -2604,14 +2603,23 @@ :msg "draw 1 card from the bottom of the stack" :effect (effect (move (last (:deck runner)) :hand))}]}) -;; TODO - add an autoresolve to this, make it work when multiple cards are exposed (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/programs.clj b/src/clj/game/cards/programs.clj index 9b603e4514..5c6aeeabf8 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -529,7 +529,6 @@ :effect (effect (add-counter :runner card :virus 1)) :msg "place 1 virus counter on itself"}]})) -;; TODO - fix this card (defcard "Aumakua" (auto-icebreaker {:implementation "[Erratum] Whenever you finish breaching a server, if you did not steal or trash any accessed cards, place 1 virus counter on this program." :abilities [(break-sub 1 1) @@ -542,7 +541,7 @@ (:did-trash target)))) :effect (effect (add-counter card :virus 1))} {:event :expose - :effect (effect (add-counter card :virus 1))}]})) + :effect (effect (add-counter card :virus (count (:cards context))))}]})) (defcard "Aurora" (auto-icebreaker {:abilities [(break-sub 2 1 "Barrier") @@ -3095,9 +3094,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)] {:card card}) (continue-ability state side (offer-jack-out) card nil)))}}}]}) (defcard "Snowball" @@ -3441,7 +3439,6 @@ {:req (req (and (= 1 (count (:subroutines current-ice))) (<= (get-strength current-ice) (get-strength card))))})) -;; TODO - fix this card (defcard "Wari" (letfn [(prompt-for-subtype [] {:prompt "Choose one" @@ -3457,11 +3454,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] {:card card}) + (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 fd9f4bd423..0af7764268 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -2756,12 +2756,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] {:card card}))}]}) (defcard "Reclaim" {:abilities diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index e932ac0ab0..b06255ab2c 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -1825,10 +1825,11 @@ :cost [(->c :trash-can)] :msg (msg "prevent a subroutine on " (:title current-ice) " from being broken")}]}) -;; TODO - fix this card (defcard "Underway Grid" - {:events [] - :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}]}) diff --git a/src/clj/game/core/expose.clj b/src/clj/game/core/expose.clj index 4e34edc204..35b9bd38ec 100644 --- a/src/clj/game/core/expose.clj +++ b/src/clj/game/core/expose.clj @@ -2,7 +2,8 @@ (: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.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.flags :refer [cards-can-prevent? get-prevent-list]] [game.core.prevention :refer [resolve-expose-prevention]] @@ -20,16 +21,19 @@ (doseq [t targets] (when-let [ability (:on-expose (card-def t))] ;; if it gets rezzed by blackguard or something, the effect shouldn't fizzle - ;; but if it dies to drive-by, the effect SHOULD fizzle (register-pending-event state :expose t (assoc ability :condition :installed)))) (queue-event state :expose {:cards targets}) - (checkpoint state side eid {:duration :expose})))) + (wait-for (checkpoint state side {:duration :expose}) + (complete-with-result state side eid {:cards targets}))))) (defn expose "Exposes the given cards." ([state side eid targets] (expose state side eid targets nil)) ([state side eid targets {:keys [unpreventable card] :as args}] - (let [targets (filterv #(not (or (rezzed? %) (nil? %))) targets)] + (let [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) diff --git a/test/clj/game/cards/assets_test.clj b/test/clj/game/cards/assets_test.clj index d73178b2d5..9514060262 100644 --- a/test/clj/game/cards/assets_test.clj +++ b/test/clj/game/cards/assets_test.clj @@ -6701,15 +6701,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 2ea27c6277..373db20395 100644 --- a/test/clj/game/cards/events_test.clj +++ b/test/clj/game/cards/events_test.clj @@ -2935,13 +2935,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 +3730,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 @@ -6721,7 +6720,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"))) diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index d7cdfdf3de..41a6151d63 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -2349,7 +2349,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") @@ -5737,6 +5737,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/identities_test.clj b/test/clj/game/cards/identities_test.clj index af03f5dd22..63faf515b0 100644 --- a/test/clj/game/cards/identities_test.clj +++ b/test/clj/game/cards/identities_test.clj @@ -14,106 +14,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 @@ -129,27 +129,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 From 0be58744e0f3afa630ea608c40dcd97196d97aeb Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 13:19:05 +1300 Subject: [PATCH 16/38] prevent jack out --- src/clj/game/cards/agendas.clj | 22 ++++--- src/clj/game/cards/events.clj | 3 +- src/clj/game/cards/hardware.clj | 3 +- src/clj/game/cards/ice.clj | 70 +++++++++++---------- src/clj/game/cards/operations.clj | 15 +++-- src/clj/game/cards/programs.clj | 3 +- src/clj/game/core.clj | 2 - src/clj/game/core/choose_one.clj | 93 ++++++++++++++++++++++++++++ src/clj/game/core/def_helpers.clj | 86 ------------------------- src/clj/game/core/diffs.clj | 3 +- src/clj/game/core/flags.clj | 3 - src/clj/game/core/prevention.clj | 59 +++++++++++++----- src/clj/game/core/runs.clj | 49 ++++++--------- test/clj/game/cards/agendas_test.clj | 24 +++---- test/clj/game/cards/ice_test.clj | 21 ++++--- 15 files changed, 243 insertions(+), 213 deletions(-) create mode 100644 src/clj/game/core/choose_one.clj diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index a8fcb7add7..cc4536df40 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -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 [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 @@ -1177,12 +1178,19 @@ (defcard "Labyrinthine Servers" {:on-score {:silent (req true) :effect (effect (add-counter card :power 2))} - :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 + :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) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 0ffa3e56b9..67caffc601 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.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]] diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 31b929dcbe..6409c6e980 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -10,10 +10,11 @@ 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 enable-runner-damage-choice runner-can-choose-damage?]] - [game.core.def-helpers :refer [breach-access-bonus choose-one-helper defcard offer-jack-out + [game.core.def-helpers :refer [breach-access-bonus defcard offer-jack-out reorder-choice trash-on-empty get-x-fn]] [game.core.drawing :refer [draw]] [game.core.effects :refer [any-effects register-lingering-effect diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index 9288d50fae..da82c10972 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 [any-effects 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]] @@ -2351,22 +2351,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) @@ -3921,22 +3916,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 @@ -4481,7 +4479,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/operations.clj b/src/clj/game/cards/operations.clj index 37bb41bde1..a1cfec0faa 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.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]] @@ -210,9 +211,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))}}}) diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index 5c6aeeabf8..92e29ee501 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.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]] diff --git a/src/clj/game/core.clj b/src/clj/game/core.clj index 66a58244e7..ed7da76c8f 100644 --- a/src/clj/game/core.clj +++ b/src/clj/game/core.clj @@ -431,7 +431,6 @@ persistent-flag? prevent-current prevent-draw - prevent-jack-out register-persistent-flag! register-run-flag! register-turn-flag! @@ -713,7 +712,6 @@ get-runnable-zones handle-end-run jack-out - jack-out-prevent make-run pass-ice prevent-access 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/def_helpers.clj b/src/clj/game/core/def_helpers.clj index d27a5eafd5..28614e01d7 100644 --- a/src/clj/game/core/def_helpers.clj +++ b/src/clj/game/core/def_helpers.clj @@ -259,92 +259,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 (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}}) - (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/flags.clj b/src/clj/game/core/flags.clj index e6e31cb692..9591d40672 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)) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index dbade6652b..7026a55c06 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -1,10 +1,10 @@ (ns game.core.prevention (:require - [game.core.board :refer [all-installed]] + [game.core.board :refer [all-active]] [game.core.card :refer [get-card 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.def-helpers :refer [choose-one-helper]] [game.core.eid :refer [complete-with-result effect-completed]] [game.core.effects :refer [any-effects]] [game.core.engine :refer [resolve-ability trigger-event-simult trigger-event-sync]] @@ -45,7 +45,7 @@ (defn- gather-prevention-abilities [state side eid key] - (mapcat #(relevant-prevention-abilities state side eid key %) (all-installed state side))) + (mapcat #(relevant-prevention-abilities state side eid key %) (all-active state side))) (defn prevent-numeric [state side eid key n] @@ -86,6 +86,47 @@ :req (:req (:ability prevention)) :effect (req (trigger-prevention state side eid key prevention))}}) +;; 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] + (let [remainder (get-in @state [:prevent :jack-out :remaining])] + (if (or (not (pos? remainder)) (get-in @state [:prevent :jack-out :passed])) + (do (swap! state dissoc-in [:prevent :jack-out :passed]) + (effect-completed state side eid)) + (let [preventions (gather-prevention-abilities state side eid :jack-out)] + (if (empty? preventions) + (effect-completed state side eid) + ;; TODO - if there's exactly ONE choice, and it's also mandatory, just rip that choice + (if (and (= 1 (count preventions)) + (:mandatory (first preventions))) + (wait-for (trigger-prevention state side :jack-out (first preventions)) + (resolve-jack-out-prevention-for-side state side eid)) + (wait-for (resolve-ability + state side + (choose-one-helper + {:prompt "Prevent the Runner from jacking out?" + :waiting-prompt "your opponent to prevent you from jacking out"} + (concat (mapv #(build-prevention-option % :jack-out) preventions) + [(when-not (some :mandatory preventions) + {:option (str "Allow the Runner to jack out") + :ability {:effect (req (swap! state assoc-in [:prevent :jack-out :passed] true))}})])) + nil nil) + (resolve-jack-out-prevention-for-side state side eid)))))))) + +(defn resolve-jack-out-prevention + [state side eid {:keys [unpreventable card] :as args}] + (swap! state assoc-in [:prevent :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 @@ -105,18 +146,6 @@ (do (println "tried to prevent expose outside of an expose prevention window") (effect-completed state side eid)))) - ;; (if ( - ;; [n] - ;; {:prompt (str "Prevent " (quantify n "card") " from being exposed") - ;; :label (str "prevent " (quantify n "card") " from being exposed") - ;; :choices {:req (req (vec-contains-card? (get-in @state [:prevent :expose :remaining]) target)) - ;; :max n - ;; :all true} - ;; :msg (msg "prevent " (enumerate-str (map #(card-str state %) targets)) " from being exposed") - ;; :effect (req (let [remainder (get-in @state [:prevent :expose :remaining]) - ;; remainder (filterv #(not (vec-contains-card? targets %)) remainder)] - ;; (swap! state assoc-in [:prevent :expose :remaining] remainder)))}) - (defn resolve-expose-prevention-for-side [state side eid] (let [remainder (get-in @state [:prevent :expose :remaining])] diff --git a/src/clj/game/core/runs.clj b/src/clj/game/core/runs.clj index 33ebcfe825..615e6754be 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? cards-can-prevent? clear-run-register! get-prevent-list]] [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-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]] @@ -744,11 +745,6 @@ (resolve-end-run 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) @@ -758,31 +754,22 @@ (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/test/clj/game/cards/agendas_test.clj b/test/clj/game/cards/agendas_test.clj index 4e0cabd036..5f733d190c 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" diff --git a/test/clj/game/cards/ice_test.clj b/test/clj/game/cards/ice_test.clj index d808e1485b..6a45b37e96 100644 --- a/test/clj/game/cards/ice_test.clj +++ b/test/clj/game/cards/ice_test.clj @@ -3740,14 +3740,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 @@ -7265,12 +7267,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 +7297,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 +8435,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 +8453,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 From 90ec9010cc91e5441a2eaa1631a9fa61a58d6e79 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 13:38:13 +1300 Subject: [PATCH 17/38] fix tests --- test/clj/game/cards/operations_test.clj | 4 +++- test/clj/game/cards/programs_test.clj | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/clj/game/cards/operations_test.clj b/test/clj/game/cards/operations_test.clj index 5d832c9e55..a2d7c03007 100644 --- a/test/clj/game/cards/operations_test.clj +++ b/test/clj/game/cards/operations_test.clj @@ -258,7 +258,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 diff --git a/test/clj/game/cards/programs_test.clj b/test/clj/game/cards/programs_test.clj index 7ea1ee835f..46b3caaf4d 100644 --- a/test/clj/game/cards/programs_test.clj +++ b/test/clj/game/cards/programs_test.clj @@ -6021,6 +6021,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) From 9ebcf8ea25be00fd2255208dd0ca18472656294f Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 14:34:34 +1300 Subject: [PATCH 18/38] preventions for end-the-run effects --- src/clj/game/cards/hardware.clj | 19 ++++++------ src/clj/game/cards/programs.clj | 35 +++++++--------------- src/clj/game/core.clj | 1 - src/clj/game/core/prevention.clj | 42 +++++++++++++++++++++++++++ src/clj/game/core/runs.clj | 36 ++++------------------- test/clj/game/cards/hardware_test.clj | 15 ++++------ 6 files changed, 74 insertions(+), 74 deletions(-) diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 6409c6e980..6258fc1451 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -43,12 +43,12 @@ [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 [prevent-tag]] + [game.core.prevention :refer [prevent-end-run prevent-tag]] [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 [can-pay-to-rez? derez rez]] - [game.core.runs :refer [bypass-ice end-run end-run-prevent + [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]] @@ -1340,13 +1340,14 @@ (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)) + (= :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]}] diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index 92e29ee501..cb0693f966 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -44,11 +44,12 @@ trash-prevent]] [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 [prevent-end-run]] [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]] @@ -581,29 +582,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") diff --git a/src/clj/game/core.clj b/src/clj/game/core.clj index ed7da76c8f..1241dfd582 100644 --- a/src/clj/game/core.clj +++ b/src/clj/game/core.clj @@ -704,7 +704,6 @@ continue encounter-ends end-run - end-run-prevent force-ice-encounter gain-next-run-credits gain-run-credits diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 7026a55c06..0e6fdefb03 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -86,6 +86,48 @@ :req (:req (:ability prevention)) :effect (req (trigger-prevention state side eid key prevention))}}) +;; 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] + (let [remainder (get-in @state [:prevent :end-run :remaining])] + (if (or (not (pos? remainder)) (get-in @state [:prevent :end-run :passed])) + (do (swap! state dissoc-in [:prevent :end-run :passed]) + (effect-completed state side eid)) + (let [preventions (gather-prevention-abilities state side eid :end-run)] + (if (empty? preventions) + (effect-completed state side eid) + ;; TODO - if there's exactly ONE choice, and it's also mandatory, just rip that choice + (if (and (= 1 (count preventions)) + (:mandatory (first preventions))) + (wait-for (trigger-prevention state side :end-run (first preventions)) + (resolve-end-run-prevention-for-side state side eid)) + (wait-for (resolve-ability + state side + (choose-one-helper + {:prompt "Prevent the run from ending?" + :waiting-prompt "your opponent to prevent the run from ending"} + (concat (mapv #(build-prevention-option % :end-run) preventions) + [(when-not (some :mandatory preventions) + {:option (str "Allow the run to end") + :ability {:effect (req (swap! state assoc-in [:prevent :end-run :passed] true))}})])) + nil nil) + (resolve-end-run-prevention-for-side state side eid)))))))) + +(defn resolve-end-run-prevention + [state side eid {:keys [unpreventable card] :as args}] + (swap! state assoc-in [:prevent :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)) + (wait-for + (resolve-end-run-prevention-for-side state :runner) + (complete-with-result state side eid (fetch-and-clear! state :end-run)))))) + ;; JACK OUT PREVENTION (def prevent-jack-out diff --git a/src/clj/game/core/runs.clj b/src/clj/game/core/runs.clj index 615e6754be..00cb9f477c 100644 --- a/src/clj/game/core/runs.clj +++ b/src/clj/game/core/runs.clj @@ -13,7 +13,7 @@ [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-jack-out-prevention]] + [game.core.prevention :refer [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]] @@ -692,10 +692,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)] @@ -713,43 +709,23 @@ (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- 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." diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index 41a6151d63..520c3ead6e 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -2974,9 +2974,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 @@ -3022,7 +3020,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 @@ -3031,8 +3029,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") @@ -3045,8 +3042,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 @@ -3061,8 +3057,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 From 7a300405b0084c5e5c94756a157db345111608fb Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 15:37:58 +1300 Subject: [PATCH 19/38] made one of the functions generic/reusable --- src/clj/game/core/prevention.clj | 170 ++++++++++++------------------- 1 file changed, 66 insertions(+), 104 deletions(-) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 0e6fdefb03..d967719cd7 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -86,35 +86,53 @@ :req (:req (:ability prevention)) :effect (req (trigger-prevention state side eid key prevention))}}) -;; 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] - (let [remainder (get-in @state [:prevent :end-run :remaining])] - (if (or (not (pos? remainder)) (get-in @state [:prevent :end-run :passed])) - (do (swap! state dissoc-in [:prevent :end-run :passed]) +(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)) + waiting (if (string? waiting) waiting (waiting state)) + option (if (string? option) option (option state))] + (if (or (if (= data-type :sequential) + (not (seq remainder)) + (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 :end-run)] + (let [preventions (gather-prevention-abilities state side eid key)] (if (empty? preventions) (effect-completed state side eid) ;; TODO - if there's exactly ONE choice, and it's also mandatory, just rip that choice (if (and (= 1 (count preventions)) (:mandatory (first preventions))) - (wait-for (trigger-prevention state side :end-run (first preventions)) - (resolve-end-run-prevention-for-side state side eid)) + (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 "Prevent the run from ending?" - :waiting-prompt "your opponent to prevent the run from ending"} - (concat (mapv #(build-prevention-option % :end-run) preventions) + {:prompt prompt + :waiting-prompt waiting} + (concat (mapv #(build-prevention-option % key) preventions) [(when-not (some :mandatory preventions) - {:option (str "Allow the run to end") - :ability {:effect (req (swap! state assoc-in [:prevent :end-run :passed] true))}})])) + {:option option + :ability {:effect (req (swap! state assoc-in [:prevent key :passed] true))}})])) nil nil) - (resolve-end-run-prevention-for-side state side eid)))))))) + (resolve-keyed-prevention-for-side state side eid key args)))))))) + +;; ENCOUNTER PREVENTION +(def prevent-encounter + (fn [state side eid] (prevent-numeric state side eid :encounter 1))) + +;; 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}] @@ -135,29 +153,11 @@ (defn- resolve-jack-out-prevention-for-side [state side eid] - (let [remainder (get-in @state [:prevent :jack-out :remaining])] - (if (or (not (pos? remainder)) (get-in @state [:prevent :jack-out :passed])) - (do (swap! state dissoc-in [:prevent :jack-out :passed]) - (effect-completed state side eid)) - (let [preventions (gather-prevention-abilities state side eid :jack-out)] - (if (empty? preventions) - (effect-completed state side eid) - ;; TODO - if there's exactly ONE choice, and it's also mandatory, just rip that choice - (if (and (= 1 (count preventions)) - (:mandatory (first preventions))) - (wait-for (trigger-prevention state side :jack-out (first preventions)) - (resolve-jack-out-prevention-for-side state side eid)) - (wait-for (resolve-ability - state side - (choose-one-helper - {:prompt "Prevent the Runner from jacking out?" - :waiting-prompt "your opponent to prevent you from jacking out"} - (concat (mapv #(build-prevention-option % :jack-out) preventions) - [(when-not (some :mandatory preventions) - {:option (str "Allow the Runner to jack out") - :ability {:effect (req (swap! state assoc-in [:prevent :jack-out :passed] true))}})])) - nil nil) - (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}] @@ -190,23 +190,13 @@ (defn resolve-expose-prevention-for-side [state side eid] - (let [remainder (get-in @state [:prevent :expose :remaining])] - (if (or (not (seq remainder)) (get-in @state [:prevent :expose :passed])) - (do (swap! state dissoc-in [:prevent :expose :passed]) - (effect-completed state side eid)) - (let [preventions (gather-prevention-abilities state side eid :expose)] - (if (empty? preventions) - (effect-completed state side eid) - (wait-for (resolve-ability - state side - (choose-one-helper - {:prompt (str "Prevent " (enumerate-str (map #(card-str state % {:visible (= side :corp)}) remainder) "or") " from being exposed?") - :waiting-prompt "your opponent to prevent an Expose"} - (concat (mapv #(build-prevention-option % :expose) preventions) - [{:option (str "Allow " (quantify (count remainder) "card") " to be exposed") - :ability {:effect (req (swap! state assoc-in [:prevent :expose :passed] true))}}])) - nil nil) - (resolve-expose-prevention-for-side state side eid))))))) + (letfn [(remainder [state] (get-in @state [:prevent :expose :remaining]))] + (resolve-keyed-prevention-for-side + state side eid :expose + {:data-type :sequential + :prompt (fn [state] (str "Prevent " (enumerate-str (map #(card-str state % {:visible (= side :corp)}) (remainder state)) "or") " from being exposed?")) + :waiting "your opponent to prevent an Expose" + :option (fn [state] (str "Allow " (quantify (count (remainder state)) "card") " to be exposed"))}))) (defn resolve-expose-prevention [state side eid targets {:keys [unpreventable card] :as args}] @@ -231,33 +221,16 @@ (defn prevent-bad-publicity [state side eid n] (prevent-numeric state side eid :bad-publicity n)) -(defn- resolve-bad-pub-prevention-for-side +(defn resolve-bad-pub-prevention-for-side [state side eid] - (let [remainder (get-in @state [:prevent :bad-publicity :remaining])] - (if (or (not (pos? remainder)) (get-in @state [:prevent :bad-publicity :passed])) - (do (swap! state dissoc-in [:prevent :bad-publicity :passed]) - (effect-completed state side eid)) - (let [preventions (gather-prevention-abilities state side eid :bad-publicity)] - (if (empty? preventions) - (effect-completed state side eid) - ;; TODO - if there's exactly ONE choice, and it's also mandatory, just rip that choice - (if (and (= 1 (count preventions)) - (:mandatory (first preventions))) - (wait-for (trigger-prevention state side :bad-publicity (first preventions)) - (resolve-bad-pub-prevention-for-side state side eid)) - (wait-for (resolve-ability - state side - (choose-one-helper - {:prompt (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-prompt "your opponent to prevent bad publicity"} - (concat (mapv #(build-prevention-option % :bad-publicity) preventions) - [(when-not (some :mandatory preventions) - {:option (str "Allow " remainder " remaining bad publicity") - :ability {:effect (req (swap! state assoc-in [:prevent :bad-publicity :passed] true))}})])) - nil nil) - (resolve-bad-pub-prevention-for-side state side eid)))))))) + (letfn [(remainder [state] (get-in @state [:prevent :expose :remaining]))] + (resolve-keyed-prevention-for-side + state side eid :bad-publicity + {:prompt (fn [state] (str "Prevent any of the " (get-in @state [:prevent :bad-publicity :count]) " bad publicity?" + (when-not (= (get-in @state [:prevent :bad-publicity :count]) (remainder state) + (str "(" (remainder state) " remaining)"))))) + :waiting "your opponent to prevent bad publicity" + :option (fn [state] (str "Allow " (get-in @state [:prevent :bad-publicity :remaining]) " bad publicity"))}))) (defn resolve-bad-pub-prevention [state side eid n {:keys [unpreventable card] :as args}] @@ -290,27 +263,16 @@ :effect (req (prevent-tag state side eid target)) :cancel-effect (req (prevent-tag state side eid 0))})) -(defn- resolve-tag-prevention-for-side +(defn resolve-bad-pub-prevention-for-side [state side eid] - (let [remainder (get-in @state [:prevent :tag :remaining])] - (if (or (not (pos? remainder)) (get-in @state [:prevent :tag :passed])) - (do (swap! state dissoc-in [:prevent :tag :passed]) - (effect-completed state side eid)) - (let [preventions (gather-prevention-abilities state side eid :tag)] - (if (empty? preventions) - (effect-completed state side eid) - (wait-for (resolve-ability - state side - (choose-one-helper - {:prompt (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-prompt "your opponent to prevent tags"} - (concat (mapv #(build-prevention-option % :tag) preventions) - [{:option (str "Allow " (quantify remainder "remaining tag")) - :ability {:effect (req (swap! state assoc-in [:prevent :tag :passed] true))}}])) - nil nil) - (resolve-tag-prevention-for-side state side eid))))))) + (letfn [(remainder [state] (get-in @state [:prevent :tag :remaining]))] + (resolve-keyed-prevention-for-side + state side eid :tag + {:prompt (fn [state] (str "Prevent any of the " (get-in @state [:prevent :tag :count]) " tags?" + (when-not (= (get-in @state [:prevent :tag :count]) (remainder state) + (str "(" (remainder state) " remaining)"))))) + :waiting "your opponent to prevent tags" + :option (fn [state] (str "Allow " (quantify (remainder state) "remaining tag")))}))) (defn resolve-tag-prevention [state side eid n {:keys [unpreventable card] :as args}] From 29bb16bc08a2fd6c9e0f2a4a3ab27922b5eba983 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 15:52:50 +1300 Subject: [PATCH 20/38] more simplification --- src/clj/game/core/prevention.clj | 51 +++++++++++++++----------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index d967719cd7..160dd6d6c0 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -89,9 +89,9 @@ (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)) - waiting (if (string? waiting) waiting (waiting state)) - option (if (string? option) option (option state))] + 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)) (not (pos? remainder))) @@ -190,13 +190,12 @@ (defn resolve-expose-prevention-for-side [state side eid] - (letfn [(remainder [state] (get-in @state [:prevent :expose :remaining]))] - (resolve-keyed-prevention-for-side - state side eid :expose - {:data-type :sequential - :prompt (fn [state] (str "Prevent " (enumerate-str (map #(card-str state % {:visible (= side :corp)}) (remainder state)) "or") " from being exposed?")) - :waiting "your opponent to prevent an Expose" - :option (fn [state] (str "Allow " (quantify (count (remainder state)) "card") " to be exposed"))}))) + (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}] @@ -223,14 +222,13 @@ (defn resolve-bad-pub-prevention-for-side [state side eid] - (letfn [(remainder [state] (get-in @state [:prevent :expose :remaining]))] - (resolve-keyed-prevention-for-side - state side eid :bad-publicity - {:prompt (fn [state] (str "Prevent any of the " (get-in @state [:prevent :bad-publicity :count]) " bad publicity?" - (when-not (= (get-in @state [:prevent :bad-publicity :count]) (remainder state) - (str "(" (remainder state) " remaining)"))))) - :waiting "your opponent to prevent bad publicity" - :option (fn [state] (str "Allow " (get-in @state [:prevent :bad-publicity :remaining]) " bad publicity"))}))) + (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}] @@ -263,16 +261,15 @@ :effect (req (prevent-tag state side eid target)) :cancel-effect (req (prevent-tag state side eid 0))})) -(defn resolve-bad-pub-prevention-for-side +(defn resolve-tag-prevention-for-side [state side eid] - (letfn [(remainder [state] (get-in @state [:prevent :tag :remaining]))] - (resolve-keyed-prevention-for-side - state side eid :tag - {:prompt (fn [state] (str "Prevent any of the " (get-in @state [:prevent :tag :count]) " tags?" - (when-not (= (get-in @state [:prevent :tag :count]) (remainder state) - (str "(" (remainder state) " remaining)"))))) - :waiting "your opponent to prevent tags" - :option (fn [state] (str "Allow " (quantify (remainder state) "remaining tag")))}))) + (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}] From fe0b4160e585b7c5ba8f94b76ee787bdaab994ac Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 17:12:45 +1300 Subject: [PATCH 21/38] encounter prevention --- src/clj/game/cards/hardware.clj | 35 +++++++++----------------- src/clj/game/cards/resources.clj | 26 ++++++------------- src/clj/game/cards/upgrades.clj | 2 +- src/clj/game/core/prevention.clj | 18 +++++++++++++ src/clj/game/core/runs.clj | 13 +++++----- test/clj/game/cards/hardware_test.clj | 2 +- test/clj/game/cards/programs_test.clj | 2 +- test/clj/game/cards/resources_test.clj | 8 +++--- test/clj/game/cards/upgrades_test.clj | 4 +-- 9 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 6258fc1451..7143a32eeb 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -43,7 +43,7 @@ [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 [prevent-end-run prevent-tag]] + [game.core.prevention :refer [prevent-encounter prevent-end-run prevent-tag]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal]] @@ -101,28 +101,17 @@ {: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 :encounter + :type :event + :ability {:async true + :cost [(->c :power 1)] + :msg (msg "prevent the encounter ability on " (:title current-ice)) + :effect (req (prevent-encounter state side eid))}}] + :events [(trash-on-empty :power)] + :abilities [{:cost [(->c :power 1)] + :req (req run) + :msg "prevent 1 net damage" + :effect (effect (damage-prevent :net 1))}]}) (defcard "Akamatsu Mem Chip" {:static-abilities [(mu+ 1)]}) diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 0af7764268..29b66f740c 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -58,7 +58,7 @@ [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 [prevent-tag prevent-up-to-n-tags]] + [game.core.prevention :refer [prevent-encounter prevent-tag prevent-up-to-n-tags]] [game.core.prompts :refer [cancellable]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal reveal-loud]] @@ -1699,23 +1699,13 @@ :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 (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 diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index b06255ab2c..0cc4299e6c 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -1961,7 +1961,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/prevention.clj b/src/clj/game/core/prevention.clj index 160dd6d6c0..0580a30031 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -122,6 +122,24 @@ (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}] + (swap! state assoc-in [:prevent :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)) + (wait-for + (resolve-encounter-prevention-for-side state :runner) + (complete-with-result state side eid (fetch-and-clear! state :encounter))))) + ;; END RUN PREVENTION (def prevent-end-run (fn [state side eid] (prevent-numeric state side eid :end-run 1))) diff --git a/src/clj/game/core/runs.clj b/src/clj/game/core/runs.clj index 00cb9f477c..4a4787e1b1 100644 --- a/src/clj/game/core/runs.clj +++ b/src/clj/game/core/runs.clj @@ -13,7 +13,7 @@ [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-end-run-prevention resolve-jack-out-prevention]] + [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]] @@ -325,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). diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index 520c3ead6e..39283ba8c6 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -112,7 +112,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) diff --git a/test/clj/game/cards/programs_test.clj b/test/clj/game/cards/programs_test.clj index 46b3caaf4d..75f6f81a71 100644 --- a/test/clj/game/cards/programs_test.clj +++ b/test/clj/game/cards/programs_test.clj @@ -4784,7 +4784,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) diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index b8f2e2225a..15cddf4f9f 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -3202,7 +3202,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")))) @@ -3218,7 +3218,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"))) @@ -3239,7 +3239,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 ")))) @@ -3256,7 +3256,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)) diff --git a/test/clj/game/cards/upgrades_test.clj b/test/clj/game/cards/upgrades_test.clj index ef9c3afb5d..aa5fa9a446 100644 --- a/test/clj/game/cards/upgrades_test.clj +++ b/test/clj/game/cards/upgrades_test.clj @@ -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)))) From 8ea46192cdff03bc3263c51e9fb6de07f199aeed Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 17:35:07 +1300 Subject: [PATCH 22/38] klevetnik uses floating disable card system --- src/clj/game/cards/ice.clj | 40 +++++++++++--------------------- src/clj/game/core/turns.clj | 5 ++++ test/clj/game/cards/ice_test.clj | 17 ++++---------- 3 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index da82c10972..108f3974e4 100644 --- a/src/clj/game/cards/ice.clj +++ b/src/clj/game/cards/ice.clj @@ -2545,23 +2545,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? %))} @@ -2569,16 +2553,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]?" diff --git a/src/clj/game/core/turns.clj b/src/clj/game/core/turns.clj index 51b8893890..dba611bbaa 100644 --- a/src/clj/game/core/turns.clj +++ b/src/clj/game/core/turns.clj @@ -152,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/test/clj/game/cards/ice_test.clj b/test/clj/game/cards/ice_test.clj index 6a45b37e96..de2125b556 100644 --- a/test/clj/game/cards/ice_test.clj +++ b/test/clj/game/cards/ice_test.clj @@ -4233,9 +4233,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") @@ -4243,10 +4241,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")))) @@ -4291,15 +4288,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] From 214169c5ec468c4253bee65e0723bc7e582d0828 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 25 Feb 2025 21:42:09 +1300 Subject: [PATCH 23/38] started on damage prevention - got the pre-damage stuff out of the way, this WILL fail every test --- src/clj/game/cards/agendas.clj | 21 +++-- src/clj/game/cards/events.clj | 34 ++++--- src/clj/game/cards/hardware.clj | 47 ++++++---- src/clj/game/cards/resources.clj | 68 ++++++++------ src/clj/game/core/damage.clj | 24 +++-- src/clj/game/core/engine.clj | 1 + src/clj/game/core/prevention.clj | 154 ++++++++++++++++++++++++++++--- 7 files changed, 260 insertions(+), 89 deletions(-) diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index cc4536df40..1b45c3cbad 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -38,7 +38,7 @@ trash trash-cards]] [game.core.optional :refer [get-autoresolve set-autoresolve]] [game.core.payment :refer [can-pay? ->c]] - [game.core.prevention :refer [prevent-jack-out]] + [game.core.prevention :refer [damage-type damage-pending damage-unboostable? damage-boost 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]] @@ -2197,11 +2197,20 @@ :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 + (println "checking") + (and (= :meat (damage-type state :pre-damage)) + (= :corp (get-in @state [:prevent :pre-damage :source-player])) + (pos? (damage-pending state :pre-damage)) + (not (damage-unboostable? state :pre-damage)))) + :msg "increase the pending meat damage by 1" + :effect (req (damage-boost state side eid :pre-damage 1))}}]}) (defcard "The Future is Now" {:on-score {:interactive (req true) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 67caffc601..7d5a272c84 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -50,7 +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 [prevent-up-to-n-tags]] + [game.core.prevention :refer [damage-type damage-pending damage-unpreventable? damage-prevent* prevent-up-to-n-tags]] [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]] @@ -2379,22 +2379,32 @@ {: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 :pre-damage + :type :floating + :max-uses 1 + :card card + :mandatory true + :ability {:async true + :card card + :condition :floating + :req (req + (and + (pos? (damage-pending state :pre-damage)) + (not (damage-unpreventable? state :pre-damage)))) + :msg "prevent all damage" + :effect (req (damage-prevent* state side eid :pre-damage :all))}}}))}}}}) (defcard "Levy AR Lab Access" {:on-play diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 7143a32eeb..26fbef2438 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -12,7 +12,7 @@ [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 trash-on-empty get-x-fn]] @@ -43,7 +43,7 @@ [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 [prevent-encounter prevent-end-run prevent-tag]] + [game.core.prevention :refer [damage-type damage-pending damage-unpreventable? damage-prevent* prevent-encounter prevent-end-run prevent-tag]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal]] @@ -111,7 +111,7 @@ :abilities [{:cost [(->c :power 1)] :req (req run) :msg "prevent 1 net damage" - :effect (effect (damage-prevent :net 1))}]}) + :effect (effect (damage-prevent* :net 1))}]}) (defcard "Akamatsu Mem Chip" {:static-abilities [(mu+ 1)]}) @@ -846,11 +846,11 @@ :req (req true)}]} :abilities [{:cost [(->c :credit 3)] :msg "prevent 1 net damage" - :effect (effect (damage-prevent :net 1))} + :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))}]}) + :effect (effect (damage-prevent* :brain 2))}]}) (defcard "Flame-out" (let [register-flame-effect @@ -1140,9 +1140,9 @@ :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))}]}) + :effect (effect (damage-prevent* :brain 1) + (damage-prevent* :meat 1) + (damage-prevent* :net 1))}]}) (defcard "Hermes" (let [ab {:interactive (req true) @@ -1487,8 +1487,8 @@ :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))}]})) + :effect (effect (damage-prevent* :brain 1) + (damage-prevent* :net 1))}]})) (defcard "Mu Safecracker" {:implementation "Stealth credit restriction not enforced" @@ -1516,12 +1516,19 @@ 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 :pre-damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req + (and (= :meat (damage-type state :pre-damage)) + (first-event? state side :pre-damage-flag + #(= :meat (:type (first %)))) + (pos? (damage-pending state :pre-damage)) + (not (damage-unpreventable? state :pre-damage)))) + :msg "reduce the pending meat damage by 1" + :effect (req (damage-prevent* state side eid :pre-damage 1))}}]}) (defcard "Net-Ready Eyes" {:on-install {:async true @@ -1765,7 +1772,7 @@ :events [(trash-on-empty :power)] :abilities [{:cost [(->c :power 1)] :msg "prevent 1 meat damage" - :effect (req (damage-prevent state side :meat 1))}]}) + :effect (req (damage-prevent* state side :meat 1))}]}) (defcard "Poison Vial" (auto-icebreaker @@ -1923,8 +1930,8 @@ :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) + :effect (effect (damage-prevent* :net target) + (damage-prevent* :brain target) (mill :runner eid :runner target))} card nil)))}]}) @@ -1942,7 +1949,7 @@ (= (: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)))) + :effect (effect (damage-prevent* (:type (first (:pre-damage (eventmap @state)))) (cost-value eid :x-credits)))}]})) (defcard "Record Reconstructor" diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 29b66f740c..f351520b4d 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -58,7 +58,7 @@ [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 [prevent-encounter prevent-tag prevent-up-to-n-tags]] + [game.core.prevention :refer [damage-type damage-pending damage-unpreventable? damage-prevent* prevent-encounter prevent-tag prevent-up-to-n-tags]] [game.core.prompts :refer [cancellable]] [game.core.props :refer [add-counter add-icon remove-icon]] [game.core.revealing :refer [reveal reveal-loud]] @@ -728,9 +728,18 @@ (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 :pre-damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req + (and + (has-subtype? (:source-card context) "Cybernetic") + (pos? (damage-pending state :pre-damage)) + (not (damage-unpreventable? state :pre-damage)))) + :msg "prevent all damage" + :effect (req (damage-prevent* state side eid :pre-damage :all))}}]}) (defcard "Citadel Sanctuary" {:interactions {:prevent [{:type #{:meat} @@ -1603,32 +1612,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 :pre-damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req + (and (pos? (damage-pending state :pre-damage)) + (or (= :meat (damage-type state :pre-damage)) + (= :net (damage-type state :pre-damage))) + (not (damage-unpreventable? state :pre-damage)))) + :msg "prevent all damage" + :effect (req (wait-for (damage-prevent* state side :pre-damage :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"]) + :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" diff --git a/src/clj/game/core/damage.clj b/src/clj/game/core/damage.clj index f10760f5c2..2873f20b73 100644 --- a/src/clj/game/core/damage.clj +++ b/src/clj/game/core/damage.clj @@ -5,6 +5,7 @@ [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]] @@ -184,13 +185,16 @@ 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))))))) + (wait-for (resolve-damage-prevention state side type n args) + (println async-result)))) + + ;; (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))))))) diff --git a/src/clj/game/core/engine.clj b/src/clj/game/core/engine.clj index 9e32cdb4b2..9321ff8183 100644 --- a/src/clj/game/core/engine.clj +++ b/src/clj/game/core/engine.clj @@ -629,6 +629,7 @@ :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/prevention.clj b/src/clj/game/core/prevention.clj index 0580a30031..b3b8586ca4 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -6,9 +6,10 @@ [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]] + [game.core.effects :refer [any-effects get-effects]] [game.core.engine :refer [resolve-ability trigger-event-simult trigger-event-sync]] [game.core.payment :refer [can-pay?]] + [game.core.prompts :refer [clear-wait-prompt]] [game.core.to-string :refer [card-str]] [game.utils :refer [dissoc-in enumerate-str quantify]] [game.macros :refer [msg req wait-for]] @@ -38,14 +39,30 @@ (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 nil))] + ((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] - (mapcat #(relevant-prevention-abilities state side eid key %) (all-active state side))) + (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] @@ -70,20 +87,27 @@ "Triggers an ability as having prevented something" [state side eid key prevention] (swap! state update-in [:prevent key :uses (->> prevention :card :cid)] (fnil inc 0)) - (resolve-ability - state side (assoc eid :source (:card prevention) :source-type :ability) - (if (:prompt prevention) - {:optional {:prompt (:prompt prevention) - :yes-ability (:ability prevention)}} - (:ability prevention)) - (:card prevention) nil)) + ;; 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) + (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 - :req (:req (:ability prevention)) :effect (req (trigger-prevention state side eid key prevention))}}) (defn- resolve-keyed-prevention-for-side @@ -101,7 +125,6 @@ (let [preventions (gather-prevention-abilities state side eid key)] (if (empty? preventions) (effect-completed state side eid) - ;; TODO - if there's exactly ONE choice, and it's also mandatory, just rip that choice (if (and (= 1 (count preventions)) (:mandatory (first preventions))) (wait-for (trigger-prevention state side key (first preventions)) @@ -118,6 +141,113 @@ nil nil) (resolve-keyed-prevention-for-side state side eid key args)))))))) +;; DAMAGE PREVENTION +;; +;; The following are either interrupts, or static abilities, that must come first: +;; Guru Davinder (done) +;; Leverage (done) +;; Chrome Parlor (done) +;; Muresh Bodysuit (done) +;; The Cleaners (done) +;; +;; The following are normal prevention timing: +;; Heartbeat +;; Jarogniew Mercs +;; Monolith +;; Net Shield +;; No One Home +;; On the Lam +;; Plascrete Carapace +;; Ramujan-reliant 550 BMI +;; Recon Drone +;; Sacrificial Clone (lmao) +;; AirbladeX (JSRF Ed.) +;; Bio-Modeled Network +;; Biometric Spoofing +;; Caldera +;; Deus X +;; Feedback Filter +;; +;; These just nominate the selecting side: +;; Titanium Ribs +;; Chronos Protocol: Selective Mind-mapping +;; +;; The follwing do something funny (confirm with rules what they actually do): +;; Tori Hanzo + +(defn damage-type + [state key] + (get-in @state [:prevent key :type])) + +(defn damage-pending + [state key] + (get-in @state [:prevent key :remaining])) + +(defn damage-unboostable? + [state key] + (get-in @state [:prevent key :unboostable])) + +(defn damage-unpreventable? + [state key] + (get-in @state [:prevent key :unpreventable])) + +(defn damage-boost + [state side eid key n] + (when (pos? (damage-pending state key)) + (swap! state update-in [:prevent key :remaining] + n)) + (effect-completed state side eid)) + +;; TODO - rename this after I strip it out of damage +(defn damage-prevent* + [state side eid key n] + (when (pos? (damage-pending state key)) + (if (= n :all) + (swap! state update-in [:prevent key] merge {:remaining 0 :prevented :all}) + (do (swap! state update-in [:prevent key :remaining] #(max 0 (- % n))) + (swap! state update-in [:prevent key :prevented] (fnil inc 1))))) + (effect-completed state side eid)) + +(defn- damage-name + [state key] + (case (damage-type state key) + :meat "meat" + :brain "core" + :core "core" + :net "net" + "neat")) + +(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 :pre-damage) " " (damage-name state :pre-damage) " damage?") + (str "There is " (damage-pending state :pre-damage) " " (damage-name state :pre-damage) " pending damage"))) + :waiting "your opponent to resolve pre-damage triggers" + :option (fn [state remainder] (str "Pass priority"))})) + +(defn- resolve-pre-damage-effects + [state side eid] + (clear-wait-prompt state side) + (println "passes: " (get-in @state [:prevent :pre-damage :priority-passes])) + (if (= 2 (get-in @state [:prevent :pre-damage :priority-passes])) + (complete-with-result state side eid (fetch-and-clear! state :pre-damage)) + (wait-for (resolve-pre-damage-for-side state side) + (swap! state update-in [:prevent :pre-damage :priority-passes] (fnil inc 1)) + (resolve-pre-damage-effects state (other-side side) eid)))) + +(defn resolve-damage-prevention + [state side eid type n {:keys [unpreventable unboostable card] :as args}] + (swap! state assoc-in [:prevent :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-pre-damage-effects state side) + (swap! state assoc-in [:prevent :damage] async-result) + (complete-with-result state side eid (fetch-and-clear! state :damage))))) + + ;; ENCOUNTER PREVENTION (def prevent-encounter (fn [state side eid] (prevent-numeric state side eid :encounter 1))) From b20140a78ef88e09b84b9aee524814ff8a595d23 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Wed, 26 Feb 2025 16:32:24 +1300 Subject: [PATCH 24/38] damage prevention done (damage doesn't work though) --- src/clj/game/cards/agendas.clj | 11 +- src/clj/game/cards/assets.clj | 26 +-- src/clj/game/cards/events.clj | 25 ++- src/clj/game/cards/hardware.clj | 165 ++++++++++--------- src/clj/game/cards/programs.clj | 32 ++-- src/clj/game/cards/resources.clj | 266 ++++++++++++++++--------------- src/clj/game/cards/upgrades.clj | 36 ++--- src/clj/game/core/costs.clj | 7 +- src/clj/game/core/payment.clj | 3 +- src/clj/game/core/prevention.clj | 120 +++++++++----- src/clj/game/core/tags.clj | 11 +- 11 files changed, 379 insertions(+), 323 deletions(-) diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index 1b45c3cbad..b4d5e3be93 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -38,7 +38,7 @@ trash trash-cards]] [game.core.optional :refer [get-autoresolve set-autoresolve]] [game.core.payment :refer [can-pay? ->c]] - [game.core.prevention :refer [damage-type damage-pending damage-unboostable? damage-boost prevent-jack-out]] + [game.core.prevention :refer [damage-boost 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]] @@ -2204,11 +2204,10 @@ :ability {:async true :condition :active :req (req - (println "checking") - (and (= :meat (damage-type state :pre-damage)) - (= :corp (get-in @state [:prevent :pre-damage :source-player])) - (pos? (damage-pending state :pre-damage)) - (not (damage-unboostable? state :pre-damage)))) + (and (= :meat (:type context)) + (= :corp (:source-player context)) + (pos? (:remaining context)) + (not (:unboostable context)))) :msg "increase the pending meat damage by 1" :effect (req (damage-boost state side eid :pre-damage 1))}}]}) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index d17a010da4..86253dbc32 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -44,7 +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 [prevent-bad-publicity prevent-expose]] + [game.core.prevention :refer [damage-name damage-prevent* prevent-bad-publicity prevent-expose]] [game.core.revealing :refer [reveal]] [game.core.rezzing :refer [can-pay-to-rez? derez rez]] [game.core.runs :refer [end-run]] @@ -2183,17 +2183,19 @@ 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 (add-counter state side card :power 1) - (wait-for (gain-credits state :corp 3) - (damage-prevent state :corp :net 1) - (effect-completed state side eid)))} - {: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)) + (not (:unpreventable context)) + (pos? (:remaining context)))) + :effect (req (wait-for (damage-prevent* state side :damage 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)] diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 7d5a272c84..3b14c01748 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -50,7 +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-type damage-pending damage-unpreventable? damage-prevent* prevent-up-to-n-tags]] + [game.core.prevention :refer [damage-name damage-prevent* 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]] @@ -2400,10 +2400,9 @@ :card card :condition :floating :req (req - (and - (pos? (damage-pending state :pre-damage)) - (not (damage-unpreventable? state :pre-damage)))) - :msg "prevent all damage" + (and (pos? (:remaining context)) + (not (:unpreventable context)))) + :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") :effect (req (damage-prevent* state side eid :pre-damage :all))}}}))}}}}) (defcard "Levy AR Lab Access" @@ -2699,22 +2698,18 @@ {: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)])}] + :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 :damage #{: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} - :req (req true)}]} - :abilities [{: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" diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 26fbef2438..8e63451269 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -43,7 +43,7 @@ [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-type damage-pending damage-unpreventable? damage-prevent* prevent-encounter prevent-end-run prevent-tag]] + [game.core.prevention :refer [damage-name damage-type damage-prevent* 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]] @@ -99,19 +99,23 @@ (defcard "AirbladeX (JSRF Ed.)" {:data {:counter {:power 3}} - :interactions {:prevent [{:type #{:net} - :req (req (and run (pos? (get-counters card :power))))}]} - :prevention [{:prevents :encounter - :type :event + :prevention [{:prevents :damage + :type :ability + :ability {:async true + :cost [(->c :power 1)] + :msg "prevent 1 net damage" + :req (req (and run + (not (:unpreventable context)) + (= :net (:type context)) + (pos? (:remaining context)))) + :effect (req (damage-prevent* state side eid :damage 1))}} + {:prevents :encounter + :type :ability :ability {:async true :cost [(->c :power 1)] :msg (msg "prevent the encounter ability on " (:title current-ice)) :effect (req (prevent-encounter state side eid))}}] - :events [(trash-on-empty :power)] - :abilities [{:cost [(->c :power 1)] - :req (req run) - :msg "prevent 1 net damage" - :effect (effect (damage-prevent* :net 1))}]}) + :events [(trash-on-empty :power)]}) (defcard "Akamatsu Mem Chip" {:static-abilities [(mu+ 1)]}) @@ -842,15 +846,21 @@ :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 "3 [Credit]: Feedback Filter" + :ability {:async true + :cost [(->c :credit 3)] + :msg "prevent 1 net damage" + :req (req (and (= :net (:type context)) + (not (:unpreventable context)) + (pos? (:remaining context)))) + :effect (req (damage-prevent* state side eid :damage 1))}} + {:prevents :damage + :type :ability + :label "[Trash]: Feedback Filter" + :ability (assoc (prevent-up-to-n-damage 2 :damage #{:brain :core}) + :cost [(->c :trash-can)])}]}) (defcard "Flame-out" (let [register-flame-effect @@ -1135,14 +1145,15 @@ (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-type state :damage) " damage") + :req (req (and (not (:unpreventable context)) + (pos? (:remaining context)))) + :effect (req (damage-prevent* state side eid :damage 1))}}]}) (defcard "Hermes" (let [ab {:interactive (req true) @@ -1480,15 +1491,17 @@ :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-type state :damage) " damage") + :req (req (and (not (= :meat (:type context))) + (not (:unpreventable context)) + (pos? (:remaining context))))}}]})) (defcard "Mu Safecracker" {:implementation "Stealth credit restriction not enforced" @@ -1522,11 +1535,11 @@ :mandatory true :ability {:async true :req (req - (and (= :meat (damage-type state :pre-damage)) + (and (= :meat (:type context)) (first-event? state side :pre-damage-flag #(= :meat (:type (first %)))) - (pos? (damage-pending state :pre-damage)) - (not (damage-unpreventable? state :pre-damage)))) + (pos? (:remaining context)) + (not (:unpreventable context)))) :msg "reduce the pending meat damage by 1" :effect (req (damage-prevent* state side eid :pre-damage 1))}}]}) @@ -1767,12 +1780,17 @@ (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 run + (not (:unpreventable context)) + (= :meat (:type context)) + (pos? (:remaining context)))) + :effect (req (damage-prevent* state side eid :damage 1))}}] + :events [(trash-on-empty :power)]}) (defcard "Poison Vial" (auto-icebreaker @@ -1915,42 +1933,35 @@ (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") + :effect (req (let [prevented (:prevented context)] + (wait-for (resolve-ability + state side + (prevent-up-to-n-damage (max-trash state) :damage #{:net :core :brain}) + card nil) + (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 + :req (req (and (pos? (:remaining context)) + (not (:unpreventable 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) " damage") + :async true + :effect (req (damage-prevent* state side eid :damage (cost-value eid :x-credits)))} + card nil))}}]}) (defcard "Record Reconstructor" {:events [(successful-run-replace-breach diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index cb0693f966..fc88c4d0dd 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -44,7 +44,7 @@ trash-prevent]] [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 [prevent-end-run]] + [game.core.prevention :refer [damage-name damage-prevent* prevent-end-run 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]] @@ -1215,12 +1215,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 :damage #{: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" @@ -2339,14 +2338,17 @@ 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)) + (not (:unpreventable context)) + (first-event? state side :pre-damage-flag #(= :net (:type (first %)))) + (pos? (:remaining context)))) + :effect (req (damage-prevent* state side eid :damage 1))}}]}) (defcard "Nfr" (auto-icebreaker {:abilities [(break-sub 1 1 "Barrier")] diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index f351520b4d..4214cb9669 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -58,7 +58,7 @@ [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-type damage-pending damage-unpreventable? damage-prevent* prevent-encounter prevent-tag prevent-up-to-n-tags]] + [game.core.prevention :refer [damage-name damage-prevent* prevent-encounter prevent-tag 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]] @@ -576,25 +576,29 @@ :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)) + (not (:unpreventable context)))) + :msg (msg "prevent " (dec (:remaining context)) " " (damage-name state :pre-damage) " damage") + :effect (req (damage-prevent* state side eid :damage (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 + (and (pos? (:remaining context)) + (not (:unpreventable context)))) + :msg (msg "prevent " (min 2 (:remaining context)) " " (damage-name state :pre-damage) " damage") + :effect (req (damage-prevent* state side eid :damage (min 2 (:remaining context))))}}]}) (defcard "Blockade Runner" {:abilities [{:action true @@ -647,14 +651,15 @@ (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) " damage") + :req (req (and (contains? #{:net :core :brain} (:type context)) + (not (:unpreventable context)) + (pos? (:remaining context)))) + :effect (req (damage-prevent* state side eid :damage 1))}}]}) (defcard "Charlatan" {:abilities [{:action true @@ -734,20 +739,23 @@ :mandatory true :ability {:async true :req (req - (and - (has-subtype? (:source-card context) "Cybernetic") - (pos? (damage-pending state :pre-damage)) - (not (damage-unpreventable? state :pre-damage)))) - :msg "prevent all damage" + (and (pos? (:remaining context)) + (has-subtype? (:source-card context) "Cybernetic") + (not (:unpreventable context)))) + :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") :effect (req (damage-prevent* state side eid :pre-damage :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 + :ability {:async true + :cost [(->c :trash-can)] + :req (req + (and (pos? (:remaining context)) + (= :meat (:type context)) + (not (:unpreventable context)))) + :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") + :effect (req (damage-prevent* state side eid :damage :all))}}] :events [{:event :runner-turn-ends :interactive (req true) :msg "force the Corp to initiate a trace" @@ -1619,11 +1627,11 @@ :mandatory true :ability {:async true :req (req - (and (pos? (damage-pending state :pre-damage)) - (or (= :meat (damage-type state :pre-damage)) - (= :net (damage-type state :pre-damage))) - (not (damage-unpreventable? state :pre-damage)))) - :msg "prevent all damage" + (and (pos? (:remaining context)) + (or (= :meat (:type context)) + (= :net (:type context))) + (not (:unpreventable context)))) + :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") :effect (req (wait-for (damage-prevent* state side :pre-damage :all) (continue-ability state side @@ -1844,12 +1852,16 @@ (add-counter state :runner card :power (+ 3 (count-tags state))) (effect-completed state :runner eid)))} :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 run + (not (:unpreventable context)) + (= :meat (:type context)) + (pos? (:remaining context)))) + :effect (req (damage-prevent* state side eid :damage 1))}}]}) (defcard "John Masanori" {:events [{:event :successful-run @@ -2310,7 +2322,7 @@ run (= :corp (:active-player @state)) (#{:psi :trace} (:source-type eid)) - (#{:net :meat :brain :tag} (get-in @state [:prevent :current])))) + (get-in @state [:prevention]))) :type :credit}}}) (defcard "Network Exchange" @@ -2379,48 +2391,45 @@ :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 (do (damage-prevent state :runner :net Integer/MAX_VALUE) - (effect-completed state side eid)))}}}))] - {:prevention [{:prevents :tag - :type :event - :label "No One Home" - :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) - ;; note that the checkpoints are suppressed for both damage and tag when resolving a snare, - ;; (or at least they will be after the costs merge), so this should work - (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)))}}] - :interactions {:prevent [{:type #{:net} - :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 true?) + (not (:unpreventable context)) + (pos? (:remaining context)))) + :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 :damage #{: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) + ;; note that the checkpoints are suppressed for both damage and tag when resolving a snare, + ;; (or at least they will be after the costs merge), so this should work + ;; TODO - add a handler for 'runner prevented all damage' + (no-event? state side :all-damage-prevent #(= :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} @@ -2537,12 +2546,19 @@ (gain-credits eid 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 :pre-damage + :type :event + :max-uses 1 + :mandatory true + :ability {:async true + :req (req + (and (pos? (:remaining context)) + (= :meat (:type context)) + (not (:unpreventable context)))) + :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") + :effect (req (damage-prevent* state side eid :pre-damage :all))}}] + :static-abilities [{:type :is-tagged + :value true}]}) (defcard "Patron" (let [ability {:prompt "Choose a server" @@ -2894,34 +2910,34 @@ 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 + (and (pos? (:remaining context)) + (not (:unpreventable context)))) + :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") + :effect (req (wait-for (damage-prevent* state side :damage :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)}]} diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index 0cc4299e6c..a93e4f996c 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -1733,31 +1733,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}))}}]}) (defcard "Traffic Analyzer" {:events [{:event :rez diff --git a/src/clj/game/core/costs.clj b/src/clj/game/core/costs.clj index 3dc65af79e..a15a468845 100644 --- a/src/clj/game/core/costs.clj +++ b/src/clj/game/core/costs.clj @@ -182,7 +182,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)) @@ -193,11 +193,14 @@ (<= (stealth-value cost) (total-available-stealth-credits state side eid card)))) (defmethod handler :x-credits [cost state side eid card] + (println cost) (continue-ability 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)) 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 index b3b8586ca4..93482534c9 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -144,36 +144,46 @@ ;; DAMAGE PREVENTION ;; ;; The following are either interrupts, or static abilities, that must come first: -;; Guru Davinder (done) -;; Leverage (done) -;; Chrome Parlor (done) -;; Muresh Bodysuit (done) -;; The Cleaners (done) +;; Guru Davinder (done) +;; Leverage (done) +;; Chrome Parlor (done) +;; Muresh Bodysuit (done) +;; The Cleaners (done) +;; Paparrazi (done) ;; ;; The following are normal prevention timing: -;; Heartbeat -;; Jarogniew Mercs -;; Monolith -;; Net Shield -;; No One Home -;; On the Lam -;; Plascrete Carapace -;; Ramujan-reliant 550 BMI -;; Recon Drone -;; Sacrificial Clone (lmao) -;; AirbladeX (JSRF Ed.) -;; Bio-Modeled Network -;; Biometric Spoofing -;; Caldera -;; Deus X -;; Feedback Filter +;; Hardware: +;; AirbladeX (JSRF Ed.) (done) +;; Feedback Filter (done) +;; Heartbeat (done) +;; Monolith (done) +;; Plascrete Carapace (done) +;; Ramujan-reliant 550 BMI (done) +;; Recon Drone (done) +;; Resources: +;; Jarogniew Mercs (done) +;; No One Home (done) +;; Sacrificial Clone (lmao) (done) +;; Bio-Modeled Network (done) +;; Biometric Spoofing (done) +;; Caldera (done) +;; Citadel Sanctuary (done) +;; Programs: +;; Net Shield (done) +;; Deus X (done) +;; Events: +;; On the Lam (done) +;; Assets: +;; Prana Condenser +;; Upgrades: +;; Tori Hanzo ;; -;; These just nominate the selecting side: +;; These just nominate the selecting side, they can probably still be done in the damage class: ;; Titanium Ribs ;; Chronos Protocol: Selective Mind-mapping ;; ;; The follwing do something funny (confirm with rules what they actually do): -;; Tori Hanzo +;; Tori Hanzo (set damage to 1, change type from net to brain) (defn damage-type [state key] @@ -183,14 +193,6 @@ [state key] (get-in @state [:prevent key :remaining])) -(defn damage-unboostable? - [state key] - (get-in @state [:prevent key :unboostable])) - -(defn damage-unpreventable? - [state key] - (get-in @state [:prevent key :unpreventable])) - (defn damage-boost [state side eid key n] (when (pos? (damage-pending state key)) @@ -204,10 +206,26 @@ (if (= n :all) (swap! state update-in [:prevent key] merge {:remaining 0 :prevented :all}) (do (swap! state update-in [:prevent key :remaining] #(max 0 (- % n))) - (swap! state update-in [:prevent key :prevented] (fnil inc 1))))) + (swap! state update-in [:prevent key :prevented] (fnil #(+ n %) n))))) (effect-completed state side eid)) -(defn- damage-name +(defn prevent-up-to-n-damage + [n key types] + (letfn [(remainder [state] (get-in @state [:prevent key :remaining])) + (max-to-avoid [state n] (if (= n :all) (remainder state) (min (remainder state) n)))] + {:prompt (msg "Choose how much " (damage-name state key) " damage prevent") + :req (req (and (pos? (get-in @state [:prevent key :remaining])) + (not (get-in @state [:prevent key :unpreventable])) + (or (not types) + (contains? types (get-in @state [:prevent key :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 key) " damage") + :effect (req (damage-prevent* state side eid key target)) + :cancel-effect (req (damage-prevent* state side eid key 0))})) + +(defn damage-name [state key] (case (damage-type state key) :meat "meat" @@ -223,30 +241,52 @@ {:prompt (fn [state remainder] (if (= side :runner) (str "Prevent " (damage-pending state :pre-damage) " " (damage-name state :pre-damage) " damage?") - (str "There is " (damage-pending state :pre-damage) " " (damage-name state :pre-damage) " pending damage"))) + (str "There is " (damage-pending state :pre-damage) " pending " (damage-name state :pre-damage) " damage"))) :waiting "your opponent to resolve pre-damage triggers" - :option (fn [state remainder] (str "Pass priority"))})) + :option "Pass priority"})) (defn- resolve-pre-damage-effects [state side eid] - (clear-wait-prompt state side) - (println "passes: " (get-in @state [:prevent :pre-damage :priority-passes])) + (clear-wait-prompt state side) ;; TODO - do I need this? (if (= 2 (get-in @state [:prevent :pre-damage :priority-passes])) (complete-with-result state side eid (fetch-and-clear! state :pre-damage)) (wait-for (resolve-pre-damage-for-side state side) (swap! state update-in [:prevent :pre-damage :priority-passes] (fnil inc 1)) (resolve-pre-damage-effects state (other-side side) eid)))) +;; 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 + +;; TODO - is this generic enough that I can just make a helper function for it and throw everything else through it? +(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) " " (damage-name state :damage) " damage?") + (str "There is " (damage-pending state :damage) " pending " (damage-name state :damage) " damage"))) + :waiting "your opponent to resolve damage triggers" + :option "Pass priority"})) + +(defn resolve-damage-effects + [state side eid] + (if (= 2 (get-in @state [:prevent :damage :priority-passes])) + (complete-with-result state side eid (fetch-and-clear! state :damage)) + (wait-for (resolve-damage-for-side state side) + (swap! state update-in [:prevent :damage :priority-passes] (fnil inc 1)) + (resolve-damage-effects state (other-side side) eid)))) + (defn resolve-damage-prevention [state side eid type n {:keys [unpreventable unboostable card] :as args}] (swap! state assoc-in [:prevent :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-pre-damage-effects state side) + (wait-for (resolve-pre-damage-effects state (:active-player @state)) (swap! state assoc-in [:prevent :damage] async-result) - (complete-with-result state side eid (fetch-and-clear! state :damage))))) - + (swap! state assoc-in [:prevent :damage :priority-passes] 0) + (resolve-damage-effects state (:active-player @state) eid)))) ;; ENCOUNTER PREVENTION (def prevent-encounter diff --git a/src/clj/game/core/tags.clj b/src/clj/game/core/tags.clj index a5b77c28de..da6bf0c15f 100644 --- a/src/clj/game/core/tags.clj +++ b/src/clj/game/core/tags.clj @@ -45,11 +45,12 @@ (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." From 77ed7c6216ff611f38ce1196660699b797599604 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Wed, 26 Feb 2025 17:06:27 +1300 Subject: [PATCH 25/38] damage works again - fixed some naming --- src/clj/game/cards/agendas.clj | 2 +- src/clj/game/cards/assets.clj | 6 +- src/clj/game/cards/events.clj | 36 ++++++--- src/clj/game/cards/hardware.clj | 18 ++--- src/clj/game/cards/operations.clj | 22 ++++-- src/clj/game/cards/programs.clj | 6 +- src/clj/game/cards/resources.clj | 36 ++++----- src/clj/game/cards/upgrades.clj | 2 +- src/clj/game/core.clj | 3 - src/clj/game/core/damage.clj | 126 ++++++------------------------ src/clj/game/core/prevention.clj | 27 ++++--- 11 files changed, 116 insertions(+), 168 deletions(-) diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index b4d5e3be93..a9b26937c9 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 get-x-fn]] [game.core.drawing :refer [draw draw-up-to]] diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index 86253dbc32..bc3a390820 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -17,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 trash-on-empty get-x-fn with-revealed-hand]] [game.core.drawing :refer [draw first-time-draw-bonus max-draw @@ -44,7 +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 damage-prevent* prevent-bad-publicity prevent-expose]] + [game.core.prevention :refer [damage-name 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]] @@ -2192,7 +2192,7 @@ (= :corp (:source-player context)) (not (:unpreventable context)) (pos? (:remaining context)))) - :effect (req (wait-for (damage-prevent* state side :damage 1) + :effect (req (wait-for (prevent-damage state side :damage 1) (wait-for (add-counter state side card :power 1 {:suppress-checkpoint true}) (gain-credits state side eid 3))))}}] :abilities [{:action true diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 3b14c01748..bbc16c6514 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -15,7 +15,7 @@ [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.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]] @@ -50,7 +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 damage-prevent* prevent-up-to-n-tags prevent-up-to-n-damage]] + [game.core.prevention :refer [damage-name prevent-damage 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]] @@ -2403,7 +2403,7 @@ (and (pos? (:remaining context)) (not (:unpreventable context)))) :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (damage-prevent* state side eid :pre-damage :all))}}}))}}}}) + :effect (req (prevent-damage state side eid :pre-damage :all))}}}))}}}}) (defcard "Levy AR Lab Access" {:on-play @@ -3913,6 +3913,29 @@ (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 :pre-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])) + (pos? (:remaining context)) + (not (:unpreventable context)))) + :condition :active + :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") + :effect (req (prevent-damage state side eid :pre-damage :all))}}] :on-play {:async true :change-in-game-state (req (or (seq (:hand runner)) (seq runnable-servers))) @@ -3926,12 +3949,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 8e63451269..12050935dd 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -43,7 +43,7 @@ [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 damage-prevent* prevent-encounter prevent-end-run prevent-tag prevent-up-to-n-damage]] + [game.core.prevention :refer [damage-name damage-type 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]] @@ -108,7 +108,7 @@ (not (:unpreventable context)) (= :net (:type context)) (pos? (:remaining context)))) - :effect (req (damage-prevent* state side eid :damage 1))}} + :effect (req (prevent-damage state side eid :damage 1))}} {:prevents :encounter :type :ability :ability {:async true @@ -848,17 +848,17 @@ (defcard "Feedback Filter" {:prevention [{:prevents :damage :type :ability - :label "3 [Credit]: Feedback Filter" + :label "Feedback Filter (Net)" :ability {:async true :cost [(->c :credit 3)] :msg "prevent 1 net damage" :req (req (and (= :net (:type context)) (not (:unpreventable context)) (pos? (:remaining context)))) - :effect (req (damage-prevent* state side eid :damage 1))}} + :effect (req (prevent-damage state side eid :damage 1))}} {:prevents :damage :type :ability - :label "[Trash]: Feedback Filter" + :label "Feedback Filter (Core)" :ability (assoc (prevent-up-to-n-damage 2 :damage #{:brain :core}) :cost [(->c :trash-can)])}]}) @@ -1153,7 +1153,7 @@ :msg (msg "prevent 1 " (damage-type state :damage) " damage") :req (req (and (not (:unpreventable context)) (pos? (:remaining context)))) - :effect (req (damage-prevent* state side eid :damage 1))}}]}) + :effect (req (prevent-damage state side eid :damage 1))}}]}) (defcard "Hermes" (let [ab {:interactive (req true) @@ -1541,7 +1541,7 @@ (pos? (:remaining context)) (not (:unpreventable context)))) :msg "reduce the pending meat damage by 1" - :effect (req (damage-prevent* state side eid :pre-damage 1))}}]}) + :effect (req (prevent-damage state side eid :pre-damage 1))}}]}) (defcard "Net-Ready Eyes" {:on-install {:async true @@ -1789,7 +1789,7 @@ (not (:unpreventable context)) (= :meat (:type context)) (pos? (:remaining context)))) - :effect (req (damage-prevent* state side eid :damage 1))}}] + :effect (req (prevent-damage state side eid :damage 1))}}] :events [(trash-on-empty :power)]}) (defcard "Poison Vial" @@ -1960,7 +1960,7 @@ {:cost [(->c :trash-can) (->c :x-credits 0 {:maximum (:remaining context)})] :msg (msg "prevent " (cost-value eid :x-credits) " " (damage-type state :damage) " damage") :async true - :effect (req (damage-prevent* state side eid :damage (cost-value eid :x-credits)))} + :effect (req (prevent-damage state side eid :damage (cost-value eid :x-credits)))} card nil))}}]}) (defcard "Record Reconstructor" diff --git a/src/clj/game/cards/operations.clj b/src/clj/game/cards/operations.clj index a1cfec0faa..6de787c226 100644 --- a/src/clj/game/cards/operations.clj +++ b/src/clj/game/cards/operations.clj @@ -16,7 +16,7 @@ [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.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]] @@ -38,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]] @@ -711,11 +712,20 @@ 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 %)))) + (pos? (:remaining context)) + (not (:unboostable context)))) + :msg "increase the pending core damage by 1" + :effect (req (damage-boost state side eid :pre-damage 1))}}]}) (defcard "Digital Rights Management" {:on-play diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index fc88c4d0dd..ca03fbafa5 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -13,7 +13,7 @@ [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.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]] @@ -44,7 +44,7 @@ trash-prevent]] [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 [damage-name damage-prevent* prevent-end-run prevent-up-to-n-damage]] + [game.core.prevention :refer [damage-name prevent-damage prevent-end-run 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]] @@ -2348,7 +2348,7 @@ (not (:unpreventable context)) (first-event? state side :pre-damage-flag #(= :net (:type (first %)))) (pos? (:remaining context)))) - :effect (req (damage-prevent* state side eid :damage 1))}}]}) + :effect (req (prevent-damage state side eid :damage 1))}}]}) (defcard "Nfr" (auto-icebreaker {:abilities [(break-sub 1 1 "Barrier")] diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 4214cb9669..a3c0ab7b97 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 trash-on-empty do-net-damage]] [game.core.drawing :refer [draw click-draw-bonus]] @@ -58,7 +58,7 @@ [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 damage-prevent* prevent-encounter prevent-tag prevent-up-to-n-tags prevent-up-to-n-damage]] + [game.core.prevention :refer [damage-name prevent-damage prevent-encounter prevent-tag 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]] @@ -586,7 +586,7 @@ (= :net (:type context)) (not (:unpreventable context)))) :msg (msg "prevent " (dec (:remaining context)) " " (damage-name state :pre-damage) " damage") - :effect (req (damage-prevent* state side eid :damage (dec (:remaining context))))}}]}) + :effect (req (prevent-damage state side eid :damage (dec (:remaining context))))}}]}) (defcard "Biometric Spoofing" {:prevention [{:prevents :damage @@ -598,7 +598,7 @@ (and (pos? (:remaining context)) (not (:unpreventable context)))) :msg (msg "prevent " (min 2 (:remaining context)) " " (damage-name state :pre-damage) " damage") - :effect (req (damage-prevent* state side eid :damage (min 2 (:remaining context))))}}]}) + :effect (req (prevent-damage state side eid :damage (min 2 (:remaining context))))}}]}) (defcard "Blockade Runner" {:abilities [{:action true @@ -659,7 +659,7 @@ :req (req (and (contains? #{:net :core :brain} (:type context)) (not (:unpreventable context)) (pos? (:remaining context)))) - :effect (req (damage-prevent* state side eid :damage 1))}}]}) + :effect (req (prevent-damage state side eid :damage 1))}}]}) (defcard "Charlatan" {:abilities [{:action true @@ -743,7 +743,7 @@ (has-subtype? (:source-card context) "Cybernetic") (not (:unpreventable context)))) :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (damage-prevent* state side eid :pre-damage :all))}}]}) + :effect (req (prevent-damage state side eid :pre-damage :all))}}]}) (defcard "Citadel Sanctuary" {:prevention [{:prevents :damage @@ -755,7 +755,7 @@ (= :meat (:type context)) (not (:unpreventable context)))) :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (damage-prevent* state side eid :damage :all))}}] + :effect (req (prevent-damage state side eid :damage :all))}}] :events [{:event :runner-turn-ends :interactive (req true) :msg "force the Corp to initiate a trace" @@ -922,17 +922,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 :damage #{: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 @@ -1632,7 +1630,7 @@ (= :net (:type context))) (not (:unpreventable context)))) :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (wait-for (damage-prevent* state side :pre-damage :all) + :effect (req (wait-for (prevent-damage state side :pre-damage :all) (continue-ability state side {:msg (msg (if (= target "Trash Guru Davinder") @@ -1861,7 +1859,7 @@ (not (:unpreventable context)) (= :meat (:type context)) (pos? (:remaining context)))) - :effect (req (damage-prevent* state side eid :damage 1))}}]}) + :effect (req (prevent-damage state side eid :damage 1))}}]}) (defcard "John Masanori" {:events [{:event :successful-run @@ -2556,7 +2554,7 @@ (= :meat (:type context)) (not (:unpreventable context)))) :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (damage-prevent* state side eid :pre-damage :all))}}] + :effect (req (prevent-damage state side eid :pre-damage :all))}}] :static-abilities [{:type :is-tagged :value true}]}) @@ -2919,7 +2917,7 @@ (and (pos? (:remaining context)) (not (:unpreventable context)))) :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (wait-for (damage-prevent* state side :damage :all) + :effect (req (wait-for (prevent-damage state side :damage :all) (let [cards (concat (get-in runner [:rig :hardware]) (filter #(not (has-subtype? % "Virtual")) (get-in runner [:rig :resource])) diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index a93e4f996c..3fb17b7b76 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -1743,7 +1743,7 @@ (= :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}))}}]}) + :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 diff --git a/src/clj/game/core.clj b/src/clj/game/core.clj index 1241dfd582..1d83068743 100644 --- a/src/clj/game/core.clj +++ b/src/clj/game/core.clj @@ -280,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?]) diff --git a/src/clj/game/core/damage.clj b/src/clj/game/core/damage.clj index 2873f20b73..98415d0871 100644 --- a/src/clj/game/core/damage.clj +++ b/src/clj/game/core/damage.clj @@ -14,11 +14,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" @@ -27,40 +22,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)) @@ -95,35 +56,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]}] - (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) @@ -142,51 +96,21 @@ (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}] + ([state side eid type n {:keys [unpreventable card suppress-checkpoint] :as args}] (wait-for (resolve-damage-prevention state side type n args) - (println async-result)))) + (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)))))))) ;; (swap! state update-in [:damage :damage-bonus] dissoc type) ;; (swap! state update-in [:damage :damage-prevent] dissoc type) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 93482534c9..03cb9b7391 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -150,6 +150,8 @@ ;; Muresh Bodysuit (done) ;; The Cleaners (done) ;; Paparrazi (done) +;; The Noble Path (done) +;; Defective Brainchips (done) ;; ;; The following are normal prevention timing: ;; Hardware: @@ -199,8 +201,16 @@ (swap! state update-in [:prevent key :remaining] + n)) (effect-completed state side eid)) -;; TODO - rename this after I strip it out of damage -(defn damage-prevent* +(defn damage-name + [state key] + (case (damage-type state key) + :meat "meat" + :brain "core" + :core "core" + :net "net" + "neat")) + +(defn prevent-damage [state side eid key n] (when (pos? (damage-pending state key)) (if (= n :all) @@ -222,17 +232,8 @@ :default (req (max-to-avoid state n))} :async true :msg (msg "prevent " target " " (damage-name state key) " damage") - :effect (req (damage-prevent* state side eid key target)) - :cancel-effect (req (damage-prevent* state side eid key 0))})) - -(defn damage-name - [state key] - (case (damage-type state key) - :meat "meat" - :brain "core" - :core "core" - :net "net" - "neat")) + :effect (req (prevent-damage state side eid key target)) + :cancel-effect (req (prevent-damage state side eid key 0))})) (defn resolve-pre-damage-for-side [state side eid] From 7a129082e64fb7119368296a9a9182ba31082034 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Wed, 26 Feb 2025 17:11:26 +1300 Subject: [PATCH 26/38] declining a prevention doesn't count as using it --- src/clj/game/core/prevention.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 03cb9b7391..df521368d7 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -86,7 +86,6 @@ (defn- trigger-prevention "Triggers an ability as having prevented something" [state side eid key prevention] - (swap! state update-in [:prevent key :uses (->> prevention :card :cid)] (fnil inc 0)) ;; 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 @@ -94,6 +93,7 @@ ;; 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) From 6091e313fb217dc62a363b7e311d2e94ab599560 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Wed, 26 Feb 2025 18:05:42 +1300 Subject: [PATCH 27/38] 100 unit tests down... --- src/clj/game/cards/assets.clj | 3 ++- src/clj/game/cards/events.clj | 3 ++- src/clj/game/cards/hardware.clj | 9 ++++++--- src/clj/game/cards/resources.clj | 18 ++++++++++++------ src/clj/game/core/cost_fns.clj | 1 + src/clj/game/core/costs.clj | 1 - src/clj/game/core/damage.clj | 11 ----------- src/clj/game/core/prevention.clj | 2 +- test/clj/game/cards/agendas_test.clj | 7 ++++--- test/clj/game/cards/assets_test.clj | 18 ++++++++---------- test/clj/game/cards/resources_test.clj | 21 +++++++++++---------- test/clj/game/cards/upgrades_test.clj | 14 +++++++------- test/clj/game/core/abilities_test.clj | 3 +-- test/clj/game/core/scenarios_test.clj | 5 ++--- 14 files changed, 57 insertions(+), 59 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index bc3a390820..a8ef1856eb 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -3259,7 +3259,8 @@ (rez state side eid (last (:hosted (get-card state card))) {:cost-bonus -2})))}]}) (defcard "Zaibatsu Loyalty" - {:prevention [{:prevents :expose + {:trash-icon true + :prevention [{:prevents :expose :type :ability :label "1 [Credit]: Zaibatsu Loyalty" :ability {:cost [(->c :credit 1)] diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index bbc16c6514..7f36dc01fa 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -2695,7 +2695,8 @@ (draw state :runner eid 4)))}}) (defcard "On the Lam" - {:prevention [{:prevents :tag + {:trash-icon true + :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)])} diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 12050935dd..5bd1fe82d0 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -846,7 +846,8 @@ :abilities [(break-sub [(->c :power 2)] 2 "All")]})) (defcard "Feedback Filter" - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :label "Feedback Filter (Net)" :ability {:async true @@ -1934,7 +1935,8 @@ (defcard "Ramujan-reliant 550 BMI" (letfn [(max-trash [state] (inc (count (filter #(= (:title %) "Ramujan-reliant 550 BMI") (all-installed state :runner)))))] - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :ability {:async true :cost [(->c :trash-can)] @@ -1949,7 +1951,8 @@ (mill state :runner eid :runner prevented-this-instance)))))}}]})) (defcard "Recon Drone" - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :ability {:async true :req (req (and (pos? (:remaining context)) diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index a3c0ab7b97..03555272f3 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -576,7 +576,8 @@ :effect (req (mill state :corp eid :corp 1))}]}) (defcard "Bio-Modeled Network" - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :max-uses 1 :ability {:async true @@ -589,7 +590,8 @@ :effect (req (prevent-damage state side eid :damage (dec (:remaining context))))}}]}) (defcard "Biometric Spoofing" - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :max-uses 1 :ability {:async true @@ -746,7 +748,8 @@ :effect (req (prevent-damage state side eid :pre-damage :all))}}]}) (defcard "Citadel Sanctuary" - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :ability {:async true :cost [(->c :trash-can)] @@ -922,7 +925,8 @@ (make-run eid target card))}]})) (defcard "Crash Space" - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :ability (assoc (prevent-up-to-n-damage 3 :damage #{:meat}) :cost [(->c :trash-can)])}] @@ -1641,6 +1645,7 @@ :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))))} @@ -2320,7 +2325,7 @@ run (= :corp (:active-player @state)) (#{:psi :trace} (:source-type eid)) - (get-in @state [:prevention]))) + (get-in @state [:prevent]))) :type :credit}}}) (defcard "Network Exchange" @@ -2908,7 +2913,8 @@ card nil))}]})) (defcard "Sacrificial Clone" - {:prevention [{:prevents :damage + {:trash-icon true + :prevention [{:prevents :damage :type :ability :max-uses 1 :ability {:async true diff --git a/src/clj/game/core/cost_fns.clj b/src/clj/game/core/cost_fns.clj index 88a56c0c44..d32623ba40 100644 --- a/src/clj/game/core/cost_fns.clj +++ b/src/clj/game/core/cost_fns.clj @@ -112,6 +112,7 @@ (let [abilities (:abilities (card-def card)) events (:events (card-def card))] (or (some :trash-icon (concat abilities events)) + (:trash-icon (card-def card)) (some #(= :trash-can (:cost/type %)) (->> abilities (map :cost) diff --git a/src/clj/game/core/costs.clj b/src/clj/game/core/costs.clj index a15a468845..167b07ed07 100644 --- a/src/clj/game/core/costs.clj +++ b/src/clj/game/core/costs.clj @@ -193,7 +193,6 @@ (<= (stealth-value cost) (total-available-stealth-credits state side eid card)))) (defmethod handler :x-credits [cost state side eid card] - (println cost) (continue-ability state side {:async true diff --git a/src/clj/game/core/damage.clj b/src/clj/game/core/damage.clj index 98415d0871..2a81f93116 100644 --- a/src/clj/game/core/damage.clj +++ b/src/clj/game/core/damage.clj @@ -111,14 +111,3 @@ (if suppress-checkpoint (effect-completed state side eid) (checkpoint state side eid)))))))) - - ;; (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))))))) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index df521368d7..1676e039aa 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -80,7 +80,7 @@ "get the prevent map for a key and also dissoc it from the state" [state key] (let [res (get-in @state [:prevent key])] - (swap! state dissoc-in [:prevent key]) + (swap! state dissoc :prevent) res)) (defn- trigger-prevention diff --git a/test/clj/game/cards/agendas_test.clj b/test/clj/game/cards/agendas_test.clj index 5f733d190c..29043c9f12 100644 --- a/test/clj/game/cards/agendas_test.clj +++ b/test/clj/game/cards/agendas_test.clj @@ -4025,13 +4025,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 9514060262..1dca17826b 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 @@ -4159,18 +4157,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 @@ -4187,7 +4185,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"))))) @@ -4205,7 +4203,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")))) @@ -4259,7 +4257,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 diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 15cddf4f9f..527bf2fad2 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -867,7 +867,7 @@ (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") (is (= 3 (count (:discard (get-runner)))) "CS and all cards in grip are trashed"))) (deftest citadel-sanctuary-end-of-turn-trace @@ -4456,16 +4456,14 @@ (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)] + (let [nm (get-resource state 0)] (core/add-counter state :runner (refresh nm) :credit 4) (damage state :corp :net 2) - (card-ability state :runner ff 0) + (click-prompt state :runner "Feedback Filter (Net)") (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") + (click-prompt state :runner "Pass priority") (is (= 1 (get-counters (refresh nm) :credit)) "Net Mercur has lost 3 credits")))) (deftest network-exchange @@ -4629,15 +4627,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") @@ -4677,9 +4676,11 @@ (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 off-campus-apartment-ability-shows-a-simultaneous-resolution-prompt-when-appropriate diff --git a/test/clj/game/cards/upgrades_test.clj b/test/clj/game/cards/upgrades_test.clj index aa5fa9a446..dc333372ab 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)))) diff --git a/test/clj/game/core/abilities_test.clj b/test/clj/game/core/abilities_test.clj index ff2e3484f4..d61e7a0485 100644 --- a/test/clj/game/core/abilities_test.clj +++ b/test/clj/game/core/abilities_test.clj @@ -59,8 +59,7 @@ (doseq [card (->> (vals @all-cards) (filter #(re-find #"(?i)\[trash\].*:" (:text % "")))) :when (not-empty (card-def card))] - (is (or (core/has-trash-ability? card) - (zero? (count (:abilities card)))) + (is (core/has-trash-ability? card) (str (:title card) " needs either :cost [(->c :trash-can)] or :trash-icon true")))) (defn- x-has-labels diff --git a/test/clj/game/core/scenarios_test.clj b/test/clj/game/core/scenarios_test.clj index d24a4fdf67..8ee425d71f 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 From 0ef4a8caa4fc06d27f5b7d58cfb97383e8b02a36 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Wed, 26 Feb 2025 19:53:41 +1300 Subject: [PATCH 28/38] all damage tests pass --- src/clj/game/cards/events.clj | 2 +- src/clj/game/cards/hardware.clj | 7 +- src/clj/game/cards/resources.clj | 4 +- test/clj/game/cards/agendas_test.clj | 2 +- test/clj/game/cards/events_test.clj | 36 +++-- test/clj/game/cards/hardware_test.clj | 175 ++++++++++-------------- test/clj/game/cards/ice_test.clj | 22 ++- test/clj/game/cards/identities_test.clj | 12 +- test/clj/game/cards/operations_test.clj | 2 +- test/clj/game/cards/programs_test.clj | 6 +- test/clj/game/cards/resources_test.clj | 1 + 11 files changed, 119 insertions(+), 150 deletions(-) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 7f36dc01fa..56c19daf16 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -3939,7 +3939,7 @@ :effect (req (prevent-damage state side eid :pre-damage :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 diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 5bd1fe82d0..d5cd25500a 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -1151,7 +1151,7 @@ :label "Heartbeat" :ability {:async true :cost [(->c :trash-installed 1)] - :msg (msg "prevent 1 " (damage-type state :damage) " damage") + :msg (msg "prevent 1 " (damage-name state :damage) " damage") :req (req (and (not (:unpreventable context)) (pos? (:remaining context)))) :effect (req (prevent-damage state side eid :damage 1))}}]}) @@ -1499,7 +1499,7 @@ :type :ability :ability {:async true :cost [(->c :trash-program-from-hand 1)] - :msg (msg "prevent 1 " (damage-type state :damage) " damage") + :msg (msg "prevent 1 " (damage-name state :damage) " damage") :req (req (and (not (= :meat (:type context))) (not (:unpreventable context)) (pos? (:remaining context))))}}]})) @@ -1786,8 +1786,7 @@ :ability {:async true :cost [(->c :power 1)] :msg "prevent 1 meat damage" - :req (req (and run - (not (:unpreventable context)) + :req (req (and (not (:unpreventable context)) (= :meat (:type context)) (pos? (:remaining context)))) :effect (req (prevent-damage state side eid :damage 1))}}] diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 03555272f3..1fd914ed3b 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -751,8 +751,9 @@ {:trash-icon true :prevention [{:prevents :damage :type :ability + :prompt "Use Citadel Sanctuary to prevent meat damage?" :ability {:async true - :cost [(->c :trash-can)] + :cost [(->c :trash-can) (->c :trash-entire-hand)] :req (req (and (pos? (:remaining context)) (= :meat (:type context)) @@ -1854,6 +1855,7 @@ :effect (req (wait-for (gain-tags state :runner 1) (add-counter state :runner card :power (+ 3 (count-tags state))) (effect-completed state :runner eid)))} + :flags {:untrashable-while-resources true} :events [(trash-on-empty :power)] :prevention [{:prevents :damage :type :ability diff --git a/test/clj/game/cards/agendas_test.clj b/test/clj/game/cards/agendas_test.clj index 29043c9f12..58f48a9f9e 100644 --- a/test/clj/game/cards/agendas_test.clj +++ b/test/clj/game/cards/agendas_test.clj @@ -4032,7 +4032,7 @@ (is (= 2 (count (:discard (get-runner))))) (card-subroutine state :corp viktor 0) (click-prompt state :runner "Feedback Filter (Core)") - (click-prompt state :runner 1) ;; Prevent the brain damage this time + (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/events_test.clj b/test/clj/game/cards/events_test.clj index 373db20395..214ef26891 100644 --- a/test/clj/game/cards/events_test.clj +++ b/test/clj/game/cards/events_test.clj @@ -5031,7 +5031,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) 0) + (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"))) @@ -7133,25 +7135,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 39283ba8c6..cb56e6fd02 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -121,7 +121,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")))) @@ -1922,22 +1922,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 @@ -2384,30 +2383,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 @@ -4045,10 +4034,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")))) @@ -4401,58 +4390,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 @@ -4479,18 +4461,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") @@ -4501,27 +4479,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")))) diff --git a/test/clj/game/cards/ice_test.clj b/test/clj/game/cards/ice_test.clj index de2125b556..b6186506e9 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 @@ -6424,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")))) diff --git a/test/clj/game/cards/identities_test.clj b/test/clj/game/cards/identities_test.clj index 63faf515b0..dd2e696007 100644 --- a/test/clj/game/cards/identities_test.clj +++ b/test/clj/game/cards/identities_test.clj @@ -2689,6 +2689,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 a2d7c03007..bc5c5da4ba 100644 --- a/test/clj/game/cards/operations_test.clj +++ b/test/clj/game/cards/operations_test.clj @@ -4563,7 +4563,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 diff --git a/test/clj/game/cards/programs_test.clj b/test/clj/game/cards/programs_test.clj index 75f6f81a71..a5d646bfa9 100644 --- a/test/clj/game/cards/programs_test.clj +++ b/test/clj/game/cards/programs_test.clj @@ -2878,7 +2878,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 @@ -2893,7 +2894,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")))) diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 527bf2fad2..85f6b888ac 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -868,6 +868,7 @@ (play-from-hand state :corp "Scorched Earth") (is (zero? (count (:discard (get-runner)))) "No cards have been discarded or trashed yet") (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 From 410b6ece74803d819a425111f9b1b36013fb6d8c Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Wed, 26 Feb 2025 21:58:33 +1300 Subject: [PATCH 29/38] liza is simultaneous --- src/clj/game/cards/identities.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index dbb9b2e541..0a800bfdd4 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -1252,7 +1252,7 @@ (fn [targets] (let [context (first targets)] (is-central? (:server context))))))) - :effect (req (wait-for (gain-tags state :runner 1) + :effect (req (wait-for (gain-tags state :runner 1 {:suppress-checkpoint true}) (draw state :runner eid 2)))}]}) (defcard "Los: Data Hijacker" From 106caf4b5c8ca00b9946f3789da6375930d5de23 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Wed, 26 Feb 2025 23:01:53 +1300 Subject: [PATCH 30/38] started on trash prevention --- src/clj/game/cards/assets.clj | 16 ++- src/clj/game/cards/identities.clj | 4 +- src/clj/game/cards/programs.clj | 14 +-- src/clj/game/cards/resources.clj | 47 +++----- src/clj/game/core/moving.clj | 94 +++++---------- src/clj/game/core/prevention.clj | 161 ++++++++++++++++--------- src/clj/game/core/shuffling.clj | 4 +- test/clj/game/cards/assets_test.clj | 20 +-- test/clj/game/cards/resources_test.clj | 2 +- test/clj/game/core/rules_test.clj | 8 +- 10 files changed, 177 insertions(+), 193 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index a8ef1856eb..38379512ff 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -1689,15 +1689,13 @@ :events [(assoc ability :event :corp-turn-begins)] :on-rez {:effect (req (add-counter state side card :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] (if (same-card? card (:card ctx)) (assoc ctx :destination :shuffle-rd) ctx))))}}]})) (defcard "Mark Yale" {:events [{:event :agenda-counter-spent diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index 0a800bfdd4..696edc665c 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -1252,8 +1252,8 @@ (fn [targets] (let [context (first targets)] (is-central? (:server context))))))) - :effect (req (wait-for (gain-tags state :runner 1 {:suppress-checkpoint true}) - (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 diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index ca03fbafa5..709712a2bb 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -40,8 +40,7 @@ [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 [damage-name prevent-damage prevent-end-run prevent-up-to-n-damage]] @@ -1973,14 +1972,9 @@ (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] (not= :ability-cost (:cause context)))] + {:prevention [(prevent-trash-installed-by-type "3 [Credits]: LLDS Energy Regulator" "Hardware" [(->c :credits 3)] valid-context?) + (prevent-trash-installed-by-type "[Trash]: LLDS Energy Regulator" "Hardware" [(->c :trash-can 3)] valid-context?)]})) (defcard "Lobisomem" (auto-icebreaker {:data {:counter {:power 1}} diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 1fd914ed3b..9fff025bcb 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -52,13 +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 prevent-encounter prevent-tag prevent-up-to-n-tags prevent-up-to-n-damage]] + [game.core.prevention :refer [damage-name prevent-damage 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]] @@ -1317,18 +1316,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" @@ -1416,16 +1407,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" - :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 @@ -2945,12 +2933,9 @@ (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] (not= :ability-cost (:cause context)))] + {:prevention [(prevent-trash-installed-by-type "Sacrificial Construct (Program)" "Program" [(->c :trash-can)] valid-context?) + (prevent-trash-installed-by-type "Sacrificial Construct (Hardware)" "Hardware" [(->c :trash-can)] valid-context?)]})) (defcard "Safety First" {:static-abilities [(runner-hand-size+ -2)] diff --git a/src/clj/game/core/moving.clj b/src/clj/game/core/moving.clj index 4f5c34ec50..7f6fd9a8a1 100644 --- a/src/clj/game/core/moving.clj +++ b/src/clj/game/core/moving.clj @@ -14,6 +14,7 @@ [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]] @@ -267,52 +268,6 @@ [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" [state trashlist] @@ -326,15 +281,15 @@ (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- 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." @@ -402,14 +357,16 @@ ([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 (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)) 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})) + 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 (map :destination 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] @@ -419,18 +376,27 @@ ;; 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] + (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) + moved-card (move-card card destination) trash-effect (get-trash-effect state side eid card args)] (update-indicies card) (conj acc [moved-card trash-effect])) acc)) [] 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 :runner-shuffle-deck)) (swap! state update-in [:trash :trash-list] dissoc eid) - (when (and side (seq (remove #{side} (map #(to-keyword (:side %)) trashlist)))) + (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] @@ -439,7 +405,7 @@ (doseq [[card trash-effect] moved-cards :when trash-effect] (register-pending-event state trash-event card trash-effect)) - (doseq [trashed-card trashlist] + (doseq [trashed-card (map :card trashlist)] (queue-event state trash-event {:card trashed-card :cause cause :cause-card (trim-cause-card cause-card) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 1676e039aa..142dad6639 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -1,15 +1,18 @@ (ns game.core.prevention (:require - [game.core.board :refer [all-active]] - [game.core.card :refer [get-card rezzed? same-card?]] + [clojure.set :as set] + [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 [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]] @@ -73,16 +76,25 @@ (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") + (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])] - (swap! state dissoc :prevent) + (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] @@ -141,51 +153,87 @@ nil nil) (resolve-keyed-prevention-for-side state side eid key args)))))))) -;; DAMAGE PREVENTION -;; -;; The following are either interrupts, or static abilities, that must come first: -;; Guru Davinder (done) -;; Leverage (done) -;; Chrome Parlor (done) -;; Muresh Bodysuit (done) -;; The Cleaners (done) -;; Paparrazi (done) -;; The Noble Path (done) -;; Defective Brainchips (done) -;; -;; The following are normal prevention timing: -;; Hardware: -;; AirbladeX (JSRF Ed.) (done) -;; Feedback Filter (done) -;; Heartbeat (done) -;; Monolith (done) -;; Plascrete Carapace (done) -;; Ramujan-reliant 550 BMI (done) -;; Recon Drone (done) -;; Resources: -;; Jarogniew Mercs (done) -;; No One Home (done) -;; Sacrificial Clone (lmao) (done) -;; Bio-Modeled Network (done) -;; Biometric Spoofing (done) -;; Caldera (done) -;; Citadel Sanctuary (done) -;; Programs: -;; Net Shield (done) -;; Deus X (done) -;; Events: -;; On the Lam (done) -;; Assets: -;; Prana Condenser -;; Upgrades: -;; Tori Hanzo -;; -;; These just nominate the selecting side, they can probably still be done in the damage class: -;; Titanium Ribs -;; Chronos Protocol: Selective Mind-mapping -;; -;; The follwing do something funny (confirm with rules what they actually do): -;; Tori Hanzo (set damage to 1, change type from net to brain) +;; TRASH PREVENTION + +(defn prevent-trash-installed-by-type + [label type cost valid-context?] + (letfn [(relevant [state] (filter #(and (= (:type %) type) + (installed? %)) + (map :card (get-in @state [:prevent :trash :remaining]))))] + {:prevents :trash + :type :ability + :label label + :ability {:req (req + (and (seq (relevant state)) + (valid-context? context) + (can-pay? state side eid card nil cost))) + :async true + :effect (req + (resolve-ability + state side eid + (if (= 1 (count (relevant state))) + {:msg (msg "prevent " (->> (relevant state) first :title) " from being trashed") + :cost cost + :effect (req (swap! state assoc-in [:prevent :trash :remaining] []))} + {:prompt (str "Choose a " type " to save from being trashed") + :cost cost + :choices (req (relevant state)) + :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))}})) + +(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) + (if (>= 5 (count (get-in @state [:prevent :trash :remaining]))) + (str "Prevent any of " (enumerate-str (map #(->> % :card :title) (get-in @state [:prevent :trash :remaining])) "or") " from being trashed?") + (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-effects + [state side eid] + (if (= 2 (get-in @state [:prevent :trash :priority-passes])) + (complete-with-result state side eid (fetch-and-clear! state :trash)) + (wait-for (resolve-trash-for-side state side) + (swap! state update-in [:prevent :trash :priority-passes] (fnil inc 1)) + (resolve-trash-effects state (other-side side) eid)))) + +(defn resolve-trash-prevention + [state side eid targets {:keys [unpreventable game-trash cause cause-card] :as args}] + (let [untrashable (mapv #(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"]) + targets) + trashable (when (seq 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-trash-effects state (:active-player @state) eid)))) + +;; DAMAGE PREVENTION + PRE-DAMAGE PREVENTION (defn damage-type [state key] @@ -258,7 +306,6 @@ ;; 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 -;; TODO - is this generic enough that I can just make a helper function for it and throw everything else through it? (defn resolve-damage-for-side [state side eid] (resolve-keyed-prevention-for-side @@ -280,7 +327,7 @@ (defn resolve-damage-prevention [state side eid type n {:keys [unpreventable unboostable card] :as args}] - (swap! state assoc-in [:prevent :pre-damage] + (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}) @@ -303,7 +350,7 @@ (defn resolve-encounter-prevention [state side eid {:keys [unpreventable card title] :as args}] - (swap! state assoc-in [:prevent :encounter] + (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)) @@ -325,7 +372,7 @@ (defn resolve-end-run-prevention [state side eid {:keys [unpreventable card] :as args}] - (swap! state assoc-in [:prevent :end-run] + (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}) @@ -350,7 +397,7 @@ (defn resolve-jack-out-prevention [state side eid {:keys [unpreventable card] :as args}] - (swap! state assoc-in [:prevent :jack-out] + (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)) @@ -388,7 +435,7 @@ (defn resolve-expose-prevention [state side eid targets {:keys [unpreventable card] :as args}] - (swap! state assoc-in [:prevent :expose] + (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}) @@ -421,7 +468,7 @@ (defn resolve-bad-pub-prevention [state side eid n {:keys [unpreventable card] :as args}] - (swap! state assoc-in [:prevent :bad-publicity] + (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)) @@ -462,7 +509,7 @@ (defn resolve-tag-prevention [state side eid n {:keys [unpreventable card] :as args}] - (swap! state assoc-in [:prevent :tag] + (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)) 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/test/clj/game/cards/assets_test.clj b/test/clj/game/cards/assets_test.clj index 1dca17826b..51c69b2825 100644 --- a/test/clj/game/cards/assets_test.clj +++ b/test/clj/game/cards/assets_test.clj @@ -3350,21 +3350,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")))) diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 85f6b888ac..6c8fa60590 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -2766,7 +2766,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 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"))) From c748bbee67dd5bae2e61603e9ad1585e0f020516 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Thu, 27 Feb 2025 13:20:31 +1300 Subject: [PATCH 31/38] all tests pass I think --- src/clj/game/cards/assets.clj | 3 +- src/clj/game/cards/programs.clj | 10 +- src/clj/game/cards/resources.clj | 15 +- src/clj/game/core.clj | 7 - src/clj/game/core/bad_publicity.clj | 1 - src/clj/game/core/damage.clj | 1 - src/clj/game/core/expose.clj | 1 - src/clj/game/core/flags.clj | 38 ----- src/clj/game/core/moving.clj | 19 +-- src/clj/game/core/prevention.clj | 180 +++++++++++------------- src/clj/game/core/runs.clj | 2 +- src/clj/game/core/tags.clj | 1 - test/clj/game/cards/agendas_test.clj | 17 +++ test/clj/game/cards/assets_test.clj | 2 +- test/clj/game/cards/hardware_test.clj | 20 ++- test/clj/game/cards/operations_test.clj | 19 ++- test/clj/game/cards/programs_test.clj | 4 +- test/clj/game/cards/resources_test.clj | 20 ++- test/clj/game/core/scenarios_test.clj | 2 +- 19 files changed, 148 insertions(+), 214 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index 38379512ff..33d6f01070 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -1688,14 +1688,13 @@ {:derezzed-events [corp-rez-toast] :events [(assoc ability :event :corp-turn-begins)] :on-rez {:effect (req (add-counter state side card :credit 8))} - :abilities [(set-autoresolve :auto-reshuffle "Marilyn Campaign shuffling itself back into R&D")] :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] (if (same-card? card (:card ctx)) (assoc ctx :destination :shuffle-rd) ctx))))}}]})) + :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 diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index 709712a2bb..7594bbe723 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -43,7 +43,7 @@ [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 [damage-name prevent-damage prevent-end-run prevent-up-to-n-damage]] + [game.core.prevention :refer [damage-name 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]] @@ -1972,9 +1972,11 @@ (strength-pump 1 2)]}))) (defcard "LLDS Energy Regulator" - (letfn [(valid-context? [context] (not= :ability-cost (:cause context)))] - {:prevention [(prevent-trash-installed-by-type "3 [Credits]: LLDS Energy Regulator" "Hardware" [(->c :credits 3)] valid-context?) - (prevent-trash-installed-by-type "[Trash]: LLDS Energy Regulator" "Hardware" [(->c :trash-can 3)] valid-context?)]})) + (letfn [(valid-context? [context] (and (not= :ability-cost (:cause context)) + (not (:game-trash context))))] + {:trash-icon true + :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 3)] valid-context?)]})) (defcard "Lobisomem" (auto-icebreaker {:data {:counter {:power 1}} diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 9fff025bcb..1ff88f115d 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -1317,9 +1317,9 @@ (defcard "Dummy Box" (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?)]})) + {: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" @@ -1408,7 +1408,7 @@ (defcard "Fall Guy" (letfn [(valid-context? [context] (not= :ability-cost (:cause context)))] - {:prevention [(prevent-trash-installed-by-type "Fall Guy" "Resource" [(->c :trash-can)] valid-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)] @@ -2933,9 +2933,10 @@ (wait-for (lose-credits state side (make-eid state eid) :all) (lose-tags state side eid :all))))))}}]}) (defcard "Sacrificial Construct" - (letfn [(valid-context? [context] (not= :ability-cost (:cause context)))] - {:prevention [(prevent-trash-installed-by-type "Sacrificial Construct (Program)" "Program" [(->c :trash-can)] valid-context?) - (prevent-trash-installed-by-type "Sacrificial Construct (Hardware)" "Hardware" [(->c :trash-can)] valid-context?)]})) + (letfn [(valid-context? [context] (and (not= :ability-cost (:cause context)) + (not (:game-trash context))))] + {:trash-icon true + :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/core.clj b/src/clj/game/core.clj index 1d83068743..8fccba8b6d 100644 --- a/src/clj/game/core.clj +++ b/src/clj/game/core.clj @@ -395,7 +395,6 @@ (expose-vars [game.core.flags - ab-can-prevent? any-flag-fn? can-access-loud can-access? @@ -406,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! @@ -417,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? @@ -588,7 +582,6 @@ swap-installed trash trash-cards - trash-prevent uninstall]) (expose-vars diff --git a/src/clj/game/core/bad_publicity.clj b/src/clj/game/core/bad_publicity.clj index 10f17d3770..6478cc5255 100644 --- a/src/clj/game/core/bad_publicity.clj +++ b/src/clj/game/core/bad_publicity.clj @@ -2,7 +2,6 @@ (:require [game.core.eid :refer [effect-completed make-eid make-result]] [game.core.engine :refer [trigger-event trigger-event-sync]] - [game.core.flags :refer [cards-can-prevent? get-prevent-list]] [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]] diff --git a/src/clj/game/core/damage.clj b/src/clj/game/core/damage.clj index 2a81f93116..eceb24d827 100644 --- a/src/clj/game/core/damage.clj +++ b/src/clj/game/core/damage.clj @@ -3,7 +3,6 @@ [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]] diff --git a/src/clj/game/core/expose.clj b/src/clj/game/core/expose.clj index 35b9bd38ec..53641559df 100644 --- a/src/clj/game/core/expose.clj +++ b/src/clj/game/core/expose.clj @@ -5,7 +5,6 @@ [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.flags :refer [cards-can-prevent? get-prevent-list]] [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]] diff --git a/src/clj/game/core/flags.clj b/src/clj/game/core/flags.clj index 9591d40672..c56ad7b95b 100644 --- a/src/clj/game/core/flags.clj +++ b/src/clj/game/core/flags.clj @@ -321,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 7f6fd9a8a1..3f6ca34cb6 100644 --- a/src/clj/game/core/moving.clj +++ b/src/clj/game/core/moving.clj @@ -9,7 +9,7 @@ [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]] @@ -264,9 +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 update-current-ice-to-trash "If the current ice is going to be trashed, update it with any changes" @@ -281,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]}] @@ -366,7 +353,7 @@ ;; 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 (map :destination trashlist)) + 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] @@ -394,7 +381,7 @@ (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 :runner-shuffle-deck)) + (trigger-event state side :corp-shuffle-deck)) (swap! state update-in [:trash :trash-list] dissoc eid) (when (and side (seq (remove #{side} (map #(to-keyword (:side %)) (map :card trashlist))))) (swap! state assoc-in [side :register :trashed-card] true)) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 142dad6639..08bd7a7d5d 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -1,6 +1,7 @@ (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]] @@ -10,7 +11,7 @@ [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 [can-pay?]] + [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]] @@ -39,8 +40,8 @@ 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 %))) + (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?)) @@ -153,34 +154,49 @@ 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)))) + ;; TRASH PREVENTION (defn prevent-trash-installed-by-type - [label type cost valid-context?] - (letfn [(relevant [state] (filter #(and (= (:type %) type) - (installed? %)) - (map :card (get-in @state [:prevent :trash :remaining]))))] + [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)) + (and (seq (relevant state card)) + (not (:unpreventable context)) (valid-context? context) (can-pay? state side eid card nil cost))) :async true :effect (req - (resolve-ability - state side eid - (if (= 1 (count (relevant state))) - {:msg (msg "prevent " (->> (relevant state) first :title) " from being trashed") - :cost cost - :effect (req (swap! state assoc-in [:prevent :trash :remaining] []))} - {:prompt (str "Choose a " type " to save from being trashed") - :cost cost - :choices (req (relevant state)) - :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))}})) + (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] @@ -189,36 +205,33 @@ {:data-type :sequential :prompt (fn [state remainder] (if (= side :runner) - (if (>= 5 (count (get-in @state [:prevent :trash :remaining]))) - (str "Prevent any of " (enumerate-str (map #(->> % :card :title) (get-in @state [:prevent :trash :remaining])) "or") " from being trashed?") + (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-effects - [state side eid] - (if (= 2 (get-in @state [:prevent :trash :priority-passes])) - (complete-with-result state side eid (fetch-and-clear! state :trash)) - (wait-for (resolve-trash-for-side state side) - (swap! state update-in [:prevent :trash :priority-passes] (fnil inc 1)) - (resolve-trash-effects state (other-side side) eid)))) - (defn resolve-trash-prevention [state side eid targets {:keys [unpreventable game-trash cause cause-card] :as args}] - (let [untrashable (mapv #(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"]) - targets) - trashable (when (seq untrashable) + (let [untrashable (keep identity (map #(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) @@ -227,11 +240,11 @@ (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 {}}) + {: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-trash-effects state (:active-player @state) eid)))) + (resolve-prevent-effects-with-priority state (:active-player @state) eid :trash resolve-trash-for-side)))) ;; DAMAGE PREVENTION + PRE-DAMAGE PREVENTION @@ -294,15 +307,6 @@ :waiting "your opponent to resolve pre-damage triggers" :option "Pass priority"})) -(defn- resolve-pre-damage-effects - [state side eid] - (clear-wait-prompt state side) ;; TODO - do I need this? - (if (= 2 (get-in @state [:prevent :pre-damage :priority-passes])) - (complete-with-result state side eid (fetch-and-clear! state :pre-damage)) - (wait-for (resolve-pre-damage-for-side state side) - (swap! state update-in [:prevent :pre-damage :priority-passes] (fnil inc 1)) - (resolve-pre-damage-effects state (other-side side) eid)))) - ;; 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 @@ -317,24 +321,16 @@ :waiting "your opponent to resolve damage triggers" :option "Pass priority"})) -(defn resolve-damage-effects - [state side eid] - (if (= 2 (get-in @state [:prevent :damage :priority-passes])) - (complete-with-result state side eid (fetch-and-clear! state :damage)) - (wait-for (resolve-damage-for-side state side) - (swap! state update-in [:prevent :damage :priority-passes] (fnil inc 1)) - (resolve-damage-effects state (other-side side) eid)))) - (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 {}}) + {: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-pre-damage-effects state (:active-player @state)) + (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-damage-effects state (:active-player @state) eid)))) + (resolve-prevent-effects-with-priority state (:active-player @state) eid :damage resolve-damage-for-side)))) ;; ENCOUNTER PREVENTION (def prevent-encounter @@ -351,12 +347,10 @@ (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 {}}) + {: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)) - (wait-for - (resolve-encounter-prevention-for-side state :runner) - (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 @@ -373,14 +367,12 @@ (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 {}}) + {: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)) - (wait-for - (resolve-end-run-prevention-for-side state :runner) - (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 @@ -398,7 +390,7 @@ (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 {}}) + {: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 @@ -436,12 +428,12 @@ (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 {}}) + {: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 :counnt] (count 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) @@ -469,16 +461,10 @@ (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 {}}) + {: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)) - (let [active-side (:active-player @state) - responding-side (other-side active-side)] - (wait-for - (resolve-bad-pub-prevention-for-side state active-side) - (wait-for - (resolve-bad-pub-prevention-for-side state responding-side) - (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 @@ -510,14 +496,14 @@ (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)))))))) + {: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 4a4787e1b1..e2d0512ebf 100644 --- a/src/clj/game/core/runs.clj +++ b/src/clj/game/core/runs.clj @@ -8,7 +8,7 @@ [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]] + [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?]] diff --git a/src/clj/game/core/tags.clj b/src/clj/game/core/tags.clj index da6bf0c15f..2c7ccc0716 100644 --- a/src/clj/game/core/tags.clj +++ b/src/clj/game/core/tags.clj @@ -3,7 +3,6 @@ [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]] diff --git a/test/clj/game/cards/agendas_test.clj b/test/clj/game/cards/agendas_test.clj index 58f48a9f9e..1224733e8c 100644 --- a/test/clj/game/cards/agendas_test.clj +++ b/test/clj/game/cards/agendas_test.clj @@ -3572,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 diff --git a/test/clj/game/cards/assets_test.clj b/test/clj/game/cards/assets_test.clj index 51c69b2825..a646d5dafe 100644 --- a/test/clj/game/cards/assets_test.clj +++ b/test/clj/game/cards/assets_test.clj @@ -4548,7 +4548,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") diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index cb56e6fd02..1c40e1cd11 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 (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 diff --git a/test/clj/game/cards/operations_test.clj b/test/clj/game/cards/operations_test.clj index bc5c5da4ba..bc37c58866 100644 --- a/test/clj/game/cards/operations_test.clj +++ b/test/clj/game/cards/operations_test.clj @@ -1041,7 +1041,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") @@ -4987,9 +4987,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")))) @@ -5008,14 +5007,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?" + (is (= "Prevent any of Fall Guy, Fall Guy, or Off-Campus Apartment from being trashed?" (: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?" - (: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 a5d646bfa9..9701f30f4f 100644 --- a/test/clj/game/cards/programs_test.clj +++ b/test/clj/game/cards/programs_test.clj @@ -7207,11 +7207,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 6c8fa60590..c91ffc10db 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -2221,11 +2221,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") @@ -4957,8 +4956,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") @@ -5548,11 +5546,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"))) @@ -6264,7 +6262,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 @@ -7604,14 +7602,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/core/scenarios_test.clj b/test/clj/game/core/scenarios_test.clj index 8ee425d71f..695cffa69d 100644 --- a/test/clj/game/core/scenarios_test.clj +++ b/test/clj/game/core/scenarios_test.clj @@ -466,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")))))) From 73a1dbec1fee4067c6e4994225439905533fae4e Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Fri, 28 Feb 2025 14:57:48 +1300 Subject: [PATCH 32/38] fixed up damage a bit as per discussions with jamie --- src/clj/game/cards/agendas.clj | 4 +++- src/clj/game/cards/assets.clj | 8 ++++++- src/clj/game/cards/events.clj | 12 +++++----- src/clj/game/cards/hardware.clj | 7 ++++-- src/clj/game/cards/operations.clj | 1 + src/clj/game/cards/resources.clj | 40 +++++++++++++++++-------------- src/clj/game/core/prevention.clj | 4 +++- 7 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index 02c3c3b3db..7062aecfe0 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -1208,6 +1208,8 @@ :msg "prevent the runner from jacking out for the remainder of this run" :condition :active :async true + :req (req (and (pos? (:remaining context)) + (not (:unpreventable context)))) :effect (req (wait-for (prevent-jack-out state side) (register-lingering-effect state side card @@ -2252,8 +2254,8 @@ :condition :active :req (req (and (= :meat (:type context)) + (not= :all (:prevented context)) (= :corp (:source-player context)) - (pos? (:remaining context)) (not (:unboostable context)))) :msg "increase the pending meat damage by 1" :effect (req (damage-boost state side eid :pre-damage 1))}}]}) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index 0b496aa9db..2e0dc435a7 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -448,7 +448,9 @@ :type :event :max-uses 1 :mandatory true - :ability {:trace {:base 3 + :ability {:req (req (and (pos? (:remaining context)) + (not (:unpreventable context)))) + :trace {:base 3 :successful {:msg "prevent all bad publicity" :async true :effect (req (prevent-bad-publicity state side eid :all))}}}}]}) @@ -3288,6 +3290,8 @@ :type :ability :label "1 [Credit]: Zaibatsu Loyalty" :ability {:cost [(->c :credit 1)] + :req (req (and (seq (:remaining context)) + (not (:unpreventable context)))) :msg "prevent a card from being exposed" :async true :effect (req (prevent-expose state side eid card))}} @@ -3295,6 +3299,8 @@ :type :ability :label "[trash]: Zaibatsu Loyalty" :ability {:cost [(->c :trash-can)] + :req (req (and (seq (:remaining context)) + (not (:unpreventable context)))) :msg "prevent a card from being exposed" :async true :effect (req (prevent-expose state side eid card))}}] diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index b27f2c3daa..60a489ca9d 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -2393,7 +2393,7 @@ {:type :prevention :duration :until-runner-turn-begins :req (req (= :runner side)) - :value {:prevents :pre-damage + :value {:prevents :damage :type :floating :max-uses 1 :card card @@ -2404,8 +2404,8 @@ :req (req (and (pos? (:remaining context)) (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (prevent-damage state side eid :pre-damage :all))}}}))}}}}) + :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 @@ -3929,7 +3929,7 @@ {:type :cannot-pay-meat :req (req run) :value true}] - :prevention [{:prevents :pre-damage + :prevention [{:prevents :damage :type :event :max-uses 1 :mandatory true @@ -3941,8 +3941,8 @@ (pos? (:remaining context)) (not (:unpreventable context)))) :condition :active - :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (prevent-damage state side eid :pre-damage :all))}}] + :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") + :effect (req (prevent-damage state side eid :damage :all))}}] :on-play {:async true :change-in-game-state (req (or (seq (:hand runner)) (seq runnable-servers))) diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 7f7244fd1c..6f33af9251 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -113,6 +113,7 @@ :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)]}) @@ -1347,6 +1348,7 @@ {: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 @@ -1537,7 +1539,7 @@ card [(breach-access-bonus :rd 1 {:duration :end-of-run})]))}}}]}) (defcard "Muresh Bodysuit" - {:prevention [{:prevents :pre-damage + {:prevention [{:prevents :damage :type :event :max-uses 1 :mandatory true @@ -1549,7 +1551,7 @@ (pos? (:remaining context)) (not (:unpreventable context)))) :msg "reduce the pending meat damage by 1" - :effect (req (prevent-damage state side eid :pre-damage 1))}}]}) + :effect (req (prevent-damage state side eid :damage 1))}}]}) (defcard "Net-Ready Eyes" {:on-install {:async true @@ -1948,6 +1950,7 @@ :ability {:async true :cost [(->c :trash-can)] :msg (msg "prevent up to " (max-trash state) " damage") + :req (:req (prevent-up-to-n-damage 1 :damage #{:net :core :brain})) :effect (req (let [prevented (:prevented context)] (wait-for (resolve-ability state side diff --git a/src/clj/game/cards/operations.clj b/src/clj/game/cards/operations.clj index efc8dd4e71..ffca0a9578 100644 --- a/src/clj/game/cards/operations.clj +++ b/src/clj/game/cards/operations.clj @@ -729,6 +729,7 @@ (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" diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index fa1d631fd7..5ea1bcf178 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -596,7 +596,7 @@ (and (> (:remaining context) 1) (= :net (:type context)) (not (:unpreventable context)))) - :msg (msg "prevent " (dec (:remaining context)) " " (damage-name state :pre-damage) " damage") + :msg (msg "prevent " (dec (:remaining context)) " " (damage-name state :damage) " damage") :effect (req (prevent-damage state side eid :damage (dec (:remaining context))))}}]}) (defcard "Biometric Spoofing" @@ -609,7 +609,7 @@ :req (req (and (pos? (:remaining context)) (not (:unpreventable context)))) - :msg (msg "prevent " (min 2 (:remaining context)) " " (damage-name state :pre-damage) " damage") + :msg (msg "prevent " (min 2 (:remaining context)) " " (damage-name state :damage) " damage") :effect (req (prevent-damage state side eid :damage (min 2 (:remaining context))))}}]}) (defcard "Blockade Runner" @@ -747,7 +747,7 @@ (effect-completed state side eid))))}]}) (defcard "Chrome Parlor" - {:prevention [{:prevents :pre-damage + {:prevention [{:prevents :damage :type :event :max-uses 1 :mandatory true @@ -756,8 +756,8 @@ (and (pos? (:remaining context)) (has-subtype? (:source-card context) "Cybernetic") (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (prevent-damage state side eid :pre-damage :all))}}]}) + :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") + :effect (req (prevent-damage state side eid :damage :all))}}]}) (defcard "Citadel Sanctuary" {:trash-icon true @@ -770,7 +770,7 @@ (and (pos? (:remaining context)) (= :meat (:type context)) (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") + :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") :effect (req (prevent-damage state side eid :damage :all))}}] :events [{:event :runner-turn-ends :interactive (req true) @@ -1178,6 +1178,8 @@ :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))}}]}) (defcard "District 99" @@ -1645,7 +1647,7 @@ :value true} {:type :cannot-pay-meat :value true}] - :prevention [{:prevents :pre-damage + :prevention [{:prevents :damage :type :event :max-uses 1 :mandatory true @@ -1655,8 +1657,8 @@ (or (= :meat (:type context)) (= :net (:type context))) (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (wait-for (prevent-damage state side :pre-damage :all) + :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") + :effect (req (wait-for (prevent-damage state side :damage :all) (continue-ability state side {:msg (msg (if (= target "Trash Guru Davinder") @@ -1746,7 +1748,9 @@ :type :event :ability {:async true :once :per-turn - :req (req (not-used-once? state {:once :per-turn} card)) + :req (req (and (pos? (:remaining context)) + (not (:unpreventable 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] @@ -1882,8 +1886,7 @@ :ability {:async true :cost [(->c :power 1)] :msg "prevent 1 meat damage" - :req (req (and run - (not (:unpreventable context)) + :req (req (and (not (:unpreventable context)) (= :meat (:type context)) (pos? (:remaining context)))) :effect (req (prevent-damage state side eid :damage 1))}}]}) @@ -2392,6 +2395,8 @@ :ability {:async true :cost [(->c :credit 2)] :msg "avoid 1 tag" + :req (req (and (pos? (:remaining context)) + (not (:unpreventable context)))) :effect (req (wait-for (prevent-tag state :runner 1) (continue-ability state side (prevent-another-tag) card nil)))}}] :events [{:event :agenda-stolen @@ -2439,8 +2444,7 @@ :req (req (and (first-event? state side :tag-interrupt) ;; note that the checkpoints are suppressed for both damage and tag when resolving a snare, ;; (or at least they will be after the costs merge), so this should work - ;; TODO - add a handler for 'runner prevented all damage' - (no-event? state side :all-damage-prevent #(= :net (:type (first %)))) + (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}) @@ -2566,7 +2570,7 @@ :effect (req (take-credits state side eid card :credit 1))})) (defcard "Paparazzi" - {:prevention [{:prevents :pre-damage + {:prevention [{:prevents :damage :type :event :max-uses 1 :mandatory true @@ -2575,8 +2579,8 @@ (and (pos? (:remaining context)) (= :meat (:type context)) (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") - :effect (req (prevent-damage state side eid :pre-damage :all))}}] + :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") + :effect (req (prevent-damage state side eid :damage :all))}}] :static-abilities [{:type :is-tagged :value true}]}) @@ -2933,7 +2937,7 @@ :req (req (and (pos? (:remaining context)) (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :pre-damage) " damage") + :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") :effect (req (wait-for (prevent-damage state side :damage :all) (let [cards (concat (get-in runner [:rig :hardware]) (filter #(not (has-subtype? % "Virtual")) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 0b0391b28b..c8846c7176 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -131,7 +131,9 @@ option (if (string? option) option (option state remainder))] (if (or (if (= data-type :sequential) (not (seq remainder)) - (not (pos? remainder))) + nil) + ;;(not (pos? remainder))) -> the CR says these numbers can go to (or below) 0 withoutout actually closing the interrupt, + ;;even though most abilities cannot interact with them (get-in @state [:prevent key :passed])) (do (swap! state dissoc-in [:prevent key :passed]) (effect-completed state side eid)) From 483c100c603a7ceffd5a6aa02eacde3b13c27722 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Sun, 2 Mar 2025 17:03:26 +1300 Subject: [PATCH 33/38] snare order reflects the card now --- src/clj/game/cards/assets.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index 2e0dc435a7..86fb5bae51 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -2716,9 +2716,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)} From afac03bf6b73296c2bd3b9c52dbe5e3fb0c685fe Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Sun, 2 Mar 2025 17:03:48 +1300 Subject: [PATCH 34/38] checked NoH works as intended with a unit test --- src/clj/game/cards/resources.clj | 5 +-- src/clj/game/core/prevention.clj | 6 +-- test/clj/game/cards/resources_test.clj | 54 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 5ea1bcf178..47ad6a14b7 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -2424,7 +2424,8 @@ :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 true?) + (no-event? state side :runner-prevents-all-tags) + (no-event? state side :runner-gain-tag) (not (:unpreventable context)) (pos? (:remaining context)))) :effect (req (wait-for @@ -2442,8 +2443,6 @@ :ability {:async true :msg "force the Corp to trace" :req (req (and (first-event? state side :tag-interrupt) - ;; note that the checkpoints are suppressed for both damage and tag when resolving a snare, - ;; (or at least they will be after the costs merge), so this should work (no-event? state side :all-damage-was-prevented #(= :net (:type (first %)))) (no-event? state side :damage #(= :net (:damage-type (first %)))))) :effect (req (wait-for diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index c8846c7176..fb2ccb8ca7 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -131,9 +131,9 @@ option (if (string? option) option (option state remainder))] (if (or (if (= data-type :sequential) (not (seq remainder)) - nil) - ;;(not (pos? remainder))) -> the CR says these numbers can go to (or below) 0 withoutout actually closing the interrupt, - ;;even though most abilities cannot interact with them + ;; 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 + (or (= 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)) diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 04887f53e2..18a202f6dc 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -4692,6 +4692,60 @@ (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 From c76e6da3b916e37bb52906931836fa6ad3cee391 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Sun, 2 Mar 2025 17:05:13 +1300 Subject: [PATCH 35/38] corrected logic for cleaners/brainchips --- src/clj/game/core/prevention.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index fb2ccb8ca7..30f30ec259 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -133,7 +133,7 @@ (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 - (or (= key :pre-damage) (not (pos? remainder)))) + (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)) From dcd742f7552c0c08649104700d8ab432fc89a13d Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 4 Mar 2025 12:18:48 +1300 Subject: [PATCH 36/38] simplified damage, documentation, tests pass --- src/clj/game/cards/agendas.clj | 7 +- src/clj/game/cards/assets.clj | 16 ++- src/clj/game/cards/events.clj | 15 ++- src/clj/game/cards/hardware.clj | 58 +++++------ src/clj/game/cards/operations.clj | 2 +- src/clj/game/cards/programs.clj | 11 +- src/clj/game/cards/resources.clj | 98 ++++++++---------- src/clj/game/core/prevention.clj | 165 +++++++++++++++++++++--------- 8 files changed, 206 insertions(+), 166 deletions(-) diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index 7062aecfe0..d817f40807 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -38,7 +38,7 @@ trash trash-cards]] [game.core.optional :refer [get-autoresolve set-autoresolve]] [game.core.payment :refer [can-pay? ->c]] - [game.core.prevention :refer [damage-boost prevent-jack-out]] + [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]] @@ -1208,8 +1208,7 @@ :msg "prevent the runner from jacking out for the remainder of this run" :condition :active :async true - :req (req (and (pos? (:remaining context)) - (not (:unpreventable context)))) + :req (req (preventable? context)) :effect (req (wait-for (prevent-jack-out state side) (register-lingering-effect state side card @@ -2258,7 +2257,7 @@ (= :corp (:source-player context)) (not (:unboostable context)))) :msg "increase the pending meat damage by 1" - :effect (req (damage-boost state side eid :pre-damage 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 86fb5bae51..e66ced0c53 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -44,7 +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 prevent-bad-publicity prevent-damage prevent-expose]] + [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]] @@ -448,8 +448,7 @@ :type :event :max-uses 1 :mandatory true - :ability {:req (req (and (pos? (:remaining context)) - (not (:unpreventable context)))) + :ability {:req (req (preventable? context)) :trace {:base 3 :successful {:msg "prevent all bad publicity" :async true @@ -2216,9 +2215,8 @@ :msg "prevent 1 net damage, place 1 counter on itself, and gain 3 [Credits]" :req (req (and (= :net (:type context)) (= :corp (:source-player context)) - (not (:unpreventable context)) - (pos? (:remaining context)))) - :effect (req (wait-for (prevent-damage state side :damage 1) + (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 @@ -3290,8 +3288,7 @@ :type :ability :label "1 [Credit]: Zaibatsu Loyalty" :ability {:cost [(->c :credit 1)] - :req (req (and (seq (:remaining context)) - (not (:unpreventable context)))) + :req (req (preventable? context)) :msg "prevent a card from being exposed" :async true :effect (req (prevent-expose state side eid card))}} @@ -3299,8 +3296,7 @@ :type :ability :label "[trash]: Zaibatsu Loyalty" :ability {:cost [(->c :trash-can)] - :req (req (and (seq (:remaining context)) - (not (:unpreventable context)))) + :req (req (preventable? context)) :msg "prevent a card from being exposed" :async true :effect (req (prevent-expose state side eid card))}}] diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 60a489ca9d..dde585138d 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -50,7 +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 prevent-up-to-n-tags prevent-up-to-n-damage]] + [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]] @@ -2401,9 +2401,7 @@ :ability {:async true :card card :condition :floating - :req (req - (and (pos? (:remaining context)) - (not (:unpreventable context)))) + :req (req (preventable? context)) :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") :effect (req (prevent-damage state side eid :damage :all))}}}))}}}}) @@ -2705,7 +2703,7 @@ {:prevents :damage :type :ability :prompt "Trash On the Lam to prevent up to 3 damage?" - :ability (assoc (prevent-up-to-n-damage 3 :damage #{:net :meat :core :brain}) :cost [(->c :trash-can)])}] + :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? %))} @@ -3938,11 +3936,10 @@ (and run (same-card? card (get-in @state [:runner :play-area 0])) - (pos? (:remaining context)) - (not (:unpreventable context)))) + (preventable? context))) :condition :active - :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") - :effect (req (prevent-damage state side eid :damage :all))}}] + :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))) diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 6f33af9251..a0f54a88e0 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -43,7 +43,7 @@ [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 prevent-damage prevent-encounter prevent-end-run prevent-tag prevent-up-to-n-damage]] + [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]] @@ -105,10 +105,9 @@ :cost [(->c :power 1)] :msg "prevent 1 net damage" :req (req (and run - (not (:unpreventable context)) (= :net (:type context)) - (pos? (:remaining context)))) - :effect (req (prevent-damage state side eid :damage 1))}} + (preventable? context))) + :effect (req (prevent-damage state side eid 1))}} {:prevents :encounter :type :ability :ability {:async true @@ -859,13 +858,12 @@ :cost [(->c :credit 3)] :msg "prevent 1 net damage" :req (req (and (= :net (:type context)) - (not (:unpreventable context)) - (pos? (:remaining context)))) - :effect (req (prevent-damage state side eid :damage 1))}} + (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 :damage #{:brain :core}) + :ability (assoc (prevent-up-to-n-damage 2 #{:brain :core}) :cost [(->c :trash-can)])}]}) (defcard "Flame-out" @@ -1161,10 +1159,9 @@ :label "Heartbeat" :ability {:async true :cost [(->c :trash-installed 1)] - :msg (msg "prevent 1 " (damage-name state :damage) " damage") - :req (req (and (not (:unpreventable context)) - (pos? (:remaining context)))) - :effect (req (prevent-damage state side eid :damage 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) @@ -1508,10 +1505,9 @@ :type :ability :ability {:async true :cost [(->c :trash-program-from-hand 1)] - :msg (msg "prevent 1 " (damage-name state :damage) " damage") + :msg (msg "prevent 1 " (damage-name state) " damage") :req (req (and (not (= :meat (:type context))) - (not (:unpreventable context)) - (pos? (:remaining context))))}}]})) + (preventable? context)))}}]})) (defcard "Mu Safecracker" {:implementation "Stealth credit restriction not enforced" @@ -1544,14 +1540,12 @@ :max-uses 1 :mandatory true :ability {:async true - :req (req - (and (= :meat (:type context)) - (first-event? state side :pre-damage-flag - #(= :meat (:type (first %)))) - (pos? (:remaining context)) - (not (:unpreventable context)))) + :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 :damage 1))}}]}) + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "Net-Ready Eyes" {:on-install {:async true @@ -1796,10 +1790,9 @@ :ability {:async true :cost [(->c :power 1)] :msg "prevent 1 meat damage" - :req (req (and (not (:unpreventable context)) - (= :meat (:type context)) - (pos? (:remaining context)))) - :effect (req (prevent-damage state side eid :damage 1))}}] + :req (req (and (preventable? context) + (= :meat (:type context)))) + :effect (req (prevent-damage state side eid 1))}}] :events [(trash-on-empty :power)]}) (defcard "Poison Vial" @@ -1950,12 +1943,12 @@ :ability {:async true :cost [(->c :trash-can)] :msg (msg "prevent up to " (max-trash state) " damage") - :req (:req (prevent-up-to-n-damage 1 :damage #{:net :core :brain})) + :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) :damage #{:net :core :brain}) - card nil) + (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)))))}}]})) @@ -1965,15 +1958,14 @@ :prevention [{:prevents :damage :type :ability :ability {:async true - :req (req (and (pos? (:remaining context)) - (not (:unpreventable context)) + :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) " damage") + :msg (msg "prevent " (cost-value eid :x-credits) " " (damage-type state) " damage") :async true - :effect (req (prevent-damage state side eid :damage (cost-value eid :x-credits)))} + :effect (req (prevent-damage state side eid (cost-value eid :x-credits)))} card nil))}}]}) (defcard "Record Reconstructor" diff --git a/src/clj/game/cards/operations.clj b/src/clj/game/cards/operations.clj index ffca0a9578..c1b15dd454 100644 --- a/src/clj/game/cards/operations.clj +++ b/src/clj/game/cards/operations.clj @@ -733,7 +733,7 @@ (pos? (:remaining context)) (not (:unboostable context)))) :msg "increase the pending core damage by 1" - :effect (req (damage-boost state side eid :pre-damage 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 1667d00a6e..fb0f528661 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -43,7 +43,7 @@ [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 [damage-name prevent-damage prevent-end-run prevent-up-to-n-damage prevent-trash-installed-by-type]] + [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]] @@ -1238,7 +1238,7 @@ (defcard "Deus X" {:prevention [{:prevents :damage :type :ability - :ability (assoc (prevent-up-to-n-damage :all :damage #{:net}) + :ability (assoc (prevent-up-to-n-damage :all #{:net}) :cost [(->c :trash-can)])}] :abilities [(break-sub [(->c :trash-can)] 0 "AP")]}) @@ -2372,10 +2372,9 @@ :cost [(->c :credit 1)] :msg "prevent 1 net damage" :req (req (and (= :net (:type context)) - (not (:unpreventable context)) - (first-event? state side :pre-damage-flag #(= :net (:type (first %)))) - (pos? (:remaining context)))) - :effect (req (prevent-damage state side eid :damage 1))}}]}) + (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")] diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 47ad6a14b7..dcce9fcf2a 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -57,7 +57,7 @@ [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 prevent-encounter prevent-tag prevent-trash-installed-by-type prevent-up-to-n-tags prevent-up-to-n-damage]] + [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]] @@ -595,9 +595,9 @@ :req (req (and (> (:remaining context) 1) (= :net (:type context)) - (not (:unpreventable context)))) - :msg (msg "prevent " (dec (:remaining context)) " " (damage-name state :damage) " damage") - :effect (req (prevent-damage state side eid :damage (dec (:remaining 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" {:trash-icon true @@ -606,11 +606,9 @@ :max-uses 1 :ability {:async true :cost [(->c :trash-can)] - :req (req - (and (pos? (:remaining context)) - (not (:unpreventable context)))) - :msg (msg "prevent " (min 2 (:remaining context)) " " (damage-name state :damage) " damage") - :effect (req (prevent-damage state side eid :damage (min 2 (:remaining context))))}}]}) + :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 @@ -668,11 +666,10 @@ :type :ability :ability {:async true :cost [(->c :credit 3)] - :msg (msg "prevent 1 " (damage-name state :damage) " damage") + :msg (msg "prevent 1 " (damage-name state) " damage") :req (req (and (contains? #{:net :core :brain} (:type context)) - (not (:unpreventable context)) - (pos? (:remaining context)))) - :effect (req (prevent-damage state side eid :damage 1))}}]}) + (preventable? context))) + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "Charlatan" {:abilities [{:action true @@ -752,12 +749,10 @@ :max-uses 1 :mandatory true :ability {:async true - :req (req - (and (pos? (:remaining context)) - (has-subtype? (:source-card context) "Cybernetic") - (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") - :effect (req (prevent-damage state side eid :damage :all))}}]}) + :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" {:trash-icon true @@ -766,12 +761,10 @@ :prompt "Use Citadel Sanctuary to prevent meat damage?" :ability {:async true :cost [(->c :trash-can) (->c :trash-entire-hand)] - :req (req - (and (pos? (:remaining context)) - (= :meat (:type context)) - (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") - :effect (req (prevent-damage state side eid :damage :all))}}] + :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" @@ -944,7 +937,7 @@ {:trash-icon true :prevention [{:prevents :damage :type :ability - :ability (assoc (prevent-up-to-n-damage 3 :damage #{:meat}) + :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)) @@ -1178,8 +1171,7 @@ :ability {:async true :cost [(->c :trash-can)] :msg "avoid 1 tag" - :req (req (and (pos? (:remaining context)) - (not (:unpreventable context)))) + :req (req (preventable? context)) :effect (req (prevent-tag state :runner eid 1))}}]}) (defcard "District 99" @@ -1652,13 +1644,12 @@ :max-uses 1 :mandatory true :ability {:async true - :req (req - (and (pos? (:remaining context)) - (or (= :meat (:type context)) - (= :net (:type context))) - (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") - :effect (req (wait-for (prevent-damage state side :damage :all) + :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") @@ -1748,8 +1739,7 @@ :type :event :ability {:async true :once :per-turn - :req (req (and (pos? (:remaining context)) - (not (:unpreventable context)) + :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))}}] @@ -1886,10 +1876,9 @@ :ability {:async true :cost [(->c :power 1)] :msg "prevent 1 meat damage" - :req (req (and (not (:unpreventable context)) - (= :meat (:type context)) - (pos? (:remaining context)))) - :effect (req (prevent-damage state side eid :damage 1))}}]}) + :req (req (and (= :meat (:type context)) + (preventable? context))) + :effect (req (prevent-damage state side eid 1))}}]}) (defcard "John Masanori" {:events [{:event :successful-run @@ -2395,8 +2384,7 @@ :ability {:async true :cost [(->c :credit 2)] :msg "avoid 1 tag" - :req (req (and (pos? (:remaining context)) - (not (:unpreventable context)))) + :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 @@ -2426,8 +2414,7 @@ (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) - (not (:unpreventable context)) - (pos? (:remaining context)))) + (preventable? state :damage))) :effect (req (wait-for (trash state side card {:unpreventable true :cause-card card}) (continue-ability @@ -2435,7 +2422,7 @@ {: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 :damage #{:net}) card nil))}}} + :effect (req (continue-ability state :runner (prevent-up-to-n-damage :all #{:net}) card nil))}}} card nil)))}} {:prevents :tag :type :event @@ -2443,6 +2430,7 @@ :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 @@ -2574,12 +2562,10 @@ :max-uses 1 :mandatory true :ability {:async true - :req (req - (and (pos? (:remaining context)) - (= :meat (:type context)) - (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") - :effect (req (prevent-damage state side eid :damage :all))}}] + :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}]}) @@ -2933,11 +2919,9 @@ :max-uses 1 :ability {:async true :cost [(->c :trash-can)] - :req (req - (and (pos? (:remaining context)) - (not (:unpreventable context)))) - :msg (msg "prevent " (:remaining context) " " (damage-name state :damage) " damage") - :effect (req (wait-for (prevent-damage state side :damage :all) + :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])) diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 30f30ec259..ca142f815e 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -19,12 +19,66 @@ [game.macros :refer [msg req wait-for]] [jinteki.utils :refer [other-side]])) -;; so how is this going to work? -;; each player, starting with the active player, gets a chance to prevent effects -;; we get a list of all cards that have prevention effects, and create a prompt with all the specific prevention abilities, along with: -;; * are they repeatable? -;; * the source card -;; * is it an ability, an interrupt, or a triggered event? +;; 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: @@ -165,6 +219,14 @@ (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 @@ -220,19 +282,19 @@ (defn resolve-trash-prevention [state side eid targets {:keys [unpreventable game-trash cause cause-card] :as args}] - (let [untrashable (keep identity (map #(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)) + (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)) @@ -250,23 +312,35 @@ ;; 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 key] - (get-in @state [:prevent key :type])) + [state] + (get-in @state [:prevent (damage-key state) :type])) (defn damage-pending - [state key] - (get-in @state [:prevent key :remaining])) + [state] + (get-in @state [:prevent (damage-key state) :remaining])) (defn damage-boost - [state side eid key n] - (when (pos? (damage-pending state key)) - (swap! state update-in [:prevent key :remaining] + n)) + [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 key] - (case (damage-type state key) + [state] + (case (damage-type state) :meat "meat" :brain "core" :core "core" @@ -274,29 +348,28 @@ "neat")) (defn prevent-damage - [state side eid key n] - (when (pos? (damage-pending state key)) + [state side eid n] + (when (pos? (damage-pending state)) (if (= n :all) - (swap! state update-in [:prevent key] merge {:remaining 0 :prevented :all}) - (do (swap! state update-in [:prevent key :remaining] #(max 0 (- % n))) - (swap! state update-in [:prevent key :prevented] (fnil #(+ n %) n))))) + (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 key types] - (letfn [(remainder [state] (get-in @state [:prevent key :remaining])) + [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 key) " damage prevent") - :req (req (and (pos? (get-in @state [:prevent key :remaining])) - (not (get-in @state [:prevent key :unpreventable])) + {: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 key :type]))))) + (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 key) " damage") - :effect (req (prevent-damage state side eid key target)) - :cancel-effect (req (prevent-damage state side eid key 0))})) + :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] @@ -304,8 +377,8 @@ state side eid :pre-damage {:prompt (fn [state remainder] (if (= side :runner) - (str "Prevent " (damage-pending state :pre-damage) " " (damage-name state :pre-damage) " damage?") - (str "There is " (damage-pending state :pre-damage) " pending " (damage-name state :pre-damage) " damage"))) + (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"})) @@ -318,8 +391,8 @@ state side eid :damage {:prompt (fn [state remainder] (if (= side :runner) - (str "Prevent " (damage-pending state :damage) " " (damage-name state :damage) " damage?") - (str "There is " (damage-pending state :damage) " pending " (damage-name state :damage) " damage"))) + (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"})) From 0b11ad56168ac51a6433854b905e10cccf61149b Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 4 Mar 2025 12:51:15 +1300 Subject: [PATCH 37/38] trash icon checking cleaned up --- src/clj/game/cards/assets.clj | 3 +-- src/clj/game/cards/events.clj | 3 +-- src/clj/game/cards/hardware.clj | 10 ++++------ src/clj/game/cards/programs.clj | 5 ++--- src/clj/game/cards/resources.clj | 21 +++++++-------------- src/clj/game/core/cost_fns.clj | 7 ++++--- src/clj/game/core/prevention.clj | 1 + 7 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index e66ced0c53..e059c33d80 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -3283,8 +3283,7 @@ (rez state side eid (last (:hosted (get-card state card))) {:cost-bonus -2})))}]}) (defcard "Zaibatsu Loyalty" - {:trash-icon true - :prevention [{:prevents :expose + {:prevention [{:prevents :expose :type :ability :label "1 [Credit]: Zaibatsu Loyalty" :ability {:cost [(->c :credit 1)] diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index dde585138d..67e12d8cc4 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -2695,8 +2695,7 @@ (draw state :runner eid 4)))}}) (defcard "On the Lam" - {:trash-icon true - :prevention [{:prevents :tag + {: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)])} diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index a0f54a88e0..274df6b701 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -850,8 +850,7 @@ :abilities [(break-sub [(->c :power 2)] 2 "All")]})) (defcard "Feedback Filter" - {:trash-icon true - :prevention [{:prevents :damage + {:prevention [{:prevents :damage :type :ability :label "Feedback Filter (Net)" :ability {:async true @@ -1937,8 +1936,7 @@ (defcard "Ramujan-reliant 550 BMI" (letfn [(max-trash [state] (inc (count (filter #(= (:title %) "Ramujan-reliant 550 BMI") (all-installed state :runner)))))] - {:trash-icon true - :prevention [{:prevents :damage + {:prevention [{:prevents :damage :type :ability :ability {:async true :cost [(->c :trash-can)] @@ -1954,10 +1952,10 @@ (mill state :runner eid :runner prevented-this-instance)))))}}]})) (defcard "Recon Drone" - {:trash-icon true - :prevention [{:prevents :damage + {: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 diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index fb0f528661..c24f46cb8c 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -2003,9 +2003,8 @@ (defcard "LLDS Energy Regulator" (letfn [(valid-context? [context] (and (not= :ability-cost (:cause context)) (not (:game-trash context))))] - {:trash-icon true - :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 3)] valid-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}} diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index dcce9fcf2a..33a1cf1e59 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -586,8 +586,7 @@ :effect (req (mill state :corp eid :corp 1))}]}) (defcard "Bio-Modeled Network" - {:trash-icon true - :prevention [{:prevents :damage + {:prevention [{:prevents :damage :type :ability :max-uses 1 :ability {:async true @@ -600,8 +599,7 @@ :effect (req (prevent-damage state side eid (dec (:remaining context))))}}]}) (defcard "Biometric Spoofing" - {:trash-icon true - :prevention [{:prevents :damage + {:prevention [{:prevents :damage :type :ability :max-uses 1 :ability {:async true @@ -755,8 +753,7 @@ :effect (req (prevent-damage state side eid :all))}}]}) (defcard "Citadel Sanctuary" - {:trash-icon true - :prevention [{:prevents :damage + {:prevention [{:prevents :damage :type :ability :prompt "Use Citadel Sanctuary to prevent meat damage?" :ability {:async true @@ -934,8 +931,7 @@ (make-run eid target card))}]})) (defcard "Crash Space" - {:trash-icon true - :prevention [{:prevents :damage + {:prevention [{:prevents :damage :type :ability :ability (assoc (prevent-up-to-n-damage 3 #{:meat}) :cost [(->c :trash-can)])}] @@ -1163,8 +1159,7 @@ :type :credit}}}) (defcard "Decoy" - {:trash-icon true - :prevention [{:prevents :tag + {:prevention [{:prevents :tag :type :ability :label "Decoy" :prompt "Trash Decoy to avoid 1 tag?" @@ -2913,8 +2908,7 @@ card nil))}]})) (defcard "Sacrificial Clone" - {:trash-icon true - :prevention [{:prevents :damage + {:prevention [{:prevents :damage :type :ability :max-uses 1 :ability {:async true @@ -2943,8 +2937,7 @@ (defcard "Sacrificial Construct" (letfn [(valid-context? [context] (and (not= :ability-cost (:cause context)) (not (:game-trash context))))] - {:trash-icon true - :prevention [(prevent-trash-installed-by-type "Sacrificial Construct" #{"Program" "Hardware"} [(->c :trash-can)] valid-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/core/cost_fns.clj b/src/clj/game/core/cost_fns.clj index d32623ba40..30e74d89b5 100644 --- a/src/clj/game/core/cost_fns.clj +++ b/src/clj/game/core/cost_fns.clj @@ -110,11 +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)) - (:trash-icon (card-def card)) + (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/prevention.clj b/src/clj/game/core/prevention.clj index ca142f815e..2a9a758333 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -246,6 +246,7 @@ (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 From 9b69594bf8f50f4e48dde2c5e27d6885db982f68 Mon Sep 17 00:00:00 2001 From: NB Kelly Date: Tue, 4 Mar 2025 13:03:08 +1300 Subject: [PATCH 38/38] expose simplified --- src/clj/game/cards/events.clj | 12 ++++++------ src/clj/game/cards/hardware.clj | 2 +- src/clj/game/cards/identities.clj | 6 +++--- src/clj/game/cards/programs.clj | 4 ++-- src/clj/game/cards/resources.clj | 2 +- src/clj/game/core/expose.clj | 5 +++-- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 67e12d8cc4..3a04632793 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -1062,7 +1062,7 @@ :choices {:card #(and (installed? %) (ice? %))} :async true - :effect (req (wait-for (expose state side [target] {:card card}) + :effect (req (wait-for (expose state side [target]) (continue-ability state side {:prompt "Choose a server" @@ -1210,7 +1210,7 @@ (= (last (get-zone topmost)) :content) (not (:rezzed %))))} :async true - :effect (req (wait-for (expose state side [target] {:card card}) + :effect (req (wait-for (expose state side [target]) (if-let [target (when async-result (first (:cards async-result)))] (if (or (asset? target) (upgrade? target)) @@ -1500,7 +1500,7 @@ (= (last (get-zone topmost)) :content) (not (rezzed? %))))} :async true - :effect (req (wait-for (expose state side [target] {:card card}) + :effect (req (wait-for (expose state side [target]) (continue-ability state :runner (when (and async-result ;; expose was successful @@ -1932,7 +1932,7 @@ {:choices {:card #(and (installed? %) (not (rezzed? %)))} :async true - :effect (effect (expose eid [target] {:card card}))} + :effect (effect (expose eid [target]))} {:msg "gain 2 [Credits]" :async true :effect (effect (gain-credits eid 2))}) @@ -3507,7 +3507,7 @@ :async true :change-in-game-state (req (some (complement faceup?) (all-installed state :corp))) :effect (req (if (pos? (count targets)) - (expose state side eid targets {:card card}) + (expose state side eid targets) (effect-completed state side eid)))}}) (defcard "Scavenge" @@ -3705,7 +3705,7 @@ (not (ice? %)) (corp? %))} :async true - :effect (req (wait-for (expose state side [target] {:card card}) + :effect (req (wait-for (expose state side [target]) (continue-ability state side {:prompt "Choose a server" diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index 274df6b701..a343cb8799 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -1140,7 +1140,7 @@ (not (rezzed? current-ice)))) :label "expose approached ice" :async true - :effect (req (wait-for (expose state side (make-eid state eid) [current-ice] {:card card}) + :effect (req (wait-for (expose state side (make-eid state eid) [current-ice]) (continue-ability state side (offer-jack-out) card nil)))}]}) (defcard "Grimoire" diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index b0b3e2128e..67172b0066 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -111,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)] {:card card})) + (expose state :runner eid [(:card context)])) (continue-ability state side {:optional @@ -120,7 +120,7 @@ :player :corp :no-ability {:async true - :effect (effect (expose :runner eid [(:card context)] {:card card}))} + :effect (effect (expose :runner eid [(:card context)]))} :yes-ability {:async true :effect @@ -1916,7 +1916,7 @@ (first-successful-run-on-server? state :hq))) :choices {:card #(and (installed? %) (not (rezzed? %)))} - :effect (effect (expose eid [target] {:card card}))}]}) + :effect (effect (expose eid [target]))}]}) (defcard "Skorpios Defense Systems: Persuasive Power" (let [set-resolution-mode diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index c24f46cb8c..57be735975 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -3114,7 +3114,7 @@ :yes-ability {:async true :effect (req (wait-for - (expose state side [(:ice context)] {:card card}) + (expose state side [(:ice context)]) (continue-ability state side (offer-jack-out) card nil)))}}}]}) (defcard "Snowball" @@ -3483,7 +3483,7 @@ (not (rezzed? %)))} :async true :msg (str "name " chosen-subtype) - :effect (req (wait-for (expose state side [target] {:card card}) + :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 diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index 33a1cf1e59..18af95f27d 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -2778,7 +2778,7 @@ :choices {:card installed?} :async true :cost [(->c :trash-can)] - :effect (effect (expose eid [target] {:card card}))}]}) + :effect (effect (expose eid [target]))}]}) (defcard "Reclaim" {:abilities diff --git a/src/clj/game/core/expose.clj b/src/clj/game/core/expose.clj index 53641559df..3654c02ca0 100644 --- a/src/clj/game/core/expose.clj +++ b/src/clj/game/core/expose.clj @@ -28,8 +28,9 @@ (defn expose "Exposes the given cards." ([state side eid targets] (expose state side eid targets nil)) - ([state side eid targets {:keys [unpreventable card] :as args}] - (let [targets (filterv #(not (or (rezzed? %) + ([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)]