diff --git a/src/clj/game/cards/basic.clj b/src/clj/game/cards/basic.clj index 3a03d7fa55..3cbc0ffcc2 100644 --- a/src/clj/game/cards/basic.clj +++ b/src/clj/game/cards/basic.clj @@ -61,15 +61,17 @@ (corp-can-pay-and-install? state side eid target-card server {:base-cost [(->c :click 1)] - :action :corp-click-install - :no-toast true}) + :ignore-ice-cost true + :action :corp-click-install + :no-toast true}) (some (fn [server] (corp-can-pay-and-install? state side eid target-card server {:base-cost [(->c :click 1)] - :action :corp-click-install - :no-toast true})) + :ignore-ice-cost true + :action :corp-click-install + :no-toast true})) (installable-servers state target-card)))))) :effect (req (let [{target-card :card server :server} context] (corp-install diff --git a/src/clj/game/core/diffs.clj b/src/clj/game/core/diffs.clj index 107ec00fdb..2db13ca6f4 100644 --- a/src/clj/game/core/diffs.clj +++ b/src/clj/game/core/diffs.clj @@ -256,6 +256,7 @@ :hand-size :keep :quote + :trash-like-cards :prompt-state :agenda-point :agenda-point-req]) diff --git a/src/clj/game/core/installing.clj b/src/clj/game/core/installing.clj index ff78a8f8a9..1fbc6549ae 100644 --- a/src/clj/game/core/installing.clj +++ b/src/clj/game/core/installing.clj @@ -7,6 +7,7 @@ [game.core.card :refer [agenda? asset? condition-counter? convert-to-condition-counter corp? event? get-card get-zone has-subtype? ice? installed? operation? program? resource? rezzed? upgrade?]] [game.core.card-defs :refer [card-def]] [game.core.cost-fns :refer [ignore-install-cost? install-additional-cost-bonus install-cost]] + [game.core.costs :refer [total-available-credits]] [game.core.eid :refer [complete-with-result effect-completed make-eid]] [game.core.engine :refer [checkpoint register-pending-event pay queue-event register-events trigger-event-simult unregister-events]] [game.core.effects :refer [is-disabled-reg? register-static-abilities unregister-static-abilities update-disabled-cards]] @@ -27,7 +28,7 @@ [game.core.toasts :refer [toast]] [game.core.update :refer [update!]] [game.macros :refer [continue-ability effect req wait-for]] - [game.utils :refer [dissoc-in in-coll? same-card? to-keyword quantify]] + [game.utils :refer [dissoc-in enumerate-str in-coll? same-card? to-keyword quantify]] [medley.core :refer [find-first]])) (defn install-locked? @@ -287,13 +288,14 @@ (defn corp-install-cost [state side card server - {:keys [base-cost ignore-install-cost ignore-all-cost cost-bonus cached-costs] :as args}] + {:keys [base-cost ignore-install-cost ignore-all-cost cost-bonus cached-costs ignore-ice-cost] :as args}] (or cached-costs (let [slot (get-slot state card server args) dest-zone (get-in @state (cons :corp slot)) ice-cost (if (and (ice? card) (not ignore-install-cost) (not ignore-all-cost) + (not ignore-ice-cost) (not (ignore-install-cost? state side card))) (count dest-zone) 0) @@ -316,7 +318,7 @@ (defn- corp-install-pay "Used by corp-install to pay install costs" - [state side eid card server {:keys [action] :as args}] + [state side eid card server {:keys [action resolved-optional-trash] :as args}] (let [slot (get-slot state card server args) costs (corp-install-cost state side card server (dissoc args :cached-costs)) credcost (or (value (find-first #(= :credit (:cost/type %)) costs)) 0) @@ -324,18 +326,57 @@ appldisc (if (and (not (zero? credcost)) (not (zero? discount))) (if (>= credcost discount) discount credcost) 0) args (if discount (assoc args :cost-bonus (- appldisc discount)) args) - costs (conj costs (->c :credit (- 0 appldisc)))] - ;; get a functional discount and apply it to - (if (corp-can-pay-and-install? state side eid card server (assoc args :cached-costs costs)) - (wait-for (pay state side (make-eid state (assoc eid :action action)) card costs) - (if-let [payment-str (:msg async-result)] - (if (= server "New remote") - (wait-for (trigger-event-simult state side :server-created nil card) - (make-rid state) - (corp-install-continue state side eid card server args slot payment-str)) - (corp-install-continue state side eid card server args slot payment-str)) - (effect-completed state side eid))) - (effect-completed state side eid)))) + costs (conj costs (->c :credit (- 0 appldisc))) + corp-wants-to-trash? (and (get-in @state [:corp :trash-like-cards]) + (seq (get-in @state (concat [:corp] slot))) + (not resolved-optional-trash))] + (if (and (not corp-wants-to-trash?) (corp-can-pay-and-install? state side eid card server (assoc args :cached-costs costs))) + (wait-for + (pay state side (make-eid state (assoc eid :action action)) card costs) + (if-let [payment-str (:msg async-result)] + (if (= server "New remote") + (wait-for (trigger-event-simult state side :server-created nil card) + (make-rid state) + (corp-install-continue state side eid card server args slot payment-str)) + (corp-install-continue state side eid card server args slot payment-str)) + (effect-completed state side eid))) + ;; NOTE - Diwan and Network Exchange both alter the cost of installs + ;; if it's not ice AND we can't afford it, there's nothing we can do + ;; Diwan will get accounted for, but Network Exchange wont (oh well) - nbk, 2025 + (let [shortfall (- (or (value (find-first #(= :credit (:cost/type %)) costs)) 0) (total-available-credits state side eid card)) + need-to-trash (max 0 shortfall) + cards-in-slot (count (get-in @state (concat [:corp] slot))) + possible? (and (ice? card) (>= cards-in-slot need-to-trash))] + (cond (and possible? (pos? need-to-trash)) + (letfn [(trash-all-or-none [] {:prompt (str "Trash ice protecting " (name-zone :corp slot) " (minimum " need-to-trash ")") + :choices {:req (req (= (:zone target) slot)) + :max cards-in-slot} + :waiting-prompt true + :async true + :effect (req (if (>= (count targets) need-to-trash) + (do (system-msg state side (str "trashes " (enumerate-str (map #(card-str state %) targets)))) + (wait-for + (trash-cards state side targets {:keep-server-alive true}) + (corp-install-pay state side eid card server (assoc args :resolved-optional-trash true)))) + (do (toast state :corp (str "You must either trash at least " need-to-trash " ice, or trash none of them")) + (continue-ability state side (trash-all-or-none) card targets)))) + :cancel-effect (req (effect-completed state side eid))})] + (continue-ability state side (trash-all-or-none) card nil)) + (and corp-wants-to-trash? (zero? need-to-trash)) + (continue-ability + state side + {:prompt (str "Trash any number of " (if (ice? card) "ice protecting " "cards in ") (name-zone :corp slot)) + :choices {:req (req (= (:zone target) slot)) + :max cards-in-slot} + :async true + :waiting-prompt true + :effect (req (do (system-msg state side (str "trashes " (enumerate-str (map #(card-str state %) targets)))) + (wait-for + (trash-cards state side targets {:keep-server-alive true}) + (corp-install-pay state side eid card server (assoc args :resolved-optional-trash true))))) + :cancel-effect (req (corp-install-pay state side eid card server (assoc args :resolved-optional-trash true)))} + card nil) + :else (effect-completed state side eid)))))) (defn corp-install "Installs a card in the chosen server. If server is nil, asks for server to install in. @@ -457,8 +498,7 @@ (defn runner-install-continue [state side eid card {:keys [previous-zone host-card facedown no-mu no-msg payment-str] :as args}] - (let [ - c (if host-card + (let [c (if host-card (host state side host-card card) (move state side card [:rig (if facedown :facedown (to-keyword (:type card)))])) @@ -519,17 +559,22 @@ true)))) (defn runner-install-pay - [state side eid card {:keys [no-mu facedown host-card] :as args}] + [state side eid card {:keys [no-mu facedown host-card resolved-optional-trash] :as args}] (let [costs (runner-install-cost state side (assoc card :facedown facedown) (dissoc args :cached-costs)) - available-mem (available-mu state)] + available-mem (available-mu state) + runner-wants-to-trash? (and (get-in @state [:runner :trash-like-cards]) + (not resolved-optional-trash))] (if-not (runner-can-pay-and-install? state side eid card (assoc args :cached-costs costs)) (effect-completed state side eid) (if (and (program? card) (not facedown) - (not (or no-mu (sufficient-mu? state card)))) + (or (not (or no-mu (sufficient-mu? state card))) + runner-wants-to-trash?)) (continue-ability state side - {:prompt (format "Insufficient MU to install %s. Trash installed programs?" (:title card)) + {:prompt (if (and runner-wants-to-trash? (or no-mu (sufficient-mu? state card))) + (format "Trash installed programs before installing %s?" (:title card)) + (format "Insufficient MU to install %s. Trash installed programs?" (:title card))) :choices {:max (count (filter #(and (program? %) (not (has-ancestor? % host-card))) (all-installed state :runner))) :card #(and (installed? %) ;; note: rules team says we can't create illegal gamestates by @@ -541,11 +586,13 @@ :async true :effect (req (wait-for (trash-cards state side (make-eid state eid) targets {:unpreventable true}) (update-mu state) - (runner-install-pay state side eid card args))) + (runner-install-pay state side eid card (assoc args :resolved-optional-trash true)))) :cancel-effect (req (update-mu state) - (if (= available-mem (available-mu state)) + (if (and (= available-mem (available-mu state)) + ;;(not runner-wants-to-trash?) + (not (or no-mu (sufficient-mu? state card)))) (effect-completed state side eid) - (runner-install-pay state side eid card args)))} + (runner-install-pay state side eid card (assoc args :resolved-optional-trash true))))} card nil) (let [played-card (move state side (assoc card :facedown facedown) :play-area {:suppress-event true})] (wait-for (pay state side (make-eid state eid) card costs) diff --git a/src/clj/game/core/player.clj b/src/clj/game/core/player.clj index f017cc97e7..ecc8098a86 100644 --- a/src/clj/game/core/player.clj +++ b/src/clj/game/core/player.clj @@ -20,6 +20,7 @@ set-aside set-aside-tracking servers + trash-like-cards click click-per-turn credit @@ -56,6 +57,7 @@ :credit 5 :bad-publicity (map->BadPublicity {:base 0 :additional 0}) :toast [] + :trash-like-cards nil :hand-size (map->HandSize {:base 5 :total 5}) :agenda-point 0 :agenda-point-req 7 :keep false @@ -85,6 +87,7 @@ run-credit link tag + trash-like-cards memory hand-size agenda-point @@ -118,6 +121,7 @@ :toast [] :click 0 :click-per-turn 4 :credit 5 :run-credit 0 + :trash-like-cards nil :link 0 :tag (map->Tags {:base 0 :total 0 :is-tagged false}) :memory {:base 4 diff --git a/src/clj/game/core/process_actions.clj b/src/clj/game/core/process_actions.clj index 0d21116952..f1530d0cc1 100644 --- a/src/clj/game/core/process_actions.clj +++ b/src/clj/game/core/process_actions.clj @@ -32,6 +32,13 @@ (handle-end-run state :corp nil) (fake-checkpoint state))) +(defn set-property + "set properties of the game state that need to be adjustable by the frontend + ie: * do we want an offer to trash like cards on installs?" + [state side {:keys [key value]}] + (case key + :trash-like-cards (swap! state assoc-in [side :trash-like-cards] value))) + (defn command-parser [state side {:keys [user text] :as args}] (let [author (or user (get-in @state [side :user])) @@ -74,6 +81,7 @@ "runner-ability" #'play-runner-ability "score" #(score %1 %2 (make-eid %1) (get-card %1 (:card %3)) nil) "select" #'select + "set-property" #'set-property "shuffle" #'shuffle-deck "start-turn" #'start-turn "subroutine" #'play-subroutine diff --git a/src/cljc/i18n/en.cljc b/src/cljc/i18n/en.cljc index c59e420493..e73418b674 100644 --- a/src/cljc/i18n/en.cljc +++ b/src/cljc/i18n/en.cljc @@ -745,6 +745,7 @@ :archives "Archives" :max-hand "Max hand size" :brain-damage "Core Damage" + :trash-like-cards "Offer to trash like cards" :tag-count (fn [[base additional total]] (str base (when (pos? additional) (str " + " additional)) " Tag" (if (not= total 1) "s" ""))) :agenda-count (fn [[agenda-point]] (str agenda-point " Agenda Point" (when (not= agenda-point 1) "s"))) :link-strength "Link Strength" diff --git a/src/cljs/nr/gameboard/player_stats.cljs b/src/cljs/nr/gameboard/player_stats.cljs index f7f72578fe..f0d5c0caa9 100644 --- a/src/cljs/nr/gameboard/player_stats.cljs +++ b/src/cljs/nr/gameboard/player_stats.cljs @@ -67,7 +67,7 @@ (defmethod stats-area "Runner" [runner] (let [ctrl (stat-controls-for-side :runner)] (fn [runner] - (let [{:keys [user click credit run-credit memory link tag + (let [{:keys [user click credit run-credit memory link tag trash-like-cards brain-damage active]} @runner base-credit (- credit run-credit) plus-run-credit (when (pos? run-credit) (str "+" run-credit)) @@ -94,12 +94,19 @@ (when show-tagged [:div.warning "!"])])) (ctrl :brain-damage - [:div (str brain-damage " " (tr [:game.brain-damage "Core Damage"]))])])))) + [:div (str brain-damage " " (tr [:game.brain-damage "Core Damage"]))]) + (when (= (:side @game-state) :runner) + (let [toggle-offer-trash #(send-command "set-property" {:key :trash-like-cards :delta (.. % -target -checked)})] + [:div [:label [:input {:type "checkbox" + :value true + :checked trash-like-cards + :on-click toggle-offer-trash}] + (tr [:game.trash-like-cards "Offer to trash like cards"])]]))])))) (defmethod stats-area "Corp" [corp] (let [ctrl (stat-controls-for-side :corp)] (fn [corp] - (let [{:keys [user click credit bad-publicity active]} @corp + (let [{:keys [user click credit bad-publicity active trash-like-cards]} @corp icons? (get-in @app-state [:options :player-stats-icons] true)] [:div.stats-area (if icons? @@ -110,7 +117,14 @@ (ctrl :click [:div (tr [:game.click-count] click)]) (ctrl :credit [:div (tr [:game.credit-count] credit -1)])]) (let [{:keys [base additional]} bad-publicity] - (ctrl :bad-publicity [:div (tr [:game.bad-pub-count] base additional)]))])))) + (ctrl :bad-publicity [:div (tr [:game.bad-pub-count] base additional)])) + (when (= (:side @game-state) :corp) + (let [toggle-offer-trash #(send-command "set-property" {:key :trash-like-cards :delta (.. % -target -checked)})] + [:div [:label [:input {:type "checkbox" + :value true + :checked trash-like-cards + :on-click toggle-offer-trash}] + (tr [:game.trash-like-cards "Offer to trash like cards"])]]))])))) (defn stats-view [player]