diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 17a56ab627..1d9b134a28 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,7 +1,9 @@ {:config-paths ["../resources/clj-kondo.exports/netrunner"] :linters {:unused-binding {:exclude-defmulti-args true - :exclude-destructured-as true} + :exclude-destructured-as true + :exclude-destructured-keys-in-fn-args true} + :clojure-lsp/unused-public-var {:level :off} :unresolved-symbol {:exclude [(game.test-framework/do-game [state get-corp get-runner get-run refresh hand-size prompt-map prompt-button @@ -9,7 +11,6 @@ prompt-fmt])]} :unused-namespace {:exclude ["game.core.*"]} :refer-all {:level :off} - :clojure-lsp/unused-public-var {:level :off} } :lint-as {game.test-framework/before-each clojure.core/let reagent.core/with-let clojure.core/let @@ -24,5 +25,7 @@ monger.operators/defoperator clojure.core/def monger.query/with-collection clojure.core/-> } + :hooks {:analyze-call {jinteki.i18n/simple-msg hooks.netrunner/effect + jinteki.i18n/msg-with-cost hooks.netrunner/effect}} :output {:langs true} } diff --git a/.clj-kondo/imports/potemkin/potemkin/potemkin/namespaces.clj b/.clj-kondo/imports/potemkin/potemkin/potemkin/namespaces.clj new file mode 100644 index 0000000000..a247af5cd5 --- /dev/null +++ b/.clj-kondo/imports/potemkin/potemkin/potemkin/namespaces.clj @@ -0,0 +1,56 @@ +(ns potemkin.namespaces + (:require [clj-kondo.hooks-api :as api])) + +(defn import-macro* + ([sym] + `(def ~(-> sym name symbol) ~sym)) + ([sym name] + `(def ~name ~sym))) + +(defmacro import-fn + ([sym] + (import-macro* sym)) + ([sym name] + (import-macro* sym name))) + +(defmacro import-macro + ([sym] + (import-macro* sym)) + ([sym name] + (import-macro* sym name))) + +(defmacro import-def + ([sym] + (import-macro* sym)) + ([sym name] + (import-macro* sym name))) + +#_ +(defmacro import-vars + "Imports a list of vars from other namespaces." + [& syms] + (let [unravel (fn unravel [x] + (if (sequential? x) + (->> x + rest + (mapcat unravel) + (map + #(symbol + (str (first x) + (when-let [n (namespace %)] + (str "." n))) + (name %)))) + [x])) + syms (mapcat unravel syms) + result `(do + ~@(map + (fn [sym] + (let [vr (resolve sym) + m (meta vr)] + (cond + (nil? vr) `(throw (ex-info (format "`%s` does not exist" '~sym) {})) + (:macro m) `(def ~(-> sym name symbol) ~sym) + (:arglists m) `(def ~(-> sym name symbol) ~sym) + :else `(def ~(-> sym name symbol) ~sym)))) + syms))] + result)) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..62d165cfb5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +max_line_length = off diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..26cf8e3872 --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +Noah Bogart +Noah Bogart +Noah Bogart diff --git a/dev/src/clj/web/dev.clj b/dev/src/clj/web/dev.clj index 88a5696ae3..6865add3dc 100644 --- a/dev/src/clj/web/dev.clj +++ b/dev/src/clj/web/dev.clj @@ -20,3 +20,16 @@ (nrdb/fetch-data {:db true}) (system/stop :jinteki/cards) (system/start :jinteki/cards)) + +(comment + (require 'game.cards.agendas-test + 'game.cards.assets-test + 'game.cards.basic-test + 'game.cards.events-test + 'game.cards.hardware-test + 'game.cards.identities-test + 'game.cards.operations-test + 'game.cards.programs-test + 'game.cards.resources-test + 'game.cards.upgrades-test + )) diff --git a/project.clj b/project.clj index b7e52c9aef..88d794deb8 100644 --- a/project.clj +++ b/project.clj @@ -15,6 +15,7 @@ :main web.core :repl-options {:timeout 180000 + :nrepl-middleware [org.corfield.rephrase.nrepl/wrap-rephrase] :init-ns web.dev} :dependencies [[org.clojure/clojure "1.12.5"] @@ -65,7 +66,7 @@ [time-literals "0.1.5"] [metosin/reitit "0.7.2"] [org.flatland/ordered "1.15.12"] - [io.github.noahtheduke/fluent-clj "0.0.2"]] + [io.github.noahtheduke/fluent-clj "0.3.0"]] :test-selectors {:default (fn [m] (not (:kaocha/pending m)))} @@ -77,6 +78,7 @@ [instaparse "1.5.0"] [integrant/repl "0.3.2"] [lambdaisland/kaocha "1.68.1059"] + [org.corfield/rephrase "1.0.2"] [thheller/shadow-cljs "2.28.21"]] :plugins [[lein-eftest "0.6.0"] [jonase/eastwood "1.4.3"] diff --git a/resources/public/i18n/ca.ftl b/resources/public/i18n/ca/ui.ftl similarity index 100% rename from resources/public/i18n/ca.ftl rename to resources/public/i18n/ca/ui.ftl diff --git a/resources/public/i18n/de.ftl b/resources/public/i18n/de/ui.ftl similarity index 100% rename from resources/public/i18n/de.ftl rename to resources/public/i18n/de/ui.ftl diff --git a/resources/public/i18n/en/messages.ftl b/resources/public/i18n/en/messages.ftl new file mode 100644 index 0000000000..b45c879021 --- /dev/null +++ b/resources/public/i18n/en/messages.ftl @@ -0,0 +1,852 @@ +### templates??? + +## Terms aka consistent names + +-corp = {$case -> + [genitive] Corp + [accusative] the Corp + [possessive] the Corp's + *[nominative] the Corp +} + +-runner = {$case -> + [genitive] Runner + [accusative] the Runner + *[nominative] the Runner +} + +-archives = Archives +-hq = HQ +-rd = R&D + +-grip = the grip +-heap = the heap +-stack = the stack + +-credit = [Credit] +-click = [Click] +-bad-publicity = bad publicity +-tag = tag + +-credit-pool = credit pool + +## card types + +-agenda = agenda +-agenda-plural = agendas +-asset = asset +-asset-plural = assets +-event = event +-event-plural = events +-hardware = piece of hardware +-hardware-plural = pieces of hardware +-ice = piece of ice +-ice-plural = pieces of ice +-ice-type = ICE +-identity = identity +-identity-plural = identities +-operation = operation +-operation-plural = operations +-program = program +-program-plural = programs +-resource = resource +-resource-plural = resources +-upgrade = upgrade +-upgrade-plural = upgrades + +## locations + +server-name = {$server -> + [archives] {-archives} + [hq] {-hq} + [rd] {-rd} + *[other] Server {$server} +} + +## Logical templates + +use-card = {$username} uses {$title} to {$do-ability}. +pay-use-card = {$username} {$payment} to use {$title} to {$do-ability}. +satisfy-card = {$username} {$payment} to satisfy {$title}. + +# join ability framents together naturally +# for example, "$player uses $card to do x and do y and do z" will use [join-with-and] +# like this: "$player uses $card to do x[join-with-and]do y[join-with-and]do z." + +join-with-and = {" "}and{" "} +join-list = ,{" "} + +## Ability fragments + +# generic + +do-nothing = do nothing +play-card = play {$title} +play-card-no-additional-costs = play {$title}, ignoring additional costs +select = select {$card-str} + +# trashing cards + +trash-self = trash itself +trash-card = trash {$card-str} +trash-card-at-no-cost = trash {$card-str} at no cost +trash-n-cards = trash {$count -> + [zero] no cards + [one] 1 card + *[other] {$count} cards +} +trash-cards = trash {$count -> + [zero] no cards + [one] 1 card ({$card-strs}) + *[other] {$count} cards ({$card-strs}) +} +trash-accessed-card = trash the accessed card ({$title}) +trash-all-cards-in-grip = trash all cards in {-grip} +trash-all-agendas-by-type = trash all {-agenda-plural} +trash-all-assets-by-type = trash all {-asset-plural} +trash-all-events-by-type = trash all {-event-plural} +trash-all-hardware-by-type = trash all {-hardware-plural} +trash-all-ice-by-type = trash all {-ice-type} +trash-all-operations-by-type = trash all {-operation-plural} +trash-all-resource-by-type = trash all {-resource-plural} +trash-all-upgrade-by-type = trash all {-upgrade-plural} + +trash-all-cards-in-server-at-no-cost = trash all cards in {$server} at no cost + +# credits + +gain-credits = gain {$count} {-credit} +corp-gains-credits = make {-corp(case: "accusative")} gain {$count} {-credit} +runner-gains-credits = make {-runner(case: "accusative")} gain {$count} {-credit} + +# drawing cards + +draw-cards = draw {$count -> + [one] 1 card + *[other] {$count} cards +} + +# clicks + +gain-clicks = gain {$count -> + [one] [Click] + *[other] {$count} [Click] +} + +lose-clicks = lose {$count -> + [one] [Click] + *[other] {$count} [Click] +} + +# tags + +avoid-tags = avoid {$count -> + [one] 1 tag + *[other] {$count} tags +} +take-tags = take {$count -> + [one] 1 tag + *[other] {$count} tags +} +remove-tags = remove {$count -> + [one] 1 tag + *[other] {$count} tags +} +remove-all-tags = remove all tags ({$count}) + +# runner shuffling + +shuffle-grip-into-stack = shuffle { -grip } into { -stack } +shuffle-grip-and-heap-into-stack = shuffle { -grip } and { -heap } into { -stack } +shuffle-self-into-stack = shuffle itself into {-stack} +shuffle-cards-into-stack = shuffle {$count -> + [zero] 0 cards into {-stack} + [one] {$titles} into {-stack} + *[other] {$count} cards ({$titles}) into {-stack} +} +shuffle-stack = shuffle {-stack} + +# corp shuffling + +shuffle-cards-in-server-into-rd = shuffle all cards in {$server} into {-rd} +shuffle-cards-into-rd = shuffle {$count -> + [zero] 0 cards into {-rd} + [one] {$titles} into {-rd} + *[other] {$count} cards ({$titles}) into {-rd} +} + +# score area stuff + +forfeit = forfeits {$title} +add-self-to-score-area = add itself to [their] score area as an agenda worth {$value -> + [one] 1 agenda point + *[other] {$value} agenda points +} + +give-bad-publicity = give {-corp(case: "accusative")} {$count} { -bad-publicity } + +# moving cards + +add-self-to-grip = add itself to {-grip} +add-card-to-grip = add {$title} to {-grip} +add-card-to-hq = add {$card-str} to {-hq} + +add-card-from-stack-to-grip = add {$card-str} from {-stack} to {-grip} +add-card-to-top-of-stack = add {$card-str} to the top of {-stack} +add-card-to-bottom-of-stack = add {$card-str} to the bottom of {-stack} + +add-card-to-top-of-rd = add {$title} to the top of {-rd} +add-card-to-bottom-of-rd = add {$title} to the bottom of {-rd} + +add-cards-from-heap-to-grip = add {$titles} from {-heap} to {-grip} + +move-seen-unseen-into-grip = move {$seen} and {$unseen-cnt -> + [one] 1 unseen card into {-grip} + *[other] {$unseen-cnt} unseen cards into {-grip} +} +move-seen-into-grip = move {$seen} into {-grip} +move-unseen-into-grip = move {$unseen-cnt -> + [one] 1 unseen card into {-grip} + *[other] {$unseen-cnt} unseen cards into {-grip} +} + +move-seen-unseen-into-hq = move {$seen} and {$unseen-cnt -> + [one] 1 unseen card into {-hq} + *[other] {$unseen-cnt} unseen cards into {-hq} +} +move-seen-into-hq = move {$seen} into {-hq} +move-unseen-into-hq = move {$unseen-cnt -> + [one] 1 unseen card into {-hq} + *[other] {$unseen-cnt} unseen cards into {-hq} +} + +# remove from the game (rfg) + +rfg-card = remove {$title} from the game + +# reveal + +expose-card = expose {$title} +reveal-n-cards-in-hq = reveal {$count -> + [one] 1 card from {-hq} + *[other] {$count} cards from {-hq} +} +reveal-cards-in-hq = reveal {$count -> + [one] {$titles} from {-hq} + *[other] {$count} cards ({$titles}) from {-hq} +} +reveal-cards-in-grip = reveal {$count -> + [one] {$titles} from {-grip} + *[other] {$count} cards ({$titles}) from {-grip} +} +reveal-top-of-stack = reveal {$title} from the top of {-stack} + +disable-corp-id = disable {-corp(case:"nominative")} identity +disable-runner-id = disable {-runner(case:"nominative")} identity + +# turns + +take-additional-turn = take an additional turn after this one +reduce-corp-max-hand-size-bad-publicity = reduce {-corp(case:"possessive")} maximum hand size by 1 for each {-bad-publicity} +reduce-corp-click-next-turn = give {-corp(case:"nominative")} {$count} fewer {-click} to spend on [corp-pronoun] next turn + +# rearrange stuff + +rearrange-installed-ice = rearrange any number of ice protecting all servers +rearrange-top-n-cards-rd = rearrange the top {$count -> + [one] 1 card of {-rd} + *[other] {$count} cards of {-rd} +} +trash-or-rearrange-top-of-stack = look at and trash or rearrange the top {$count -> + [one] 1 card of {-stack} + *[other] {$count} cards of {-stack} +} + +swap-two-ice-positions = swap the positions of {$card-str} and {$card-str2} + +# advancement counters + +place-n-advancement-counters = place {$count -> + [zero] no advancement counters on {$card-str} + [one] 1 advancement counter on {$card-str} + *[other] {$count} advancement counters on {$card-str} +} + +remove-advancement-counters = remove {$count -> + [zero] no advancement counters from {$card-str} + [one] 1 advancement counter from {$card-str} + *[other] {$count} advancement counters from {$card-str} +} + +place-virus-counters = place {$count -> + [one] 1 virus counter on {$title} + *[other] {$count} virus counters on {$title} +} + +charge-card = trash {$card-str} {$count -> + [one] 1 time + *[other] {$count} times +} + +place-credits-on-self = place {$credits} {-credit} on itself +place-credits-on-self-for-trash-costs = place {$credits} {-credit} for paying trash costs + +look-at-top-cards-add-to-grip = look at the top {$top-count -> + [one] card of the stack and add {$add-count} of them to the grip + *[other] {$top-count} cards of the stack and add {$add-count} of them to the grip +} + +guess = guess {$choice} + +reveal-copies-of-self = reveal {$count -> + [one] 1 copy of itself + *[other] {$count} copies of itself +} + +# forcing + +force-take-bad-publicity = force {-corp(case: "nominative")} to take {$count} {-bad-publicity} +force-trash-installed-ice = force {-corp(case: "nominative")} to trash a {-ice} protecting {$server} +force-corp-trash-top-of-rd = force the Corp to trash the top {$count -> + [one] card of R&D + *[other] {$count} cards of R&D +} +force-corp-trash-additional-top-of-rd = force the Corp to trash an additional {$count -> + [one] card from the top of R&D + *[other] {$count} cards from the top of R&D +} +force-corp-rez = force the Corp to rez {$title} +force-corp-trash = force the Corp to trash {$title} +force-add-all-hq-cards-to-top-of-rd = force {-corp(case: "nominative")} to add all cards in {-hq} to the top of {-rd} +force-corp-pay-credits = force {-corp(case: "nominative")} to pay {$credits} {-credit} +force-corp-lose-credits = force {-corp(case: "nominative")} to lose {$credits} {-credit} +force-corp-draw-cards = force {-corp(case: "nominative")} to draw {$count -> + [one] 1 card + *[other] {$count} cards +} +force-corp-discard-from-hq = force {-corp(case: "nominative")} to discard {$count -> + [one] 1 card from {-hq} + *[other] {$count} cards from {-hq} +} +force-corp-random-discard-from-hq = force {-corp(case: "nominative")} to discard {$count -> + [one] 1 card from {-hq} at random + *[other] {$count} cards from {-hq} at random +} + +force-runner-draw-cards = force {-runner(case: "nominative")} to draw {$count -> + [one] 1 card + *[other] {$count} cards +} + +force-runner-gain-credits = force {-runner(case: "nominative")} to gain {$credits} {-credit} + +each-player-draws-cards = make each player draw {$count -> + [one] 1 card + *[other] {$count} cards +} + +# all installs + +# runner installs + +runner-install-card = install {$title} + +install-with-discount = install {$title}, lowering the cost by {$discount} {-credit} +install-from-grip = install {$title} from {-grip} +install-from-grip-with-discount = install {$title} from {-grip}, lowering the cost by {$discount} + +install-from-stack = install {$title} from {-stack} +install-from-stack-with-discount = install {$title} from {-stack}, lowering the cost by {$discount} +install-program-from-heap = install a {-program} from {-heap} +install-program-from-stack = install a {-program} from {-stack} + +# hosting + +host-self-as-condition-counter = host itself on {$card-str} as a hosted condition counter +host-card-on-card = host {$title} on {$card-str} + +# rezzing + +rez-card = rez {$card-str} +derez-card = derez {$card-str} +derez-cards = derez {$card-strs} + +# make a run + +make-a-run = make a run +make-a-run-on = make a run on {$server} +make-a-run-on-preventing-all-damage = make a run on {$server}, preventing all damage +run-on-with-no-rezzed-ice = make a run on {$server} during which no ice can be rezzed +rfg-to-make-a-run-on = remove {$title} from the game to make a run on {$server} + +# redirect run + +redirect-run-to-archives = change the attacked server to {-archives} +redirect-run-to-hq = change the attacked server to {-hq} +redirect-run-to-rd = change the attacked server to {-rd} + +# icebreaker strength + +give-strength-to-icebreaker-during-run = give +{$bonus} strength to {$title} during the run +give-strength-to-icebreaker-remainder-of-run = give +{$bonus} strength to {$card-str} during the run +give-strength-all-icebreakers-during-run = give +{$bonus} strength to all icebreakers during the run + +# ice + +ice-gains-barrier-code-gate-sentry-end-of-turn = {$card-str} gains barrier, code gate, and sentry until end of turn + +bypass-ice = bypass {$card-str} + +prevent-run-ending = prevent the run from ending +prevent-ice-rezzed-during-run = prevent {-ice-type} from being rezzed during the run +prevent-corp-rez-card-during-turn = prevent {-corp(case:"nominative")} from rezzing {$card-str} for the rest of the turn +prevent-corp-rez-non-ice-on-runner-turn = prevent {-corp(case:"nominative")} from rezzing non-ice cards during {-runner(case:"possessive")} turn + +increase-rez-cost-first-unrezzed-approached-ice = increase the rez cost of the first unrezzed {-ice} approached by {$credits} {-credit} + +# prevention + +prevent-core-damage = suffer {$count} core damage +prevent-meat-damage = prevent {$count} meat damage +prevent-net-damage = suffer {$count} net damage + +prevent-corp-advancing-cards-next-turn = prevent {-corp(case: "nominative")} from advancing cards during [their] next turn + +# Damage + +suffer-meat-damage = suffer {$value} meat damage +suffer-net-damage = suffer {$value} net damage +suffer-brain-damage = suffer {$value} core damage +suffer-core-damage = suffer {$value} core damage + +prevent-damage-until-next-turn = prevent all damage until your next turn + +# Access + +access-another-card = access another card +access-card = access {$card-str} +access-additional-in-hq = access {$count -> + [one] 1 additional card in {-hq} + *[other] {$count} additional cards in {-hq} +} +access-additional-in-rd = access {$count -> + [one] 1 additional card in {-rd} + *[other] {$count} additional cards in {-rd} +} +access-from-bottom-of-rd = access cards from the bottom of {-rd} + +# searching + +search-stack-for-connection-resource = search {-stack} for a connection resource +search-stack-for-run-event = search {-stack} for a run event +search-stack-for-virtual-resource = search {-stack} for a virtual resource + +## Specific card abilities + +# Apocalypse +trash-all-installed-corp = trash all installed { -corp(case: "genitive") } cards + +# Apocalypse +turn-all-installed-runner-facedown = turn all installed { -runner(case: "genitive") } cards facedown + +# Rebirth + +change-identity = change identity to {$title} + +## Payments + +payment-click = spends {$value} {-click} +payment-credit = pays {$value} {-credit} +payment-x-credit = pays {$value} {-credit} +payment-credit-pool = pays {$value} {-credit} from [their] {-credit-pool} +payment-hosted-credit = pays {$value} {-credit} from {$title} +payment-bad-publicity = pays {$value} {-credit} from {-bad-publicity} + +payment-extend = trashes {$title} from {-hq} +payment-trash-can = [trash] +payment-trash-self = trashes {$title} +payment-forfeit = forfeits {$count -> + [zero] no agendas + [one] 1 agenda ({$titles}) + *[other] {$count} agendas ({$titles}) +} +payment-forfeit-self = forfeits {$title} + +payment-gain-tags = takes {$count -> + [zero] no tags + [one] 1 tag + *[other] {$count} tags +} +payment-tag = removes {$count -> + [zero] no tags + [one] 1 tag + *[other] {$count} tags +} +payment-gain-bad-publicity = gains {$count} bad publicity +payment-return-to-grip = returns {$title} to {-grip} +payment-return-to-hq = returns {$title} to {-hq} + +payment-remove-from-game = removes {$title} from the game +payment-rfg-program = removes {$count -> + [zero] no installed programs from the game + [one] 1 installed program from the game ({$titles}) + *[other] {$count} installed programs from the game ({$titles}) +} +payment-trash-installed = trashes {$count -> + [zero] no installed cards + [one] 1 installed card ({$titles}) + *[other] {$count} installed cards ({$titles}) +} +payment-trash-hardware = trashes {$count -> + [zero] no installed pieces of hardwares + [one] 1 installed piece of hardware ({$titles}) + *[other] {$count} installed pieces of hardware ({$titles}) +} +payment-trash-program = trashes {$count -> + [zero] no installed programs + [one] 1 installed program ({$titles}) + *[other] {$count} installed programs ({$titles}) +} +payment-trash-resource = trashes {$count -> + [zero] no installed resources + [one] 1 installed resource ({$titles}) + *[other] {$count} installed resources ({$titles}) +} +payment-trash-connection = trashes {$count -> + [zero] no installed connection resources + [one] 1 installed connection resource ({$titles}) + *[other] {$count} installed connection resources ({$titles}) +} +payment-trash-ice = trashes {$count -> + [zero] no installed rezzed {-ice-type} + [one] 1 installed rezzed {-ice-type} ({$titles}) + *[other] {$count} installed rezzed {-ice-type} ({$titles}) +} +payment-trash-bioroid = trashes {$count -> + [zero] no installed rezzed Bioroids + [one] 1 installed rezzed Bioroid ({$titles}) + *[other] {$count} installed rezzed Bioroids ({$titles}) +} + +payment-trash-from-stack = trashes {$count -> + [zero] no cards from the top of {-stack} + [one] 1 card from the top of {-stack} + *[other] {$count} cards from the top of {-stack} +} +payment-trash-from-rd = trashes {$count -> + [zero] no cards from the top of {-rd} + [one] 1 card from the top of {-rd} + *[other] {$count} cards from the top of {-rd} +} +payment-trash-from-grip = trashes {$count -> + [zero] no cards from {-grip} + [one] 1 card ({$titles}) from {-grip} + *[other] {$count} cards ({$titles}) from {-grip} +} +payment-trash-from-hq = trashes {$count -> + [zero] no cards from {-hq} + [one] 1 card from {-hq} + *[other] {$count} cards from {-hq} +} +payment-reveal-trash-from-grip = reveals and trashes {$count -> + [zero] no cards from {-grip} + [one] 1 card ({$titles}) from {-grip} + *[other] {$count} cards ({$titles}) from {-grip} +} +payment-reveal-trash-from-hq = reveals and trashes {$count -> + [zero] no cards from {-hq} + [one] 1 card ({$titles}) from {-hq} + *[other] {$count} cards ({$titles}) from {-hq} +} +payment-randomly-trash-from-grip = randomly trashes {$count -> + [zero] no cards from {-grip} + [one] 1 card ({$titles}) from {-grip} + *[other] {$count} cards ({$titles}) from {-grip} +} +payment-randomly-trash-from-hq = randomly trashes {$count -> + [zero] no cards from {-hq} + [one] 1 card from {-hq} + *[other] {$count} cards from {-hq} +} +payment-randomly-reveal-trash-from-grip = reveals and trashes {$count -> + [zero] no cards from {-grip} + [one] 1 random card ({$titles}) from {-grip} + *[other] {$count} random cards ({$titles}) from {-grip} +} +payment-randomly-reveal-trash-from-hq = reveals and trashes {$count -> + [zero] no cards from {-hq} + [one] 1 random card ({$titles}) from {-hq} + *[other] {$count} random cards ({$titles}) from {-hq} +} +payment-trash-all-cards-in-hq = trashes all cards ({$count} total) in {-hq} +payment-trash-all-cards-in-grip = trashes all cards ({$count} total) in {-grip} ({$titles}) + +payment-trash-hardware-in-grip = trashes {$count -> + [one] 1 piece of hardware from {-grip} ({$titles}) + *[other] {$count} pieces of hardware from {-grip} ({$titles}) +} +payment-trash-program-in-grip = trashes {$count -> + [one] 1 programs from {-grip} ({$titles}) + *[other] {$count} programs from {-grip} ({$titles}) +} +payment-trash-resource-in-grip = trashes {$count -> + [one] 1 resource from {-grip} ({$titles}) + *[other] {$count} resources from {-grip} ({$titles}) +} + +payment-meat = suffer {$value} meat damage +payment-net = suffer {$value} net damage +payment-core = suffer {$value} core damage + +payment-shuffle-installed-into-stack = shuffles {$count -> + [one] 1 card ({$titles}) into {-stack} + *[other] {$count} cards ({$titles}) into {-stack} +} +payment-shuffle-installed-into-rd = shuffles {$count -> + [one] 1 card into {-rd} + *[other] {$count} cards into {-rd} +} +payment-add-installed-bottom-stack = adds {$count -> + [one] 1 installed card ({$titles}) to the bottom of {-stack} + *[other] {$count} installed cards ({$titles}) to the bottom of {-stack} +} +payment-add-installed-bottom-rd = adds {$count -> + [one] 1 installed card ({$titles}) to the bottom of {-rd} + *[other] {$count} installed cards ({$titles}) to the bottom of {-rd} +} + +payment-turn-hosted-matryoshka-facedown = turns {$count -> + [one] 1 hosted copy of Matryoshka facedown + *[other] {$count} hosted copy of Matryoshka facedown +} + +payment-add-random-from-hand-to-bottom-of-stack = adds {$count -> + [one] 1 random card to the bottom of {-stack} + *[other] {$count} random cards to the bottom of {-stack} +} +payment-add-random-from-hand-to-bottom-of-rd = adds {$count -> + [one] 1 random card to the bottom of {-rd} + *[other] {$count} random cards to the bottom of {-rd} +} + +payment-hosted-to-hq = adds {$count -> + [one] 1 hosted card to {-hq} ({$titles}) + *[other] {$count} hosted cards to {-hq} ({$titles}) +} + +payment-advancement-counter = spends {$count -> + [one] 1 hosted advancement counter from {$title} + *[one] {$count} hosted advancement counters from {$title} +} +payment-agenda-counter = spends {$count -> + [one] 1 hosted agenda counter from {$title} + *[one] {$count} hosted agenda counters from {$title} +} +payment-power-counter = spends {$count -> + [one] 1 hosted power counter from {$title} + *[one] {$count} hosted power counters from {$title} +} +payment-virus-counter = spends {$count -> + [one] 1 hosted virus counter from {$title} + *[one] {$count} hosted virus counters from {$title} +} + +payment-derez-harmonic = derezzes {$count} Harmonic {-ice-type} ({$titles}) + +## game.core.to-string/card-str + +# it's one thing to say "uses Corroder", it's another to say "this specific Ice +# Wall on the table". + +# "Corroder" +card-str-runner-seen = {$title} + +# "installed Corroder" +card-str-runner-installed-seen = an installed {$title} + +# "a card in the grip" +card-str-runner-grip = a card in {-grip} + +# "Corroder in the heap" +card-str-runner-discard = {$title} in {-heap} + +# "a facedown card" +card-str-runner-unknown = a facedown card + +# "hosted Corroder" +card-str-runner-hosted-seen = hosted {$title} + +# "hosted facedown card" +card-str-runner-hosted-unknown = hosted facedown card + +# "Merger in the Runner's score area" +card-str-runner-scored = {$title} in {-runner(case: "possessive")} score area + +# "Merger in the Corp's score area" +card-str-corp-scored = {$title} in {-corp(case: "possessive")} score area +card-str-corp-rfg = {$title} removed from the game +card-str-corp-play-area = {$title} in {-corp(case: "possessive")} play area +card-str-corp-destroyed = destroyed {$title} + +# "hosted Ice Wall" +card-str-corp-hosted-seen = hosted {$title} +card-str-corp-hosted-known = hosted facedown {$title} +card-str-corp-hosted-unknown = hosted card{$server -> + [archives] {" "}in root of {-archives} + [hq] {" "}in root of {-hq} + [rd] {" "}in root of {-rd} + *[other] {" "}in Server {$server-n} +} + +# "NGO Front in Server 5" +card-str-corp-installed-remote-seen = {$title} in Server {$server-n} + +# "facedown Obokata Protocol in Server 2" +card-str-corp-installed-remote-known = facedown {$title} in Server {$server-n} + +# "a card in Server 1" +card-str-corp-installed-remote-unknown = a card in Server {$server-n} + +# "Prisec in root of HQ" +card-str-corp-installed-central-seen = {$server -> + [archives] {$title} in root of {-archives} + [hq] {$title} in root of {-hq} + [rd] {$title} in root of {-rd} + *[other] unknown central ({$server}) +} + +# "facedown Ryon Knight in root of R&D" +card-str-corp-installed-central-known = facedown {$title} in root of {$server -> + [archives] {-archives} + [hq] {-hq} + [rd] {-rd} + *[other] unknown central ({$server}) +} + +# "a card in root of Archives" +card-str-corp-installed-central-unknown = a card in root of {$server -> + [archives] {-archives} + [hq] {-hq} + [rd] {-rd} + *[other] unknown central ({$server}) +} + +# "Ice Wall protecting HQ at position 1" +card-str-corp-installed-ice-seen = {$title} protecting {$server -> + [archives] {-archives} at position {$position} + [hq] {-hq} at position {$position} + [rd] {-rd} at position {$position} + *[other] Server {$server-n} at position {$position} +} + +# "facedown ICE protecting HQ at position 1" +card-str-corp-installed-ice-known = facedown {$title} protecting {$server -> + [archives] {-archives} at position {$position} + [hq] {-hq} at position {$position} + [rd] {-rd} at position {$position} + *[other] Server {$server-n} at position {$position} +} + +# "ICE protecting HQ at position 1" +card-str-corp-installed-ice-unknown = {-ice-type} protecting {$server -> + [archives] {-archives} at position {$position} + [hq] {-hq} at position {$position} + [rd] {-rd} at position {$position} + *[other] Server {$server-n} at position {$position} +} + +### Standalone Messages + +## Game End + +tie = The game has ended in a tie. +win = {$username} wins the game. +concede = {$username} concedes. +win-decked = {$username} is decked. +win-flatline = {$username} is flatlined. +clear-win = {$username} cleared the win condition. + +## Mulligan + +mulligan-take = {$username} takes a mulligan. +mulligan-keep = {$username} keeps [their] hand. + +## Moving + +msg-draw-cards = {$username} draws {$count -> + [zero] no cards. + [one] 1 card. + *[other] {$count} cards. +} + +msg-forfeit-agenda = {$username} forfeits {$title}. + +msg-trash-card = {$username} trashes {$card-str}. + +msg-trash-cards = {$username} trashes {$count -> + [zero] no cards. + [one] 1 card ({$titles}). + *[other] {$count} cards ({$titles}). +} + +msg-derez-card = {$username} derezzes {$card-str}. + +msg-rfg-n-cards-from-stack = {$username} removes {$count -> + [one] the top 1 card of {-stack} from the game. + *[other] the top {$count} cards of {-stack} from the game. +} + +## Traces + +increase-trace-strength = {$username} {$payment} to increase trace strength to {$value}. + +## Turn messages + +corp-start-of-turn = {$username} started [their] turn {$turn} with {$credits} {-credit} and {$cards -> + [one] 1 card in { -hq }. + *[other] {$cards} cards in { -hq }. +} +corp-end-of-turn = {$username} is ending [their] turn {$turn} with {$credits} {-credit} and {$cards -> + [one] 1 card in { -hq }. + *[other] {$cards} cards in { -hq }. +} + +runner-start-of-turn = {$username} started [their] turn {$turn} with {$credits} {-credit} and {$cards -> + [one] 1 card in { -grip }. + *[other] {$cards} cards in { -grip }. +} +runner-end-of-turn = {$username} is ending [their] turn {$turn} with {$credits} {-credit} and {$cards -> + [one] 1 card in { -grip }. + *[other] {$cards} cards in { -grip }. +} + +mandatory-start-of-turn-draw = {$username} makes [their] mandatory start of turn draw. + +no-further-actions = {$username} has no further actions. + +skip-discard-step = {$username} skips [their] discard step this turn. + +corp-discard-cards-from-hand-eot = {$username} discards {$cards -> + [one] 1 card from { -hq } at end of turn. + *[other] {$cards} from { -hq } at end of turn. +} +runner-discard-cards-from-hand-eot = {$username} discards {$cards} from {-stack} at end of turn. + +extra-turns-remaining = {$username} will have {$turns -> + [one] 1 extra turn remaining. + *[other] {$turns} extra turns remaning. +} + +## Waiting Prompts + +waiting-corp-default = Waiting for {-corp} to make a decision. +waiting-runner-default = Waiting for {-runner} to make a decision. +waiting-trash-prevention-triggers = Waiting for your opponent to resolve trash prevention triggers. +waiting-pre-damage-triggers = Waiting for your opponent to resolve pre-damage triggers. +waiting-damage-triggers = Waiting for your opponent to resolve damage triggers. +waiting-prevent-when-encountered = Waiting for your opponent to prevent a "when encountered" ability. +waiting-prevent-run-ending = Waiting for your opponent to prevent the run from ending. +waiting-prevent-jacking-out = Waiting for your opponent to prevent you from jacking out. +waiting-prevent-expose = Waiting for your opponent to prevent an expose. +waiting-prevent-bad-publicity = Waiting for your opponent to prevent {-bad-publicity}. +waiting-prevent-tags = Waiting for your opponent to prevent tags. diff --git a/resources/public/i18n/en.ftl b/resources/public/i18n/en/ui.ftl similarity index 99% rename from resources/public/i18n/en.ftl rename to resources/public/i18n/en/ui.ftl index b2f9b9e517..93288641d3 100644 --- a/resources/public/i18n/en.ftl +++ b/resources/public/i18n/en/ui.ftl @@ -1048,6 +1048,8 @@ lobby_players = Players lobby_private = PRIVATE +lobby_quick-draft = Quickly draft a deck to play against your opponent, using a smaller deck size and lower than normal agenda-point total. + lobby_rejoin = Rejoin lobby_reload = Reload list @@ -1915,6 +1917,8 @@ stats_tags-gained = Tags Gained stats_unavailable = Replay unavailable +stats_unshare = Unshare replay + stats_unique-accesses = Unique Cards Accessed stats_view-games = Return to stats screen diff --git a/resources/public/i18n/es.ftl b/resources/public/i18n/es/ui.ftl similarity index 100% rename from resources/public/i18n/es.ftl rename to resources/public/i18n/es/ui.ftl diff --git a/resources/public/i18n/fr/messages.ftl b/resources/public/i18n/fr/messages.ftl new file mode 100644 index 0000000000..161a1466de --- /dev/null +++ b/resources/public/i18n/fr/messages.ftl @@ -0,0 +1,80 @@ +### templates??? + +## Terms aka consistent names + +-corp = {$case -> + [nominative] Corpo + *[adjective] la Corpo +} +-runner = {$case -> + [nominative] Runner + *[adjective] lae Runner +} + +-archives = Archives +-hq = QG +-rd = R&D + +-grip = la main +-heap = le tas +-stack = la pile + +## Logical templates + +use-card = {$username} utilise {$title} pour {$do-ability}. +pay-use-card = {$username} {$payment} pour utiliser {$title} pour {$do-ability}. + +# join ability framents together naturally +# for example, "$player uses $card to do x and do y and do z" will use [join-with-and] +# like this: "$player uses $card to do x[join-with-and]do y[join-with-and]do z." + +join-with-and = {" "}et{" "} + +## Ability fragments + +trash-card = efface {$title} + +trash-cards = efface {$count -> + [one] 1 carte + *[other] {$count} cartes +} + +gain-credits = gagner {$count} [Credit] + +draw-cards = piocher {$count -> + [one] 1 carte + *[other] {$count} cartes +} + +avoid-tags = éviter {$count -> + [one] 1 tag + *[other] {$count} tags +} + +take-tags = prend {$count -> + [one] 1 tag + *[other] {$count} tags +} + +shuffle-grip-into-stack = mélange { -grip } dans { -stack } +shuffle-grip-and-heap-into-stack = mélange { -grip } et { -heap } dans { -stack } + +## Specific card abilities + +# Apocalypse +trash-all-installed-corp = effacer toutes les cartes { -corp(case: "nominative") } installées + +# Apocalypse +turn-all-installed-runner-facedown = retourner toutes les cartes { -runner(case: "nominative") } installées face cachée + +## Standalone messages + +increase-trace-strength = {$username} {$payment} pour augmenter la force de la trace à {$value}. + +## Payments + +payment-click = dépense {$value} [Click] +payment-credit = paie {$value} [Credit] +payment-credit-pool = paie {$value} [Credit] depuis sa réserve de crédits. +payment-hosted-credit = paie {$value} [Credit] depuis {$title} +payment-bad-publicity = paie {$value} [Credit] de Mauvaise Presse diff --git a/resources/public/i18n/fr.ftl b/resources/public/i18n/fr/ui.ftl similarity index 99% rename from resources/public/i18n/fr.ftl rename to resources/public/i18n/fr/ui.ftl index 327ea9f77d..52b47201b0 100644 --- a/resources/public/i18n/fr.ftl +++ b/resources/public/i18n/fr/ui.ftl @@ -318,6 +318,8 @@ game_game-start = Démarrer la partie: {$timestamp} game_grip = Main +game_grip-count = Main ({$cnt}) + game_heap = Tas ({$cnt}) game_hq = QG @@ -369,7 +371,7 @@ game_rez = Activer game_rfg = Retiré du jeu -game_rnd = R&D +game_rnd = R&D ({$cnt}) game_run = Lancer un piratage @@ -394,6 +396,8 @@ game_spec-view = Vue Spectateur game_stack = Pile +game_stack-count = Pile ({$cnt}) + game_start = Démarrer la partie game_start-turn = Commencer le tour diff --git a/resources/public/i18n/it.ftl b/resources/public/i18n/it/ui.ftl similarity index 100% rename from resources/public/i18n/it.ftl rename to resources/public/i18n/it/ui.ftl diff --git a/resources/public/i18n/ja/messages.ftl b/resources/public/i18n/ja/messages.ftl new file mode 100644 index 0000000000..40289ed5d6 --- /dev/null +++ b/resources/public/i18n/ja/messages.ftl @@ -0,0 +1,52 @@ +## Logical templates + +use-card = {$username} uses {$title} to {$do-ability}. +pay-use-card = {$username} {$payment} to use {$title} to {$do-ability}. + +# join ability framents together naturally +# for example, "$player uses $card to do x and do y and do z" will use [join-with-and] +# like this: "$player uses $card to do x[join-with-and]do y[join-with-and]do z." + +join-with-and = {" "}and{" "} +join-list = ,{" "} + +## locations + +server-name = {$server -> + [archives] {-archives} + [hq] {-hq} + [rd] {-rd} + *[other] Server {$server} +} + +## Ability fragments + +trash-card = trash {$card-str} +trash-card-at-no-cost = trash {$card-str} at no cost + +trash-n-cards = trash {$count -> + [zero] no cards + [one] 1 card + *[other] {$count} cards +} + +trash-cards = trash {$count -> + [zero] no cards + [one] 1 card ({$card-strs}) + *[other] {$count} cards ({$card-strs}) +} + +gain-credits = gain {$count} {-credit} + +draw-cards = draw {$count -> + [one] 1 card + *[other] {$count} cards +} + +make-a-run = make a run +make-a-run-on = make a run on {$server} + +## Payments + +payment-click = spends {$value} {-click} +payment-credit = pays {$value} {-credit} diff --git a/resources/public/i18n/ja.ftl b/resources/public/i18n/ja/ui.ftl similarity index 100% rename from resources/public/i18n/ja.ftl rename to resources/public/i18n/ja/ui.ftl diff --git a/resources/public/i18n/ko.ftl b/resources/public/i18n/ko/ui.ftl similarity index 99% rename from resources/public/i18n/ko.ftl rename to resources/public/i18n/ko/ui.ftl index d2ca1857be..5e44d6869f 100644 --- a/resources/public/i18n/ko.ftl +++ b/resources/public/i18n/ko/ui.ftl @@ -274,6 +274,8 @@ game_game-start = 게임 시작: {$timestamp} game_grip = 그립 +game_grip-count = 그립({$cnt}) + game_heap = 힙({$cnt}) game_hq = 본부(HQ) @@ -342,6 +344,8 @@ game_special-mu-count = {$available} 메모리 유닛 중 {$unused} {$mu-type} game_stack = 스택 +game_stack-count = 스택({$cnt}) + game_start = 게임 시작 game_start-turn = 차례 시작 diff --git a/resources/public/i18n/la-pig.ftl b/resources/public/i18n/la-pig/ui.ftl similarity index 99% rename from resources/public/i18n/la-pig.ftl rename to resources/public/i18n/la-pig/ui.ftl index f39288e3f6..7f266028a4 100644 --- a/resources/public/i18n/la-pig.ftl +++ b/resources/public/i18n/la-pig/ui.ftl @@ -319,6 +319,8 @@ game_game-start = Amegay Artstay: {$timestamp} game_grip = Ipgray +game_grip-count = Ipgray ({$cnt}) + game_heap = Eaphay ({$cnt}) game_hq = HQYAY @@ -406,6 +408,8 @@ game_special-mu-count = (fn [[unused available mu-type]] (str unused " ofyay " a game_stack = Ackstay +game_stack-count = Ackstay ({$cnt}) + game_start = Artstay Amegay game_start-turn = Artstay Urntay diff --git a/resources/public/i18n/mover.clj b/resources/public/i18n/mover.clj new file mode 100644 index 0000000000..58848cef8f --- /dev/null +++ b/resources/public/i18n/mover.clj @@ -0,0 +1,8 @@ +(ns public.i18n.mover + (:require + [babashka.fs :as fs])) + +(doseq [f (fs/glob "." "**.ftl" {:recursive false}) + :let [d (fs/strip-ext f)]] + (fs/move f (fs/file d "ui.ftl") {:replace-existing true}) + ) diff --git a/resources/public/i18n/pl.ftl b/resources/public/i18n/pl/ui.ftl similarity index 99% rename from resources/public/i18n/pl.ftl rename to resources/public/i18n/pl/ui.ftl index 7bad221f74..1fc6a6102e 100644 --- a/resources/public/i18n/pl.ftl +++ b/resources/public/i18n/pl/ui.ftl @@ -286,6 +286,8 @@ game_game-start = Rozpoczęcie gry: {$timestamp} game_grip = Garść +game_grip-count = Garść ({$cnt}) + game_heap = Sterta ({$cnt}) game_hq = KG @@ -358,6 +360,8 @@ game_special-mu-count = Wolne: {$unused} z {$available} {$mu-type} JP game_stack = Stos +game_stack-count = Stos ({$cnt}) + game_start = Rozpoczęcie gry game_start-turn = Rozpoczęcie tury diff --git a/resources/public/i18n/pt/messages.ftl b/resources/public/i18n/pt/messages.ftl new file mode 100644 index 0000000000..2529451454 --- /dev/null +++ b/resources/public/i18n/pt/messages.ftl @@ -0,0 +1,59 @@ +## Logical templates + +use-card = {$username} usa {$title} para {$do-ability}. +pay-use-card = {$username} {$payment} para usar {$title} para {$do-ability}. + +# join ability framents together naturally +# for example, "$player uses $card to do x and do y and do z" will use [join-with-and] +# like this: "$player uses $card to do x[join-with-and]do y[join-with-and]do z." + +join-with-and = {" "}e{" "} +join-list = ,{" "} + +## locations + +-archives = Arquivos +-hq = QG +-rd = PD + +-credit = [Credit] +-click = [Click] + +server-name = {$server -> + [archives] {-archives} + [hq] {-hq} + [rd] {-PD} + *[other] Servidor {$server} +} + +## Ability fragments + +trash-card = excluir {$card-str} +trash-card-at-no-cost = excluir {$card-str} sem custo + +trash-n-cards = trash {$count -> + [zero] nenhuma carta + [one] 1 carta + *[other] {$count} cartas +} + +trash-cards = excluir {$count -> + [zero] nenhuma carta + [one] 1 carta ({$card-strs}) + *[other] {$count} cartas ({$card-strs}) +} + +gain-credits = ganhar {$count} {-credit} + +draw-cards = comprar {$count -> + [one] 1 carta + *[other] {$count} cartas +} + +make-a-run = fazer um corre +make-a-run-on = fazer um corre no {$server} + +## Payments + +payment-click = gasta {$value} {-click} +payment-credit = paga {$value} {-credit} diff --git a/resources/public/i18n/pt.ftl b/resources/public/i18n/pt/ui.ftl similarity index 99% rename from resources/public/i18n/pt.ftl rename to resources/public/i18n/pt/ui.ftl index c2ba37a965..8c89da2dda 100644 --- a/resources/public/i18n/pt.ftl +++ b/resources/public/i18n/pt/ui.ftl @@ -310,6 +310,8 @@ game_game-start = Começar partida: {$timestamp} game_grip = Mão +game_grip-count = Mão ({$cnt}) + game_heap = Heap ({$cnt}) game_hq = QG @@ -385,6 +387,8 @@ game_special-mu-count = {$unused} de {$available} {$mu-type} MU não utilizada game_stack = Pilha +game_stack-count = Pilha ({$cnt}) + game_start = Começar Partida game_start-turn = Iniciar Turno diff --git a/resources/public/i18n/ru.ftl b/resources/public/i18n/ru/ui.ftl similarity index 99% rename from resources/public/i18n/ru.ftl rename to resources/public/i18n/ru/ui.ftl index e583080a3b..9aad755f51 100644 --- a/resources/public/i18n/ru.ftl +++ b/resources/public/i18n/ru/ui.ftl @@ -643,7 +643,7 @@ game_game-start = Начало игры: {$timestamp} game_grip = Рука -game_grip-count = Grip ({$cnt}) +game_grip-count = Рука ({$cnt}) game_heap = Куча ({$cnt}) diff --git a/resources/public/i18n/zh-simp.ftl b/resources/public/i18n/zh-simp/ui.ftl similarity index 100% rename from resources/public/i18n/zh-simp.ftl rename to resources/public/i18n/zh-simp/ui.ftl diff --git a/resources/public/i18n/zh-trad.ftl b/resources/public/i18n/zh-trad/ui.ftl similarity index 100% rename from resources/public/i18n/zh-trad.ftl rename to resources/public/i18n/zh-trad/ui.ftl diff --git a/src/clj/game/cards/agendas.clj b/src/clj/game/cards/agendas.clj index 79f6cce6fb..b69e73a703 100644 --- a/src/clj/game/cards/agendas.clj +++ b/src/clj/game/cards/agendas.clj @@ -1102,7 +1102,6 @@ (let [abi {:prompt "Choose a card to add to the bottom of R&D" :label "add card to bottom of R&D" :show-discard true - :event :corp-turn-begins :once :per-turn :choices {:card #(and (corp? %) (in-discard? %))} @@ -1116,6 +1115,7 @@ (is-scored? state :corp card)))} :abilities [abi] :events [(assoc abi + :event :corp-turn-begins :change-in-game-state {:req (req (seq (:discard corp))) :silent true})]})) (defcard "Helium-3 Deposit" @@ -2319,9 +2319,9 @@ (effect-completed state side eid) (continue-ability state side - {:cost (if (= target "Pay 1 [Credit]") - (->c :credit 1) - (->c :trash-from-hand 1)) + {:cost [(if (= target "Pay 1 [Credit]") + (->c :credit 1) + (->c :trash-from-hand 1))] :change-in-game-state {:req (req enc-ice (rezzed? enc-ice))} :msg (msg "make the runner encounter " (card-str state enc-ice) " again") :async true diff --git a/src/clj/game/cards/assets.clj b/src/clj/game/cards/assets.clj index ef358a0f88..f78189fe3d 100644 --- a/src/clj/game/cards/assets.clj +++ b/src/clj/game/cards/assets.clj @@ -38,6 +38,7 @@ [game.core.ice :refer [update-all-ice update-ice-strength]] [game.core.initializing :refer [card-init]] [game.core.installing :refer [corp-install corp-install-msg]] + [jinteki.i18n :refer [simple-msg]] [game.core.moving :refer [as-agenda mill move remove-from-currently-drawing swap-agendas swap-cards swap-installed trash trash-cards]] [game.core.optional :refer [get-autoresolve set-autoresolve]] @@ -1220,7 +1221,7 @@ (defcard "Front Company" {:static-abilities [{:type :cannot-run-on-server :req (req (not (pos? (count (turn-events state side :run))))) - :value (effect (map first (get-remotes state)))}] + :value (effect (mapv first (get-remotes state)))}] :rez-req (effect (= (:active-player @state) :corp)) :events [{:event :run :req (req (= :archives (target-server context)) @@ -1749,9 +1750,9 @@ (:hand corp)))) :label "Reveal an agenda worth X points from HQ" :async true - :cost [(->c :x-power)] + :cost [(->c :x-power-counter)] :keep-menu-open :while-power-tokens-left - :effect (effect (let [paid-amt (cost-value eid :x-power)] + :effect (effect (let [paid-amt (cost-value eid :power-counter)] (continue-ability state side {:prompt "Choose an agenda in HQ to reveal" @@ -1872,12 +1873,20 @@ :change-in-game-state {:req (req (pos? (count-bad-pub state))) :silent true} :optional {:interactive (effect true) :prompt "Host a bad publicity counter to gain 3 [Credits] and draw a card?" - :yes-ability {:msg (msg "gain 3 [Credits] and draw 1 card") - :cost [(->c :host-bad-pub 1)] + :yes-ability {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 3} + {:effect/type :draw-cards + :effect/count 1}) :async true - :effect (effect (wait-for - (gain-credits state side 3 {:suppress-checkpoint true}) - (draw state side eid 1)))}}}]}) + :effect (effect + (wait-for + (lose-bad-publicity state side 1 nil) + (wait-for + (add-counter state side card :bad-publicity 1 {:suppress-checkpoint true}) + (wait-for + (gain-credits state side 3 {:suppress-checkpoint true}) + (draw state side eid 1)))))}}}]}) (defcard "Magistrate Revontulet" {:static-abilities [{:type :steal-additional-cost diff --git a/src/clj/game/cards/events.clj b/src/clj/game/cards/events.clj index 77889ddd57..5816677a05 100644 --- a/src/clj/game/cards/events.clj +++ b/src/clj/game/cards/events.clj @@ -9,72 +9,85 @@ [game.core.board :refer [all-active-installed all-installed server->zone]] [game.core.card :refer [agenda? asset? card-index condition-counter? corp? event? facedown? faceup? get-card get-counters - get-nested-host get-title get-zone hardware? has-subtype? ice? in-discard? in-hand? - installed? is-type? operation? program? resource? rezzed? runner? upgrade?]] + get-nested-host get-title get-zone hardware? + has-subtype? ice? identity? in-discard? in-hand? + installed? is-type? operation? program? resource? + rezzed? runner? same-faction? 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 cost-option]] - [game.core.cost-fns :refer [install-cost play-cost rez-cost]] + [game.core.cost-fns :refer [play-cost rez-cost]] [game.core.damage :refer [damage]] - [game.core.def-helpers :refer [all-cards-in-hand* in-hand*? - breach-access-bonus defcard draw-abi drain-credits gain-credits-ability offer-jack-out - reorder-choice run-any-server-ability run-central-server-ability run-remote-server-ability run-server-ability run-server-from-choices-ability scry tutor-abi with-revealed-hand - make-icon]] + [game.core.def-helpers :refer [all-cards-in-hand* breach-access-bonus + defcard drain-credits draw-abi + gain-credits-ability in-hand*? make-icon + offer-jack-out reorder-choice + run-any-server-ability + run-central-server-ability + run-remote-server-ability run-server-ability + run-server-from-choices-ability scry + tutor-abi with-revealed-hand]] [game.core.drawing :refer [draw]] [game.core.effects :refer [register-lingering-effect]] [game.core.eid :refer [complete-with-result effect-completed make-eid make-result]] - [game.core.engine :refer [checkpoint do-nothing not-used-once? pay register-events register-pending-event - resolve-ability trigger-event trigger-event-simult - unregister-events unregister-floating-events]] - [game.core.events :refer [first-event? first-run-event? run-events - turn-events run-event-count]] + [game.core.engine :refer [checkpoint not-used-once? pay register-events + register-pending-event resolve-ability + trigger-event trigger-event-simult + unregister-events]] + [game.core.events :refer [first-event? first-run-event? run-event-count + run-events turn-events]] [game.core.expose :refer [expose]] [game.core.finding :refer [find-cid find-latest]] [game.core.flags :refer [any-flag-fn? can-rez? can-trash? - clear-all-flags-for-card! clear-run-flag! clear-turn-flag! - in-corp-scored? register-run-flag! register-turn-flag! zone-locked?]] + clear-all-flags-for-card! clear-run-flag! + clear-turn-flag! in-corp-scored? + register-run-flag! register-turn-flag! + zone-locked?]] [game.core.gaining :refer [gain gain-clicks gain-credits lose lose-clicks lose-credits]] [game.core.hand-size :refer [corp-hand-size+ hand-size]] [game.core.hosting :refer [host]] - [game.core.ice :refer [all-subs-broken? get-strength pump pump-all-icebreakers + [game.core.ice :refer [get-strength pump pump-all-icebreakers update-all-ice update-breaker-strength]] - [game.core.identities :refer [disable-card disable-identity enable-card - enable-identity]] + [game.core.identities :refer [disable-identity]] [game.core.initializing :refer [card-init make-card]] [game.core.installing :refer [install-as-condition-counter install-locked? - runner-can-install? runner-can-pay-and-install? runner-install]] + runner-can-install? + runner-can-pay-and-install? runner-install]] [game.core.link :refer [get-link]] [game.core.mark :refer [identify-mark-ability mark-changed-event]] [game.core.memory :refer [available-mu]] [game.core.moving :refer [as-agenda flip-facedown forfeit mill move swap-ice trash trash-cards]] - [game.core.payment :refer [can-pay? ->c cost-value x-cost-value]] + [game.core.payment :refer [->c can-pay? cost-target x-cost-value]] [game.core.play-instants :refer [play-instant]] - [game.core.prevention :refer [damage-name prevent-damage preventable? prevent-end-run prevent-up-to-n-tags prevent-up-to-n-damage]] + [game.core.prevention :refer [damage-type prevent-damage prevent-end-run + prevent-up-to-n-damage prevent-up-to-n-tags + preventable?]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-prop]] [game.core.revealing :refer [reveal reveal-loud]] [game.core.rezzing :refer [derez get-rez-cost rez]] - [game.core.runs :refer [bypass-ice can-run-server? gain-next-run-credits get-runnable-zones - make-run prevent-access successful-run-replace-breach - total-cards-accessed]] + [game.core.runs :refer [bypass-ice gain-next-run-credits get-runnable-zones + make-run prevent-access + successful-run-replace-breach total-cards-accessed]] [game.core.sabotage :refer [sabotage-ability]] [game.core.say :refer [system-msg]] - [game.core.servers :refer [central->name is-central? is-remote? remote->name + [game.core.servers :refer [central->name is-remote? remote->name target-server unknown->kw zone->name zones->sorted-names]] [game.core.set-aside :refer [get-set-aside set-aside]] - [game.core.shuffling :refer [shuffle! shuffle-into-deck shuffle-my-deck! fail-to-find!]] + [game.core.shuffling :refer [fail-to-find! shuffle! shuffle-into-deck]] [game.core.tags :refer [gain-tags gain-tags-ability lose-tags]] - [game.core.threat :refer [threat threat-level]] + [game.core.threat :refer [threat-level]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] [game.core.update :refer [update!]] [game.core.virus :refer [get-virus-counters]] [game.macros :refer [continue-ability effect msg req wait-for]] [game.utils :refer :all] + [jinteki.i18n :refer [msg-with-cost simple-msg]] [jinteki.utils :refer :all] [jinteki.validator :refer [legal?]])) @@ -89,7 +102,9 @@ ;; don't need to show messages if the ice is already trashed! (get-card state (:ice context)) (first-run-event? state side :subroutines-broken #(pred (first %)))))) - :msg (msg "trash " (card-str state (:ice context))) + :msg (msg-with-cost + {:effect/type :trash-card + :effect/card-str (:ice context)}) :effect (effect (trash state side eid (:ice context) {:cause-card card}))}]}) ;; Card definitions @@ -126,17 +141,21 @@ (map unknown->kw) (filter is-remote?) (map remote->name)))) - :msg (msg "make a run on " target) + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server target}) :async true :effect (effect (make-run state side eid target card))}]})}) (defcard "Always Have a Backup Plan" {:makes-run true :on-play {:prompt "Choose a server" - :change-on-game-state (effect (seq runnable-servers)) + :change-in-game-state (effect (seq runnable-servers)) :choices (effect runnable-servers) :async true - :msg (msg "make a run on " target) + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server target}) :effect (effect (wait-for (make-run state side target card) (let [card (get-card state card) run-again (get-in card [:special :run-again])] @@ -160,12 +179,18 @@ :once :per-run :req (req (get-in card [:special :run-again]) (same-card? (:ice context) (get-in card [:special :run-again-ice]))) - :msg (msg "bypass " (:title (:ice context))) + :msg (simple-msg + {:effect/type :bypass-ice + :effect/card-str (:ice context)}) :effect (effect (bypass-ice state))}]}) (defcard "Amped Up" {:on-play - {:msg "gain [Click][Click][Click] and suffer 1 core damage" + {:msg (simple-msg + {:effect/type :gain-clicks + :effect/count 3} + {:effect/type :suffer-core-damage + :effect/value 1}) :async true :effect (effect (gain-clicks state side 3) (damage state side eid :brain 1 {:unpreventable true :card card}))}}) @@ -176,18 +201,24 @@ :unsuccessful {:async true :effect (effect (gain-credits state :runner eid (+ (:agenda-point runner) (:agenda-point corp)))) - :msg (msg (str "gain " (+ (:agenda-point runner) (:agenda-point corp)) " [Credits]"))}}}]}) + :msg (simple-msg + {:effect/type :gain-credits + :effect/count (+ (:agenda-point runner) (:agenda-point corp))})}}}]}) (defcard "Apocalypse" (let [corp-trash {:async true - :effect (effect (let [ai (all-installed state :corp) - onhost (filter #(= '(:onhost) (:zone %)) ai) - unhosted (->> ai - (remove #(= '(:onhost) (:zone %))) - (sort-by #(vec (:zone %))) - (reverse)) - allcorp (concat onhost unhosted)] - (trash-cards state :runner eid allcorp {:cause-card card})))} + :effect (effect + (let [ai (all-installed state :corp) + onhost (filter #(= [:onhost] (:zone %)) ai) + ;; trash cards from right to left + ;; otherwise, auto-killing servers would move the cards to the next server + ;; so they could no longer be trashed in the same loop + unhosted (->> ai + (remove #(= [:onhost] (:zone %))) + (sort-by #(vec (:zone %))) + (reverse)) + allcorp (concat onhost unhosted)] + (trash-cards state :runner eid allcorp {:cause-card card})))} runner-facedown {:effect (effect (let [installedcards (all-active-installed state :runner) ishosted (fn [c] (= '(:onhost) (get c :zone))) hostedcards (filter ishosted installedcards) @@ -203,19 +234,19 @@ (some #{:rd} (:successful-run runner-reg)) (some #{:archives} (:successful-run runner-reg))) :async true - ;; trash cards from right to left - ;; otherwise, auto-killing servers would move the cards to the next server - ;; so they could no longer be trashed in the same loop - :msg "trash all installed Corp cards and turn all installed Runner cards facedown" + :msg (simple-msg + :trash-all-installed-corp + :turn-all-installed-runner-facedown) :effect (effect (wait-for (resolve-ability state side corp-trash card nil) (continue-ability state side runner-facedown card nil)))}})) (defcard "Ashen Epilogue" {:on-play - {:msg (msg (if (not (zone-locked? state :runner :discard)) - "shuffle the grip and heap into the stack" - "shuffle the grip into the stack")) + {:msg (simple-msg + (if (zone-locked? state :runner :discard) + :shuffle-grip-into-stack + :shuffle-grip-and-heap-into-stack)) :rfg-instead-of-trashing true :async true :effect (effect (shuffle-into-deck state :runner :hand :discard) @@ -223,48 +254,66 @@ (doseq [c top-5] (move state side c :rfg)) (system-msg state side - (str "removes " - (enumerate-cards top-5) - " from the game and draws 5 cards")) - (draw state :runner eid 5)))}}) + {:msg/type :msg-rfg-n-cards-from-stack + :msg/count (count top-5) + :msg/card-strs top-5}) + (draw state :runner eid 5 {:loud true})))}}) (defcard "Bahia Bands" - (let [all [{:async true - :effect (effect (draw state side eid 2)) - :msg "draw 2 cards"} - {:msg "install a card from the grip, paying 1 [Credits] less" + (let [all [{:label "draw 2 cards" + :msg (simple-msg + {:effect/type :draw-cards + :effect/count 2}) :async true + :effect (effect (draw state side eid 2))} + {:label "install a card from the grip, paying 1 [Credits] less" + :msg (simple-msg + {:effect/type :install-from-grip-with-discount + :effect/title (get-title card) + :effect/discount 1}) :req (req (not (install-locked? state side))) + :async true :effect (effect (continue-ability - state side {:prompt "Choose a card to install" + state side + {:prompt "Choose a card to install" :waiting-prompt true :choices {:req (req (or (hardware? target) - (program? target) - (resource? target)) - (in-hand*? state target) - (runner-can-pay-and-install? state side (assoc eid :source card) target {:cost-bonus -1}))} + (program? target) + (resource? target)) + (in-hand*? state target) + (runner-can-pay-and-install? state side (assoc eid :source card) target {:cost-bonus -1}))} :async true - :effect (effect (runner-install state side (assoc eid :source card :source-type :runner-install) target {:cost-bonus -1 - :msg-keys {:install-source card - :display-origin true}}))} + :effect (effect (runner-install state side (assoc eid :source card + :source-type :runner-install) + target {:cost-bonus -1 + :msg-keys {:install-source card + :display-origin true}}))} card nil))} - {:msg "remove 1 tag" + {:label "remove 1 tag" + :msg (simple-msg + {:effect/type :remove-tags + :effect/count 1}) :async true :effect (effect (lose-tags state side eid 1))} - {:effect (effect (add-counter state side eid (get-card state card) :credit 4 nil)) + {:label "place 4 [Credits] for paying trash costs" + :msg (simple-msg + {:effect/type :place-credits-on-self-for-trash-costs + :effect/credits 4}) :async true - :msg "place 4 [Credits] for paying trash costs"}] + :effect (effect (add-counter state side eid (get-card state card) :credit 4 nil))}] choice (fn choice [abis rem] {:prompt (str "Choose an ability to resolve (" rem " remaining)") :waiting-prompt true - :choices (map #(capitalize (:msg %)) abis) + :choices (mapv #(capitalize (:label %)) abis) :async true - :effect (effect (let [chosen (some #(when (= target (capitalize (:msg %))) %) abis)] - (wait-for - (resolve-ability state side chosen card nil) - (if (< 1 rem) - (continue-ability state side (choice (remove-once #(= % chosen) abis) (dec rem)) card nil) - (effect-completed state side eid)))))})] + :effect (effect + (let [chosen (some #(when (= target (capitalize (:label %))) %) abis)] + (wait-for + (resolve-ability state side chosen card nil) + (continue-ability state side + (when (< 1 rem) + (choice (remove-once #(= % chosen) abis) (dec rem))) + card nil))))})] {:makes-run true :on-play (run-any-server-ability) :interactions {:pay-credits {:req (req (= :runner-trash-corp-cards (:source-type eid)) @@ -286,7 +335,10 @@ {:target-server :remote :this-card-run true :ability - {:msg "shuffle all cards in the server into R&D" + {:msg (simple-msg + {:effect/type :shuffle-cards-in-server-into-rd + :effect/server (first (:server run))}) + :label "shuffle all cards in the server into R&D" :effect (effect (doseq [c (:content run-server)] (move state :corp c :deck)) (shuffle! state :corp :deck))}})]}) @@ -300,25 +352,29 @@ (resolve-ability state side {:prompt "Install a non-virus program" - :choices (effect (cancellable (filter #(and (program? %) - (not (has-subtype? % "Virus")) - (runner-can-install? state side eid % {:no-toast true})) - (:deck runner)))) + :choices (effect (cancellable (filterv #(and (program? %) + (not (has-subtype? % "Virus")) + (runner-can-install? state side eid % {:no-toast true})) + (:deck runner)))) :async true - :effect (effect (runner-install state side eid target {:ignore-all-cost :true :msg-keys {:display-origin true :source-card card}}))} + :effect (effect (runner-install state side eid target {:ignore-all-cost :true + :msg-keys {:display-origin true + :source-card card}}))} card nil)] (continue-ability state side - (run-any-server-ability - {:events [{:event :run-ends - :unregister-once-resolved true - :duration :end-of-run - :interactive (effect true) - :automatic :last - :change-in-game-state {:silent true - :req (req (get-card state installed-card))} - :msg (msg "add " (:title installed-card) " to the top of the stack") - :effect (effect (move state side (get-card state installed-card) :deck {:front true}))}]}) - card nil)))}}) + (run-any-server-ability + {:events [{:event :run-ends + :unregister-once-resolved true + :duration :end-of-run + :interactive (effect true) + :automatic :last + :change-in-game-state {:silent true + :req (req (get-card state installed-card))} + :msg (simple-msg + {:effect/type :add-card-to-top-of-stack + :effect/card-str installed-card}) + :effect (effect (move state side installed-card :deck {:front true}))}]}) + card nil)))}}) (defcard "Black Hat" {:on-play @@ -335,7 +391,7 @@ :prompt "Choose a server" :change-in-game-state {:req (req (seq runnable-servers))} :choices (effect runnable-servers) - :msg "prevent ice from being rezzed during this run" + :msg (simple-msg :prevent-ice-rezzed-during-run) :async true :effect (effect (register-run-flag! state side card @@ -378,14 +434,17 @@ state side card [{:event :pass-ice :duration :end-of-run - :effect (effect (update! state side (update-in (get-card state card) [:special :bravado-passed] (fnil conj #{}) (:cid (:ice context)))))}]) - (make-run state side eid target (get-card state card)))} + :effect (effect + (update! state side (update-in (get-card state card) + [:special :bravado-passed] + (fnil conj #{}) (:cid (:ice context)))))}]) + (make-run state side eid target (get-card state card)))} :events [{:event :run-ends :silent true - :msg (msg "gain " - (+ 6 (count (get-in card [:special :bravado-passed])) - (get-in card [:special :bravado-moved] 0)) - " [Credits]") + :msg (simple-msg + {:effect/type :gain-credits + :effect/count (+ 6 (count (get-in card [:special :bravado-passed])) + (get-in card [:special :bravado-moved] 0))}) :async true :effect (effect (let [qty (+ 6 (count (get-in card [:special :bravado-passed])) (get-in card [:special :bravado-moved] 0))] @@ -405,7 +464,11 @@ {:async true :base-play-cost [(->c :x-credits)] :choices (effect runnable-servers) - :msg (msg "make a run on " target " and increase the rez cost of the first unrezzed piece of ice approached by " (x-cost-value eid) " [Credits]") + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server target} + {:effect/type :increase-rez-cost-first-unrezzed-approached-ice + :effect/credits (x-cost-value eid)}) :prompt "Choose a server" :effect (effect (let [bribery-x (x-cost-value eid)] (register-events @@ -443,10 +506,14 @@ (defcard "Build Script" {:on-play - {:msg "gain 1 [Credits] and draw 2 cards" + {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 1} + {:effect/type :draw-cards + :effect/count 2}) :async true :effect (effect (wait-for (gain-credits state side 1) - (draw state side eid 2)))}}) + (draw state side eid 2)))}}) (defcard "Burner" (letfn [(move-ab [chosen-cards n] @@ -460,8 +527,12 @@ {:prompt (str "Choose where to put " (:title target-card)) :choices ["Top of R&D" "Bottom of R&D"] :async true - :msg (msg "add " (:title target-card) " to the " - (decapitalize target)) + :msg (simple-msg + (if (= "Top of R&D" target) + {:effect/type :add-card-to-top-of-rd + :effect/title (get-title target-card)} + {:effect/type :add-card-to-bottom-of-rd + :effect/title (get-title target-card)})) :effect (effect (if (= target "Top of R&D") (move state :corp target-card :deck {:front true}) @@ -484,13 +555,14 @@ :ability {:req (req (<= 1 (count (:hand corp)))) :async true - :effect (effect (let [chosen-cards (take 3 (shuffle (:hand corp)))] - (wait-for - (reveal-loud state side card nil chosen-cards) - (continue-ability - state side - (move-ab chosen-cards (min 2 (count chosen-cards))) - card nil))))}})]})) + :effect (effect + (let [chosen-cards (take 3 (shuffle (:hand corp)))] + (wait-for + (reveal-loud state side card nil chosen-cards) + (continue-ability + state side + (move-ab chosen-cards (min 2 (count chosen-cards))) + card nil))))}})]})) (defcard "By Any Means" {:on-play @@ -504,23 +576,29 @@ (not (in-discard? (:accessed-card context)))) :interactive (effect true) :async true - :msg (msg "trash " (:title (:accessed-card context)) " at no cost and suffer 1 meat damage") + :msg (simple-msg + {:effect/type :trash-card-at-no-cost + :effect/card-str (assoc (:accessed-card context) :seen true)} + {:effect/type :suffer-meat-damage + :effect/value 1}) :effect (effect (wait-for (trash state side (assoc (:accessed-card context) :seen true) - {:cause-card card :accessed true}) - (swap! state assoc-in [:runner :register :trashed-card] true) - (swap! state assoc-in [:runner :register :trashed-accessed-card] true) - (damage state :runner eid :meat 1 {:unboostable true})))}]))}}) + {:cause-card card :accessed true}) + (swap! state assoc-in [:runner :register :trashed-card] true) + (swap! state assoc-in [:runner :register :trashed-accessed-card] true) + (damage state :runner eid :meat 1 {:unboostable true})))}]))}}) (defcard "Calling in Favors" {:on-play - {:msg (msg "gain " (count (filter #(and (has-subtype? % "Connection") (resource? %)) - (all-active-installed state :runner))) " [Credits]") - :change-in-game-state {:req (req (some #(and (has-subtype? % "Connection") (resource? %)) - (all-active-installed state :runner)))} + {:change-in-game-state {:req (req (some #(and (has-subtype? % "Connection") (resource? %)) + (all-active-installed state :runner)))} + :msg (simple-msg + {:effect/type :gain-credits + :effect/count (count (filter #(and (has-subtype? % "Connection") (resource? %)) + (all-active-installed state :runner)))}) :async true - :effect (effect (gain-credits state side eid (count (filter #(and (has-subtype? % "Connection") - (resource? %)) - (all-active-installed state :runner)))))}}) + :effect (effect (gain-credits state side eid + (count (filter #(and (has-subtype? % "Connection") (resource? %)) + (all-active-installed state :runner)))))}}) (defcard "Career Fair" {:on-play @@ -538,7 +616,9 @@ {:on-play {:prompt "Choose a card in or protecting a remote server" :choices {:card #(is-remote? (second (get-zone %)))} - :msg (msg "prevent the Corp from rezzing " (card-str state target) " for the rest of the turn") + :msg (simple-msg + {:effect/type :prevent-corp-rez-card-during-turn + :effect/card-str target}) :effect (effect (let [t target c card] @@ -583,13 +663,15 @@ :choices ["Done" "Start over"] :async true :effect (effect (if (= target "Done") - (do (doseq [c (reverse chosen)] - (move state :corp c :deck {:front true})) - (system-msg state side (str "The top cards of R&D are " - " are " (enumerate-cards chosen)) {:log-side :corp}) - (effect-completed state side eid)) - (continue-ability state side (cbi-choice original '() (count original) original) - card nil)))}) + (do (doseq [c (reverse chosen)] + (move state :corp c :deck {:front true})) + (system-msg state side + (str "The top cards of R&D are " + " are " (enumerate-cards chosen)) + {:log-side :corp}) + (effect-completed state side eid)) + (continue-ability state side (cbi-choice original () (count original) original) + card nil)))}) (cbi-choice [remaining chosen n original] {:player :corp :prompt "Choose a card to move next onto R&D" @@ -597,7 +679,8 @@ :async true :effect (effect (continue-ability - state side (let [chosen (cons target chosen)] + state side + (let [chosen (cons target chosen)] (if (< (count chosen) n) (cbi-choice (remove-once #(= target %) remaining) chosen n original) (cbi-final chosen original))) @@ -609,15 +692,15 @@ :mandatory true :this-card-run true :ability - {:msg "force the Corp to add all cards in HQ to the top of R&D" + {:msg (simple-msg :force-add-all-hq-cards-to-top-of-rd) :player :corp :waiting-prompt true :async true :effect (effect (continue-ability - state side (let [from (:hand corp)] - (when (pos? (count from)) - (cbi-choice from '() (count from) from))) + state side + (when-let [from (seq (:hand corp))] + (cbi-choice from () (count from) from)) card nil))}})]})) (defcard "Chain Reaction" @@ -628,7 +711,9 @@ :choices {:card (every-pred runner? installed?)} :waiting-prompt true :display-side :corp - :msg (msg "trash " (:title target)) + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str target}) :effect (effect (trash state :corp eid target))} cards-to-trash (fn [state] (min 2 (count (all-installed state :corp)))) runner-choice {:prompt (msg "choose " (quantify (cards-to-trash state) "card") " to trash") @@ -637,13 +722,17 @@ :max (effect (cards-to-trash state)) :all true} :waiting-prompt true - :msg {:public (msg "trash " (enumerate-str (map #(card-str state %) targets))) - :corp (msg "trash " (enumerate-str (map #(card-str state % {:maybe-visible true}) targets)))} + :msg {:public (simple-msg + {:effect/type :trash-cards + :effect/count (count targets) + :effect/card-strs targets}) + :corp (simple-msg + {:effect/type :trash-cards + :effect/count (count targets) + :effect/card-strs (mapv #(assoc % :maybe-visible true) targets)})} :effect (effect (wait-for (trash-cards state side targets) - (continue-ability - state :corp - corp-choice - card nil)))}] + (continue-ability + state :corp corp-choice card nil)))}] {:on-play {:async true :change-in-game-state {:req (effect (or (seq (all-installed state :corp)) (seq (all-installed state :runner))))} @@ -651,53 +740,47 @@ (some #{:rd} (:successful-run runner-reg)) (some #{:archives} (:successful-run runner-reg))) :effect (effect (if (seq (all-installed state :corp)) - (continue-ability state side runner-choice card nil) + (continue-ability state :runner runner-choice card nil) (continue-ability state :corp corp-choice card nil)))}})) (defcard "Charm Offensive" - (letfn [(trash-x-opt [t] - {:option (str "Trash a rezzed copy of " t) - :ability {:async true - :prompt (msg "Choose a rezzed copy of " t " to trash") - :choices {:card #(and (rezzed? %) - (= t (:title %)))} - :msg (msg "trash " (card-str state target)) - :effect (effect (trash state side eid target {:cause-card card}))}})] - {:makes-run true - :on-play (run-server-ability :archives) - :events [{:event :breach-server - :req (req (= :archives (:server context))) - :silent true - :effect (effect (let [ts (distinct (map :title (:discard corp)))] - (update! state side - (update-in card [:special :accessed] concat ts))))} - {:event :access-card - :req (req (in-discard? (:accessed-card context))) - :silent true - :effect (effect (update! state side - (update-in card [:special :accessed] conj (:title (:accessed-card context)))))} - {:event :run-ends - :req (req (seq (get-in card [:special :accessed]))) - :async true - :interactive (effect true) - :effect (effect (let [rezzed-titles - (map :title (filter rezzed? (all-installed state :corp))) - isec (set/intersection - (into #{} rezzed-titles) - (into #{} (get-in card [:special :accessed])))] - (if (seq isec) - (continue-ability - state side - ;; note - the frontend will highlight all applicable cards - ;; if that wasn't so, this would be an unusable approach - {:async true - :prompt "Trash a rezzed copy of a card you accessed" - :choices {:card #(and (rezzed? %) - (contains? isec (:title %)))} - :msg (msg "trash " (card-str state target)) - :effect (effect (trash state side eid target {:cause-card card}))} - card nil) - (effect-completed state side eid))))}]})) + {:makes-run true + :on-play (run-server-ability :archives) + :events [{:event :breach-server + :req (req (= :archives (:server context))) + :silent true + :effect (effect (let [ts (distinct (map :title (:discard corp)))] + (update! state side + (update-in card [:special :accessed] concat ts))))} + {:event :access-card + :req (req (in-discard? (:accessed-card context))) + :silent true + :effect (effect (update! state side + (update-in card [:special :accessed] conj (:title (:accessed-card context)))))} + {:event :run-ends + :req (req (seq (get-in card [:special :accessed]))) + :async true + :interactive (effect true) + :effect (effect (let [rezzed-titles + (map :title (filter rezzed? (all-installed state :corp))) + isec (set/intersection + (into #{} rezzed-titles) + (into #{} (get-in card [:special :accessed])))] + (if (seq isec) + (continue-ability + state side + ;; note - the frontend will highlight all applicable cards + ;; if that wasn't so, this would be an unusable approach + {:async true + :prompt "Trash a rezzed copy of a card you accessed" + :choices {:card #(and (rezzed? %) + (contains? isec (:title %)))} + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str target}) + :effect (effect (trash state side eid target {:cause-card card}))} + card nil) + (effect-completed state side eid))))}]}) (defcard "Chastushka" {:makes-run true @@ -714,25 +797,31 @@ :waiting-prompt true :choices (effect (conj (filter agenda? (:hand corp)) "Done")) :async true - :effect (effect (if (= target "Done") - (do (system-msg state :corp "declines to reveal an agenda from HQ") - (scry state :runner eid card :corp 3)) - (do - (wait-for (reveal-loud state side card {:forced true} target) - (continue-ability - state :runner - {:msg "gain [Click] and draw 1 card" - :async true - :effect (effect (gain-clicks state :runner 1) - (draw state :runner eid 1))} - card nil)))))}}) + :effect (effect + (if (= target "Done") + (do (system-msg state :corp "declines to reveal an agenda from HQ") + (scry state :runner eid card :corp 3)) + (wait-for (reveal-loud state side card {:forced true} target) + (continue-ability + state :runner + {:msg (simple-msg + {:effect/type :gain-clicks + :effect/count 1} + {:effect/type :draw-cards + :effect/count 1}) + :async true + :effect (effect (gain-clicks state :runner 1) + (draw state :runner eid 1))} + card nil))))}}) (defcard "Clean Getaway" {:makes-run true :on-play (run-any-server-ability) :events [{:event :successful-run :req (req this-card-run) - :msg "gain 6 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 6}) :async true :effect (effect (gain-credits state :runner eid 6))}]}) @@ -747,7 +836,11 @@ :ability {:async true :prompt "Choose a program to install" - :msg (msg "install " (:title target) " and take 1 tag") + :msg (simple-msg + {:effect/type :runner-install-card + :effect/title target} + {:effect/type :take-tags + :effect/count 1}) :choices (effect (filter #(and (program? %) (runner-can-pay-and-install? state side (assoc eid :source card) % {:cost-bonus (rd-ice state)})) (:deck runner))) @@ -769,7 +862,9 @@ :prompt "Choose a program that was used during the run" :choices {:card #(and (program? %) (installed? %))} - :msg (msg "trash " (:title target)) + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str target}) :async true :effect (effect (trash state side eid target {:unpreventable true :cause-card card}))}]}) @@ -802,7 +897,9 @@ :interactive (effect true) :change-in-game-state {:req (req (installed? (get-card state installed-card))) :silent true} - :msg (msg "move " (card-str state installed-card) " to the bottom of the stack") + :msg (simple-msg + {:effect/type :add-card-to-bottom-of-stack + :effect/card-str installed-card}) :effect (effect (move state side installed-card :deck))}])) (effect-completed state side eid)))})] {:makes-run true @@ -824,8 +921,13 @@ (letfn [(reveal-and-load-credits [stack] (when-let [topcard (first stack)] {:async true - :msg (msg "reveal " (get-title topcard) " from the top of the stack, " - "move it to the grip and place " (:cost topcard) " [Credits] on itself") + :msg (simple-msg + {:effect/type :reveal-top-of-stack + :effect/title topcard} + {:effect/type :move-seen-into-grip + :effect/seen [topcard]} + {:effect/type :place-credits-on-self + :effect/credits (:cost topcard)}) :effect (effect (wait-for (reveal state side topcard) (wait-for @@ -846,7 +948,10 @@ (defcard "Contaminate" {:on-play - {:msg (msg "place 3 virus counters on " (:title target)) + {:msg (simple-msg + {:effect/type :place-virus-counters + :effect/count 3 + :effect/title target}) :choices {:req (req (installed? target) (runner? target) (zero? (get-virus-counters state target)))} @@ -858,20 +963,27 @@ (defcard "Corporate \"Grant\"" {:events [{:event :runner-install :req (req (first-event? state side :runner-install)) - :msg "force the Corp to lose 1 [Credit]" + :msg (simple-msg + {:effect/type :force-corp-lose-credits + :effect/credits 1}) :async true :effect (effect (lose-credits state :corp eid 1))}]}) (defcard "Corporate Scandal" - {:on-play {:msg "give the Corp 1 additional bad publicity" + {:on-play {:msg (simple-msg + {:effect/type :give-bad-publicity + :effect/count 1}) :implementation "No enforcement that this Bad Pub cannot be removed" :effect (effect (swap! state update-in [:corp :bad-publicity :additional] inc))} :leave-play (effect (swap! state update-in [:corp :bad-publicity :additional] dec))}) (defcard "Creative Commission" - {:on-play {:msg (msg "gain 5 [Credits]" - (when (pos? (:click runner)) - " and lose [Click]")) + {:on-play {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 5} + (when (pos? (:click runner)) + {:effect/type :lose-clicks + :effect/count 1})) :async true :effect (effect (when (pos? (:click runner)) (lose-clicks state :runner 1)) @@ -903,7 +1015,9 @@ :effect (effect (system-msg state :corp (str "spends " cost " [Credits] to prevent " title " from being trashed at no cost")) (lose-credits state :corp eid cost))} - :no-ability {:msg (msg "trash " title " at no cost") + :no-ability {:msg (simple-msg + {:effect/type :trash-card-at-no-cost + :effect/card-str (assoc c :seen true)}) :async true :effect (effect (trash state side eid (assoc c :seen true) {:cause-card card}))}}} card nil) @@ -918,7 +1032,9 @@ (in-hand*? state target) (runner-can-pay-and-install? state side (assoc eid :source card) target {:cost-bonus -8}))} :async true - :cancel {:msg "take 1 tag" + :cancel {:msg (simple-msg + {:effect/type :take-tags + :effect/count 1}) :async true :effect (effect (gain-tags state :runner eid 1))} :effect (effect (let [new-eid (make-eid state {:source card :source-type :runner-install})] @@ -965,6 +1081,9 @@ true))) (make-run state side eid serv card))}} :no-ability {:async true + :msg (simple-msg + {:effect/type :run-on-with-no-rezzed-ice + :effect/server serv}) :effect (effect (register-run-flag! state side card :can-rez @@ -973,8 +1092,7 @@ ((constantly false) (toast state :corp "Cannot rez ice on this run due to Cyber Threat")) true))) - (make-run state side eid serv card)) - :msg (msg "make a run on " serv " during which no ice can be rezzed")}}} + (make-run state side eid serv card))}}} {:async true :effect (effect (register-run-flag! state side card @@ -985,7 +1103,9 @@ (toast state :corp "Cannot rez ice on this run due to Cyber Threat")) true))) (make-run state side eid serv card)) - :msg (msg "make a run on " serv " during which no ice can be rezzed")}) + :msg (simple-msg + {:effec/type :run-on-with-no-rezzed-ice + :effect/server serv})}) card nil)))}}) (defcard "Data Breach" @@ -1003,7 +1123,9 @@ (defcard "Day Job" {:on-play {:additional-cost [(->c :click 3)] - :msg "gain 10 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 10}) :async true :effect (effect (gain-credits state side eid 10))}}) @@ -1074,7 +1196,7 @@ :yes-ability {:async true :cost [(->c :click 1)] - :msg "access another card" + :msg (simple-msg :access-another-card) :effect (effect (continue-ability state side (deep-dive-access cards) card nil))}}} card nil) (do (shuffle-back state (get-set-aside state :corp eid)) @@ -1088,7 +1210,9 @@ (not (zone-locked? state :runner :discard)))} :prompt "Choose a card to add to Grip" :choices (effect (cancellable (:discard runner) :sorted)) - :msg (msg "add " (:title target) " to [their] Grip") + :msg (simple-msg + {:effect/type :add-card-to-grip + :effect/title (get-title card)}) :async true :effect (effect (move state side target :hand) (continue-ability @@ -1096,7 +1220,9 @@ {:prompt "Choose a virus to add to Grip" :change-in-game-state {:silent true :req (req (seq (filter #(has-subtype? % "Virus") (:discard runner))))} - :msg (msg "add " (:title target) " to [their] Grip") + :msg (simple-msg + {:effect/type :add-card-to-grip + :effect/title (get-title target)}) :choices (effect (cancellable (filter #(has-subtype? % "Virus") (:discard runner)) :sorted)) :effect (effect (move state side target :hand))}) card nil))}}) @@ -1107,7 +1233,9 @@ :interactions {:access-ability {:label "Trash card" :trash? true - :msg (msg "trash " (:title target) " at no cost") + :msg (simple-msg + {:effect/type :trash-card-at-no-cost + :effect/card-str target}) :req (req (can-trash? state :runner target) (not (in-discard? target))) ;;for if the run gets diverted :async true @@ -1116,15 +1244,28 @@ (defcard "Deuces Wild" (let [all [{:effect (effect (gain-credits state side eid 3)) :async true - :msg "gain 3 [Credits]"} + :label "Gain 3 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 3})} {:async true :effect (effect (draw state side eid 2)) - :msg "draw 2 cards"} + :label "Draw 2 cards" + :msg (simple-msg + {:effect/type :draw-cards + :effect/count 2})} {:async true :effect (effect (lose-tags state side eid 1)) - :msg "remove 1 tag"} + :label "Remove 1 tag" + :msg (simple-msg + {:effect/type :remove-tags + :effect/count 1})} {:prompt "Choose 1 piece of ice to expose" - :msg "expose 1 ice and make a run" + :label "Expose 1 ice and make a run" + :msg (simple-msg + {:effect/type :expose-card + :effect/title (get-title target)} + :make-a-run) :choices {:card #(and (installed? %) (ice? %))} :async true @@ -1136,10 +1277,10 @@ :cancel (run-any-server-ability)}] choice (fn choice [abis] {:prompt "Choose an ability to resolve" - :choices (map #(capitalize (:msg %)) abis) + :choices (mapv #(:label %) abis) :waiting-prompt true :async true - :effect (effect (let [chosen (some #(when (= target (capitalize (:msg %))) %) abis)] + :effect (effect (let [chosen (some #(when (= target (:label %)) %) abis)] (wait-for (resolve-ability state side chosen card nil) (if (= (count abis) 4) @@ -1205,7 +1346,7 @@ {:optional {:prompt "Shuffle Direct Access into the Stack?" :yes-ability - {:msg "shuffle itself into the Stack" + {:msg (simple-msg :shuffle-self-into-stack) :effect (effect (move state side (get-card state card) :deck) (shuffle! state side :deck))}}} card nil))}]}) @@ -1216,7 +1357,9 @@ :events [{:event :run-ends :req (req (:successful target) this-card-run) - :msg "gain 5 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 5}) :async true :effect (effect (gain-credits state :runner eid 5))}]}) @@ -1257,7 +1400,11 @@ :on-play {:prompt "Choose a server" :choices (effect runnable-servers) - :msg (msg "make a run on " target " and gain [Click]") + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server target} + {:effect/type :gain-clicks + :effect/count 1}) :async true :effect (effect (gain-clicks state side 1) (make-run state side eid target card))}}) @@ -1275,8 +1422,14 @@ :ability {:prompt "Choose a card type" :choices ["Asset" "Upgrade" "Operation" "ICE"] - :msg (msg "reveal 2 cards from HQ and trash all " - target (when (not= "ICE" target) "s")) + :msg (simple-msg + {:effect/type :reveal-n-cards-in-hq + :effect/count 2} + (case target + "Asset" :trash-all-assets-by-type + "ICE" :trash-all-ice-by-type + "Operation" :trash-all-operations-by-type + "Upgrade" :trash-all-upgrades-by-type)) :async true :effect (effect (let [cards-to-reveal (take 2 (shuffle (:hand corp))) cards-to-trash (filter #(is-type? % target) cards-to-reveal) @@ -1307,37 +1460,45 @@ (letfn [(ec [trash-cost to-trash] {:async true :prompt "Choose a piece of hardware or program to install" - :msg (msg "trash " (if (empty? to-trash) "no cards" (enumerate-cards to-trash :sorted)) - " and install " (:title target) " from the Stack, " - " lowering the cost by " trash-cost) + :msg (simple-msg + {:effect/type :trash-cards + :effect/count (count to-trash) + :effect/card-strs (sort-by get-title to-trash)} + {:effect/type :install-from-stack-with-discount + :effect/title (get-title target) + :effect/discount trash-cost}) :choices (effect (cancellable (filter #(and (or (program? %) - (hardware? %)) - (runner-can-pay-and-install? state side (assoc eid :source card) % {:cost-bonus (- trash-cost)})) + (hardware? %)) + (runner-can-pay-and-install? state side (assoc eid :source card) % {:cost-bonus (- trash-cost)})) (:deck runner)) :sorted)) - :cancel {:msg (msg "trash " (if (empty? to-trash) "no cards" (enumerate-cards to-trash :sorted)) " and shuffle the stack") + :cancel {:msg (simple-msg + {:effect/type :trash-cards + :effect/count (count to-trash) + :effect/card-strs (sort-by get-title to-trash)} + :shuffle-stack) :effect (effect (trigger-event state side :searched-stack) - (shuffle! state side :deck))} + (shuffle! state side :deck))} :effect (effect (trigger-event state side :searched-stack) - (shuffle! state side :deck) - (runner-install state side (assoc eid :source card :source-type :runner-install) - target {:cost-bonus (- trash-cost)}))})] + (shuffle! state side :deck) + (runner-install state side (assoc eid :source card :source-type :runner-install) + target {:cost-bonus (- trash-cost)}))})] {:on-play {:prompt "Choose pieces of hardware and/or programs to trash" :change-in-game-state {:req (req (or (seq (:deck runner)) (seq (:hand runner))))} :choices {:card #(and (or (hardware? %) (program? %)) - (in-hand? %)) + (in-hand? %)) :max (effect (count (:hand runner)))} :cancel (ec 0 []) :async true :effect (effect (let [trash-cost (reduce + (keep :cost targets)) - to-trash targets] - (wait-for (trash-cards state side to-trash {:unpreventable true :cause-card card}) - (continue-ability state side (ec trash-cost to-trash) card nil))))}})) + to-trash targets] + (wait-for (trash-cards state side to-trash {:unpreventable true :cause-card card}) + (continue-ability state side (ec trash-cost to-trash) card nil))))}})) (defcard "Employee Strike" - {:on-play {:msg "disable the Corp's identity"} + {:on-play {:msg (simple-msg :disable-corp-id)} :static-abilities [{:type :disable-card :req (req (same-card? target (:identity corp))) :value true}]}) @@ -1352,9 +1513,11 @@ (map second) (keep #(get-card state (:ice (first %)))) (filter (complement rezzed?)))))} - :msg (msg "trash " (card-str state target)) + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str target}) :async true - :cancel {:msg "do nothing"} + :cancel {:msg (simple-msg :do-nothing)} :effect (effect (trash state side eid target {:cause-card card}))}}) (defcard "Encore" @@ -1363,7 +1526,7 @@ (some #{:rd} (:successful-run runner-reg)) (some #{:archives} (:successful-run runner-reg))) :rfg-instead-of-trashing true - :msg "take an additional turn after this one" + :msg (simple-msg :take-additional-turn) :effect (effect (swap! state update-in [:runner :extra-turns] (fnil inc 0)))}}) (defcard "Escher" @@ -1385,7 +1548,7 @@ :mandatory true :ability {:async true - :msg "rearrange installed ice" + :msg (simple-msg :rearrange-installed-ice) :effect (effect (continue-ability state side (es) card nil))}})]})) (defcard "Eureka!" @@ -1421,16 +1584,22 @@ (defcard "Exclusive Party" {:on-play - {:msg (msg "draw 1 card and gain " - (count (filter #(= (:title %) (:title card)) (:discard runner))) - " [Credits]") + {:msg + (simple-msg + {:effect/type :draw-cards + :effect/count 1} + {:effect/type :gain-credits + :effect/count (count (filter #(= (:title %) (:title card)) (:discard runner)))}) :async true :effect (effect (wait-for (draw state side 1) (gain-credits state side eid (count (filter #(= (:title %) (:title card)) (:discard runner))))))}}) (defcard "Executive Wiretaps" {:on-play - {:msg (msg "reveal " (enumerate-cards (:hand corp) :sorted) " from HQ") + {:msg (simple-msg + {:effect/type :reveal-cards-in-hq + :effect/titles (sort (mapv get-title (:hand corp))) + :effect/count (count (mapv get-title (:hand corp)))}) :change-in-game-state {:req (req (seq (:hand corp)))} :async true :effect (effect (reveal state side eid (:hand corp)))}}) @@ -1467,8 +1636,10 @@ state side {:choices {:card #(and (pos? (get-counters % :advancement)) (= (first (:server run)) (second (get-zone %))))} - :msg (msg "remove " (quantify c "advancement counter") - " from " (card-str state target)) + :msg (simple-msg + {:effect/type :remove-advancement-counters + :effect/card-str target + :effect/count c}) :async true :effect (effect (let [to-remove (min c (get-counters target :advancement))] (add-prop state :corp eid target :advance-counter (- to-remove))))} @@ -1479,7 +1650,10 @@ {:prompt "Choose a card to add to the grip" :change-in-game-state {:req (req (seq (:deck runner)))} :choices (effect (take 4 (:deck runner))) - :msg "look at the top 4 cards of the stack and add 1 of them to the grip" + :msg (simple-msg + {:effect/type :look-at-top-cards-add-to-grip + :effect/top-count 4 + :effect/add-count 1}) :effect (effect (move state side target :hand) (shuffle! state side :deck))}}) @@ -1490,7 +1664,9 @@ {:label "Trash card" :trash? true :cost [(->c :trash-from-hand 1)] - :msg (msg "trash " (:title target) " from HQ") + :msg (simple-msg + {:effect/type :trash-accessed-card + :effect/title (get-title target)}) :async true :effect (effect (trash state side eid (assoc target :seen true) {:accessed true :cause-card card}))}} :events [{:event :successful-run @@ -1498,7 +1674,11 @@ :req (req (= :hq (target-server context)) this-card-run) :async true - :msg "take 1 tag and access 1 additional card from HQ" + :msg (simple-msg + {:effect/type :take-tags + :effect/count 1} + {:effect/type :access-additional-in-hq + :effect/count 1}) :effect (effect (wait-for (gain-tags state :runner 1 {:unpreventable true}) (register-events @@ -1510,7 +1690,9 @@ {:on-play {:prompt "Choose one" :choices ["Agenda" "Asset" "Upgrade"] - :msg (msg "guess " target) + :msg (simple-msg + {:effect/type :guess + :effect/choice target}) :async true :effect (effect (continue-ability @@ -1526,7 +1708,9 @@ state :runner (when (and exposed (= chosen-type (:type target))) - {:msg "gain 5 [Credits]" + {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 5}) :async true :effect (effect (gain-credits state side eid 5))}) card nil)))}) @@ -1541,7 +1725,9 @@ :mandatory true :ability {:async true - :msg "force the Corp to trash the top card of R&D" + :msg (simple-msg + {:effect/type :force-corp-trash-top-of-rd + :effect/count 1}) :effect (effect (wait-for (mill state :corp :corp 1) (continue-ability @@ -1552,11 +1738,11 @@ :choices {:card #(and (in-hand? %) (same-card? :title card %)) :max n} - :msg (msg "reveal " - (quantify (count targets) "cop" "y" "ies") - " of itself," - " forcing the Corp to trash " (quantify (count targets) "additional card") - " from the top of R&D") + :msg (simple-msg + {:effect/type :reveal-copies-of-self + :effect/count (count targets)} + {:effect/type :force-corp-trash-additional-top-of-rd + :effect/count (count targets)}) :effect (effect (wait-for (reveal state :runner targets) (mill state :corp eid :corp (count targets))))}) @@ -1568,7 +1754,9 @@ :events [{:event :encounter-ice :automatic :bypass :req (req (< (get-in card [:special :bypass-count] 0) 2)) - :msg (msg "bypass " (:title (:ice context))) + :msg (simple-msg + {:effect/type :bypass-ice + :effect/card-str (:ice context)}) :effect (effect (bypass-ice state) (update! state side (update-in card [:special :bypass-count] (fnil inc 0))))} {:event :successful-run @@ -1586,7 +1774,9 @@ (defcard "Fisk Investment Seminar" {:on-play - {:msg "make each player draw 3 cards" + {:msg (simple-msg + {:effect/type :each-player-draws-cards + :effect/count 3}) :change-in-game-state {:req (req (or (seq (:deck runner)) (seq (:deck corp))))} :async true @@ -1609,7 +1799,12 @@ (str "Rez " (card-str state ice))) (str "Trash " (card-str state ice))] :async true - :msg (msg "force the Corp to " (decapitalize target)) + :msg (simple-msg + (if (str/starts-with? target "Rez") + {:effect/type :force-corp-rez + :effect/title (get-title ice)} + {:effect/type :force-corp-trash + :effect/title (get-title ice)})) :waiting-prompt true :effect (effect (if (str/starts-with? target "Rez") (rez state :corp eid ice) @@ -1625,7 +1820,11 @@ {:prompt "Choose an agenda to forfeit" :change-in-game-state {:req (req (:scored runner))} :choices (effect (:scored runner)) - :msg (msg "forfeit " (get-title card) " and give the Corp 1 bad publicity") + :msg (simple-msg + {:effect/type :forfeit + :effect/title (get-title target)} + {:effect/type :give-bad-publicity + :effect/count 1}) :async true :effect (effect (wait-for (forfeit state side (make-eid state eid) target {:msg false}) (gain-bad-publicity state :corp eid 1)))}}) @@ -1677,7 +1876,9 @@ (defcard "\"Freedom Through Equality\"" {:events [{:event :agenda-stolen - :msg (msg "add itself to [their] score area as an agenda worth 1 agenda point") + :msg (simple-msg + {:effect/type :add-self-to-score-area + :effect/value 1}) :effect (effect (as-agenda state :runner card 1))}]}) (defcard "Freelance Coding Contract" @@ -1686,15 +1887,21 @@ :card #(and (program? %) (in-hand? %))} :change-in-game-state {:req (req (seq (:hand runner)))} - :msg (msg "trash " (enumerate-cards targets :sorted) " and gain " - (* 2 (count targets)) " [Credits]") + :msg (simple-msg + {:effect/type :trash-cards + :effect/count (count targets) + :effect/card-strs targets} + {:effect/type :gain-credits + :effect/count (* 2 (count targets))}) :async true :effect (effect (wait-for (trash-cards state side targets {:unpreventable true :cause-card card}) (gain-credits state side eid (* 2 (count targets)))))}}) (defcard "Game Day" {:on-play - {:msg (msg "draw " (quantify (- (hand-size state :runner) (count (:hand runner))) "card")) + {:msg (simple-msg + {:effect/type :draw-cards + :effect/count (- (hand-size state :runner) (count (:hand runner)))}) :change-in-game-state {:req (req (pos? (- (hand-size state :runner) (count (:hand runner)))))} :async true :effect (effect (draw state side eid (- (hand-size state :runner) (count (:hand runner)))))}}) @@ -1717,24 +1924,36 @@ :all true :card #(and (corp? %) (in-discard? %))} - :msg (msg "move " - (let [seen (filter :seen targets) - m (count (remove :seen targets))] - (str (enumerate-cards seen) - (when (pos? m) - (str (when-not (empty? seen) " and ") - (quantify m "unseen card"))) - " into HQ, then trash 5 cards"))) + :msg (simple-msg + (let [seen (seq (filter :seen targets)) + unseen (count (remove :seen targets))] + (cond + (and seen (pos? unseen)) + {:effect/type :move-seen-unseen-into-hq + :effect/seen seen + :effect/unseen-cnt unseen} + seen + {:effect/type :move-seen-into-hq + :effect/seen seen} + (pos? unseen) + {:effect/type :move-unseen-into-hq + :effect/unseen-cnt unseen})) + {:effect/type :trash-n-cards + :effect/count 5}) :effect (effect (doseq [c targets] (move state side c :hand)) - (trash-cards state :corp eid (take 5 (shuffle (:hand (:corp @state)))) {:cause-card card}))}})]}) + (trash-cards state :corp eid (take 5 (shuffle (:hand (:corp @state)))) + {:cause-card card}))}})]}) (defcard "Government Investigations" {:flags {:prevent-secretly-spend (effect 2)}}) (defcard "Guinea Pig" {:on-play - {:msg "trash all cards in the grip and gain 10 [Credits]" + {:msg (simple-msg + :trash-all-cards-in-grip + {:effect/type :gain-credits + :effect/count 10}) :async true :effect (effect (wait-for (trash-cards state side (:hand runner) {:unpreventable true :cause-card card}) @@ -1748,7 +1967,10 @@ (defcard "Harmony AR Therapy" (letfn [(choose-end [to-shuffle] (let [to-shuffle (sort to-shuffle)] - {:msg (msg "shuffle " (quantify (count to-shuffle) "card") " back into the stack: " (enumerate-str to-shuffle)) + {:msg (simple-msg + {:effect/type :shuffle-cards-into-stack + :effect/count (count to-shuffle) + :effect/titles (mapv #(if (string? %) % (get-title %)) to-shuffle)}) :effect (effect (doseq [c-title to-shuffle] (let [c (some #(when (= (:title %) c-title) %) (:discard runner))] (move state side c :deck))) @@ -1805,7 +2027,9 @@ :events [{:event :run-ends :req (req (:successful target) this-card-run) - :msg "gain 12 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 12}) :async true :effect (effect (gain-credits state :runner eid 12))}]}) @@ -1814,7 +2038,10 @@ {:prompt "Choose a Connection" :change-in-game-state {:req (req (seq (:deck runner)))} :choices (effect (cancellable (filter #(has-subtype? % "Connection") (:deck runner)) :sorted)) - :msg (msg "add " (:title target) " from the stack to the grip and shuffle the stack") + :msg (simple-msg + {:effect/type :add-card-from-stack-to-grip + :effect/card-str target} + :shuffle-stack) :async true :cancel fail-to-find! :effect (effect (trigger-event state side :searched-stack) @@ -1837,7 +2064,11 @@ :events [{:event :successful-run :automatic :gain-credits :async true - :msg "gain 9 [Credits] and take 1 tag" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 9} + {:effect/type :take-tags + :effect/count 1}) :req (req (= :hq (target-server context)) this-card-run) :effect (effect (wait-for (gain-tags state :runner 1 {:suppress-checkpoint true}) @@ -1851,7 +2082,9 @@ :interactive (effect true) :async true :req (req (#{:meat :net} (:cause context))) - :msg "draw 3 cards" + :msg (simple-msg + {:effect/type :draw-cards + :effect/count 3}) :effect (effect (draw state :runner eid 3))}}) (defcard "Illumination" @@ -1897,13 +2130,16 @@ :prompt "Choose a piece of ice in Archives" :choices (effect (filter ice? (:discard corp))) :effect (effect (continue-ability - state side (let [ice target] + state side + (let [ice target] {:async true :prompt (msg "Choose a rezzed copy of " (:title ice) " to trash") :choices {:card #(and (ice? %) (rezzed? %) (same-card? :title % ice))} - :msg (msg "trash " (card-str state target)) + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str target}) :effect (effect (trash state side eid target {:cause-card card}))}) card nil))}]}) @@ -1918,10 +2154,15 @@ :prompt "Choose one" :waiting-prompt true :choices ["Draw 1 card" "Gain 1 [Credits]"] - :msg (msg (decapitalize target)) + :msg (simple-msg + (if (= target "Draw 1 card") + {:effect/type :draw-cards + :effect/count 1} + {:effect/type :gain-credits + :effect/count 1})) :effect (effect (if (= target "Draw 1 card") - (draw state side eid 1) - (gain-credits state side eid 1)))}]}) + (draw state side eid 1) + (gain-credits state side eid 1)))}]}) (defcard "Independent Thinking" (letfn [(cards-to-draw [targets] @@ -1933,11 +2174,15 @@ :choices {:max 5 :card #(and (installed? %) (runner? %))} - :msg (msg "trash " (enumerate-cards targets) - " and draw " (quantify (cards-to-draw targets) "card")) + :msg (simple-msg + {:effect/type :trash-cards + :effect/count (count targets) + :effect/card-strs targets} + {:effect/type :draw-cards + :effect/count (cards-to-draw targets)}) :async true :effect (effect (wait-for (trash-cards state side targets {:cause-card card}) - (draw state :runner eid (cards-to-draw targets))))}})) + (draw state :runner eid (cards-to-draw targets))))}})) (defcard "Indexing" {:makes-run true @@ -1946,7 +2191,9 @@ {:target-server :rd :this-card-run true :ability - {:msg "rearrange the top 5 cards of R&D" + {:msg (simple-msg + {:effect/type :rearrange-top-n-cards-rd + :effect/count 5}) :waiting-prompt true :async true :effect (effect @@ -2065,7 +2312,9 @@ :automatic :bypass :req (req (first-run-event? state side :encounter-ice) this-card-is-run-source) - :msg (msg "bypass " (:title (:ice context))) + :msg (simple-msg + {:effect/type :bypass-ice + :effect/card-str (:ice context)}) :effect (effect (bypass-ice state))}]}) (defcard "Insight" @@ -2090,7 +2339,7 @@ ((constantly false) (toast state :corp "Cannot rez non-ice on the Runner's turn due to Interdiction")) true))))] - {:on-play {:msg "prevent the Corp from rezzing non-ice cards on the Runner's turn" + {:on-play {:msg (simple-msg :prevent-corp-rez-non-ice-on-runner-turn) :effect ab} :events [{:event :runner-turn-begins :silent true @@ -2100,47 +2349,58 @@ (defcard "Into the Depths" ;; note - Into the Depths specifies "each time you passed an ICE". This means, unlike bravado, ;; passing the same ice multiple times (ie thimblerig) counts. - (let [all [{:msg "gain 4 [Credits]" + (let [all [{:label "Gain 4 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 4}) :async true :effect (effect (gain-credits state side eid 4))} - {:msg "install a program from the stack" + {:label "install a program from the stack" + :msg (simple-msg :install-program-from-stack) :async true :req (req (not (install-locked? state side))) :effect (effect (continue-ability - state side {:prompt "Choose a program to install" - :msg (msg (if (= target "Done") - "shuffle the stack" - (str "install " (:title target) " from the stack"))) - :choices (effect (concat - (->> (:deck runner) - (filter - #(and (program? %) - (runner-can-pay-and-install? state side (assoc eid :source card) %))) - (sort-by :title) - (seq)) - ["Done"])) + state side + {:prompt "Choose a program to install" + :choices (effect + (concat + (->> (:deck runner) + (filter + #(and (program? %) + (runner-can-pay-and-install? state side (assoc eid :source card) %))) + (sort-by :title) + (seq)) + ["Done"])) + :msg (simple-msg + (if (= target "Done") + :shuffle-stack + {:effect/type :install-from-stack + :effect/title (:title target)})) :async true - :effect (effect (trigger-event state side :searched-stack) - (shuffle! state side :deck) - (if (= target "Done") - (effect-completed state side eid) - (runner-install state side (assoc eid :source card :source-type :runner-install) target {:msg-keys {:install-source card - :display-origin true}})))} + :effect (effect + (trigger-event state side :searched-stack) + (shuffle! state side :deck) + (if (= target "Done") + (effect-completed state side eid) + (runner-install state side (assoc eid :source card :source-type :runner-install) + target {:msg-keys {:install-source card + :display-origin true}})))} card nil))} - {:async true - :effect (effect (continue-ability state side (charge-ability state side) card nil)) - :msg "charge a card"}] + {:label "charge a card" + :async true + :effect (effect (continue-ability state side (charge-ability state side) card nil))}] choice (fn choice [abis rem] {:prompt (str "Choose an ability to resolve (" rem " remaining)") :waiting-prompt true - :choices (map #(capitalize (:msg %)) abis) + :choices (mapv #(capitalize (:label %)) abis) :async true - :effect (effect (let [chosen (some #(when (= target (capitalize (:msg %))) %) abis)] - (wait-for - (resolve-ability state side chosen card nil) - (if (< 1 rem) - (continue-ability state side (choice (remove-once #(= % chosen) abis) (dec rem)) card nil) - (effect-completed state side eid)))))})] + :effect (effect + (let [chosen (some #(when (= target (capitalize (:label %))) %) abis)] + (wait-for + (resolve-ability state side chosen card nil) + (if (< 1 rem) + (continue-ability state side (choice (remove-once #(= % chosen) abis) (dec rem)) card nil) + (effect-completed state side eid)))))})] {:makes-run true :on-play (run-any-server-ability) :events [{:event :successful-run @@ -2157,12 +2417,14 @@ (defcard "Isolation" {:on-play {:additional-cost [(->c :resource 1)] - :msg "gain 7 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 7}) :async true :effect (effect (gain-credits state side eid 7))}}) (defcard "Itinerant Protesters" - {:on-play {:msg "reduce the Corp's maximum hand size by 1 for each bad publicity"} + {:on-play {:msg (simple-msg :reduce-corp-max-hand-size-bad-publicity)} :static-abilities [(corp-hand-size+ (effect (- (count-bad-pub state))))]}) (defcard "Jailbreak" @@ -2172,7 +2434,9 @@ :automatic :draw-cards :silent true :async true - :msg "draw 1 card" + :msg (simple-msg + {:effect/type :draw-cards + :effect/count 1}) :req (req (#{:hq :rd} (target-server context)) this-card-run) :effect (effect (register-events @@ -2189,7 +2453,9 @@ :async true :req (req (= :rd (target-server context)) this-card-run) - :msg "draw 5 cards" + :msg (simple-msg + {:effect/type :draw-cards + :effect/count 5}) :effect (effect (draw state side eid 5))}]}) (defcard "Katorga Breakout" @@ -2202,7 +2468,9 @@ :prompt "Choose 1 card to add to the grip" :waiting-prompt true :choices (effect (cancellable (:discard runner) :sorted)) - :msg (msg "add " (:title target) " to the grip") + :msg (simple-msg + {:effect/type :add-card-to-grip + :effect/title (:title target)}) :effect (effect (move state side target :hand))}]}) (defcard "Khusyuk" @@ -2298,14 +2566,20 @@ :waiting-prompt true :choices {:req (req (some #(same-card? % target) valid-ice))} :cancel {:display-side :runner - :msg "give the Corp 1 bad publicity" + :msg (simple-msg + {:effect/type :give-bad-publicity + :effect/count 1}) :async true :effect (effect (gain-bad-publicity state :runner eid 1))} - :msg (msg "derez " (card-str state target)) + :msg (simple-msg + {:effect/type :derez-card + :effect/card-str target}) :display-side :corp :async true :effect (effect (derez state side eid target {:no-msg true}))} - {:msg "give the Corp 1 bad publicity" + {:msg (simple-msg + {:effect/type :give-bad-publicity + :effect/count 1}) :async true :effect (effect (gain-bad-publicity state :runner eid 1))}) card nil)))}]})) @@ -2316,19 +2590,24 @@ :prompt "Choose a server" :change-in-game-state {:req (req (some ice? (all-installed state :corp)))} :choices (effect servers) - :msg (msg "force the Corp to trash a piece of ice protecting " target) + :msg (simple-msg + {:effect/type :force-trash-installed-ice + :effect/server target}) :async true :effect (effect (continue-ability - state side (let [serv (second (server->zone state target))] + state :corp + (let [cause card + serv (second (server->zone state target))] {:player :corp :async true :prompt (msg "Choose a piece of ice in " target " to trash") :choices {:card #(and (ice? %) (= serv (second (get-zone %))))} - :effect (effect (system-msg state side (str "trashes " (card-str state target))) - (trash state :corp eid target {:cause-card card}))}) - card nil))}}) + :effect (effect (system-msg state side {:msg/type :msg-trash-card + :msg/card-str target}) + (trash state :corp eid target {:cause-card cause}))}) + nil nil))}}) (defcard "Labor Rights" {:on-play @@ -2365,14 +2644,18 @@ (draw state :runner eid 1))} {:async true :effect (effect - (do state side (system-msg state :runner "shuffles the stack and draws 1 card") - (shuffle! state :runner :deck) - (draw state :runner eid 1)))}) + (system-msg state :runner "shuffles the stack and draws 1 card") + (shuffle! state :runner :deck) + (draw state :runner eid 1))}) card nil)))))}}) (defcard "Lawyer Up" {:on-play - {:msg "remove 2 tags and draw 3 cards" + {:msg (simple-msg + {:effect/type :remove-tags + :effect/count 2} + {:effect/type :draw-cards + :effect/count 3}) :change-in-game-state {:req (req (or tagged (seq (:deck runner))))} :async true :effect (effect (wait-for (lose-tags state side 2) @@ -2384,13 +2667,16 @@ {:prompt "Choose a server" :change-in-game-state {:req (req (seq runnable-servers))} :choices (effect runnable-servers) - :msg (msg "make a run on " target - (when (<= (count (filter program? (all-active-installed state :runner))) 3) - ", giving +2 strength to all icebreakers")) + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server target} + (when (<= (count (filter program? (all-active-installed state :runner))) 3) + {:effect/type :give-strength-all-icebreakers-during-run + :effect/bonus 2})) :async true :effect (effect (when (<= (count (filter program? (all-active-installed state :runner))) 3) - (pump-all-icebreakers state side 2 :end-of-run)) - (make-run state side eid target card))}}) + (pump-all-icebreakers state side 2 :end-of-run)) + (make-run state side eid target card))}}) (defcard "Leave No Trace" {:makes-run true @@ -2422,10 +2708,12 @@ :prompt "Take 2 bad publicity?" :waiting-prompt true :yes-ability {:player :corp - :msg "takes 2 bad publicity" + :msg (simple-msg + {:effect/type :force-take-bad-publicity + :effect/count 2}) :effect (effect (gain-bad-publicity state :corp 2))} :no-ability {:player :runner - :msg "is immune to damage until the beginning of the Runner's next turn" + :msg (simple-msg :prevent-damage-until-next-turn) :effect (effect (register-lingering-effect state side card @@ -2441,14 +2729,21 @@ :card card :condition :floating :req (req (preventable? context)) - :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :msg (simple-msg + (let [t (case (damage-type state) + :meat :prevent-meat-damage + (:brain :core) :prevent-core-damage + :net :prevent-net-damage)] + {:effect/type t + :effect/count (:remaining context)})) :effect (effect (prevent-damage state side eid :all))}}}))}}}}) (defcard "Levy AR Lab Access" {:on-play - {:msg (msg (if (not (zone-locked? state :runner :discard)) - "shuffle the grip and heap into the stack and draw 5 cards" - "shuffle the grip into the stack and draw 5 cards")) + {:msg (simple-msg + (if (zone-locked? state :runner :discard) + :shuffle-grip-into-stack + :shuffle-grip-and-heap-into-stack)) :rfg-instead-of-trashing true :async true :effect (effect (shuffle-into-deck state side :hand :discard) @@ -2458,21 +2753,27 @@ (letfn [(remove-tag-opt [x] {:option (str "Remove " (quantify x "tag")) :req (req (>= (count-tags state) x)) - :ability {:msg (str "remove " (quantify x "tag")) + :ability {:msg (simple-msg + {:effect/type :remove-tags + :effect/count x}) :async true :effect (effect (lose-tags state side eid x))}})] {:on-play (choose-one-helper - {:change-in-game-state {:req (req (or (seq (:deck runner)) tagged))}} - [{:option "Draw 4 cards" - :ability {:msg "draw 4 cards" - :async true - :effect (effect (draw state side eid 4))}} - {:option "Remove up to 2 tags" - :ability (choose-one-helper (vec (map remove-tag-opt [0 1 2])))}])})) + {:change-in-game-state {:req (req (or (seq (:deck runner)) tagged))}} + [{:option "Draw 4 cards" + :ability {:msg (simple-msg + {:effect/type :draw-cards + :effect/count 4}) + :async true + :effect (effect (draw state side eid 4))}} + {:option "Remove up to 2 tags" + :ability (choose-one-helper (mapv remove-tag-opt [0 1 2]))}])})) (defcard "Lucky Find" {:on-play - {:msg "gain 9 [Credits]" + {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 9}) :async true :effect (effect (gain-credits state side eid 9))}}) @@ -2484,11 +2785,11 @@ :req (req this-card-run) :effect (effect (if (:did-steal target) (do (system-msg state :runner - (str "adds Mad Dash to [their] score area as an agenda worth 1 agenda point")) + "adds Mad Dash to [their] score area as an agenda worth 1 agenda point") (as-agenda state :runner (get-card state card) 1) (effect-completed state side eid)) (do (system-msg state :runner - (str "suffers 1 meat damage from Mad Dash")) + "suffers 1 meat damage from Mad Dash") (damage state side eid :meat 1 {:card card}))))}]}) (defcard "Maintenance Access" @@ -2496,7 +2797,7 @@ :events [{:event :pre-approach-server :unregister-once-resolved true :interactive (effect true) - :msg "change the attacked server to HQ" + :msg (simple-msg :redirect-run-to-hq) :req (req (= :archives (-> run :server first))) :effect (effect (swap! state assoc-in [:run :server] [:hq]))}] :on-play (run-server-ability :archives)}) @@ -2522,7 +2823,9 @@ (entrance-trash cards)) card nil))))})] {:on-play - {:msg "look at and trash or rearrange the top 6 cards of the stack" + {:msg (simple-msg + {:effect/type :trash-or-rearrange-top-of-stack + :effect/count 6}) :change-in-game-state {:req (req (seq (:deck runner)))} :async true :waiting-prompt true @@ -2548,13 +2851,19 @@ (unregister-events state side card)))}]}) (defcard "Mars for Martians" - (letfn [(count-clan [state] (count (filter #(and (has-subtype? % "Clan") (resource? %)) - (all-active-installed state :runner))))] + (letfn [(count-clan [state] + (->> (all-active-installed state :runner) + (filter #(and (has-subtype? % "Clan") (resource? %))) + (count)))] {:on-play - {:msg (msg "draw " (quantify (count-clan state) "card") " and gain " (count-tags state) " [Credits]") + {:msg (simple-msg + {:effect/type :draw-cards + :effect/count (count-clan state)} + {:effect/type :gain-credits + :effect/count (count-tags state)}) :async true :effect (effect (wait-for (draw state side (count-clan state)) - (gain-credits state side eid (count-tags state))))}})) + (gain-credits state side eid (count-tags state))))}})) (defcard "Mass Install" (letfn [(mhelper [n] @@ -2579,58 +2888,74 @@ (letfn [(credit-gain-abi [type] {:choices {:max (effect (count (:hand runner))) :card #(and (runner? %) - (in-hand? %) - (has-subtype? % type))} + (in-hand? %) + (has-subtype? % type))} :prompt (msg "Choose any number of " (decapitalize type) " resources to reveal") - :msg (msg "reveal " (enumerate-cards targets :sorted) " from the Grip and gain " (count targets) " [Credits]") + :msg (simple-msg + {:effect/type :reveal-cards-in-grip + :effect/count (count targets) + :effect/titles (sort-by get-title targets)} + {:effect/type :gain-credits + :effect/count (count targets)}) :async true :effect (effect (wait-for - (reveal state side targets) - (gain-credits state side eid (* 1 (count targets)))))}) + (reveal state side targets) + (gain-credits state side eid (* 1 (count targets)))))}) (tutor-abi [type] {:prompt (str "Choose a " (decapitalize type) " resource") :choices (effect (cancellable (filter #(has-subtype? % type) - (:deck runner)) :sorted)) + (:deck runner)) :sorted)) :cancel {:async true - :msg "shuffle the stack" + :msg (simple-msg :shuffle-stack) :effect (effect (trigger-event state side :searched-stack) - (shuffle! state side :deck) - (continue-ability state side (credit-gain-abi type) card nil))} - :msg (msg "add " (:title target) " from the stack to the grip and shuffle the stack") + (shuffle! state side :deck) + (continue-ability state side (credit-gain-abi type) card nil))} + :msg (simple-msg + {:effect/type :add-card-from-stack-to-grip + :effect/card-str target} + {:effect/type :shuffle-stack}) :async true :effect (effect (trigger-event state side :searched-stack) - (move state side target :hand) - (shuffle! state side :deck) - (continue-ability state side (credit-gain-abi type) card nil))})] + (move state side target :hand) + (shuffle! state side :deck) + (continue-ability state side (credit-gain-abi type) card nil))})] {:on-play {:prompt "Choose one" :async true :waiting-prompt true :choices ["Connection" "Virtual"] :effect (effect (let [choice target] - (continue-ability - state side - {:optional - {:prompt (str "Search the stack for a " (decapitalize choice) " resource?") - :yes-ability - {:async true - :msg (msg "search the stack for a " (decapitalize choice) " resource") - :effect (effect (continue-ability state side (tutor-abi choice) card nil))} - :no-ability - {:async true - :effect (effect (continue-ability state side (credit-gain-abi choice) card nil))}}} - card nil)))}})) + (continue-ability + state side + {:optional + {:prompt (str "Search the stack for a " (decapitalize choice) " resource?") + :yes-ability + {:async true + :msg (simple-msg + (if (= "Connection" choice) + :search-stack-for-connection-resource + :search-stack-for-virtual-resource)) + :effect (effect (continue-ability state side (tutor-abi choice) card nil))} + :no-ability + {:async true + :effect (effect (continue-ability state side (credit-gain-abi choice) card nil))}}} + card nil)))}})) (defcard "Mining Accident" {:on-play {:req (req (some #{:hq :rd :archives} (:successful-run runner-reg))) :rfg-instead-of-trashing true - :msg (msg "force the corp to " (decapitalize target)) :waiting-prompt true :player :corp :prompt "Choose one" :choices (effect [(when (can-pay? state :corp eid card nil (->c :credit 5)) "Pay 5 [Credits]") "Take 1 bad publicity"]) + :msg (simple-msg + (if (= "Pay 5 [Credits]" target) + {:effect/type :force-corp-pay-credits + :effect/credits 5} + {:effect/type :force-take-bad-publicity + :effect/count 1})) :async true :effect (effect (if (= target "Pay 5 [Credits]") (wait-for [{:keys [msg]} (pay state :corp (make-eid state eid) card (->c :credit 5))] @@ -2651,7 +2976,9 @@ :automatic :gain-credits :req (req (get-in card [:special :run-again]) (= :rd (target-server context))) - :msg "gain 4 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 4}) :async true :effect (effect (gain-credits state side eid 4))} {:event :run-ends @@ -2679,10 +3006,14 @@ (defcard "Moshing" {:on-play {:additional-cost [(->c :trash-from-hand 3)] - :msg "draw 3 cards and gain 3 [Credits]" + :msg (simple-msg + {:effect/type :draw-cards + :effect/count 3} + {:effect/type :gain-credits + :effect/count 3}) :async true :effect (effect (wait-for (draw state side 3) - (gain-credits state side eid 3)))}}) + (gain-credits state side eid 3)))}}) (defcard "Mutual Favor" {:on-play @@ -2690,7 +3021,10 @@ :change-in-game-state {:req (req (seq (:deck runner)))} :choices (effect (cancellable (filter #(has-subtype? % "Icebreaker") (:deck runner)) :sorted)) :cancel fail-to-find! - :msg (msg "add " (:title target) " from the stack to the grip and shuffle the stack") + :msg (simple-msg + {:effect/type :add-card-from-stack-to-grip + :effect/card-str target} + {:effect/type :shuffle-stack}) :async true :effect (effect (trigger-event state side :searched-stack) (continue-ability @@ -2701,7 +3035,9 @@ {:prompt (str "Install " (:title icebreaker) "?") :yes-ability {:async true - :msg (msg " install " (:title icebreaker)) + :msg (simple-msg + {:effect/type :runner-install-card + :effect/title icebreaker}) :effect (effect (runner-install state side (assoc eid :source card :source-type :runner-install) icebreaker nil) (shuffle! state side :deck))} :no-ability @@ -2719,9 +3055,11 @@ (defcard "Networking" {:on-play {:async true - :msg (msg (if tagged - "remove 1 tag" - "do nothing")) + :msg (simple-msg + (if tagged + {:effect/type :remove-tags + :effect/count 1} + :do-nothing)) :effect (effect (wait-for (lose-tags state side 1) (continue-ability state side @@ -2730,7 +3068,7 @@ :req (req (can-pay? state side eid card nil [(->c :credit 1)])) :yes-ability {:cost [(->c :credit 1)] - :msg "add itself to the Grip" + :msg (simple-msg :add-self-to-grip) :effect (effect (move state side card :hand))}}} card nil)))}}) @@ -2739,7 +3077,9 @@ {:req (req (some #{:hq} (:successful-run runner-reg)) (some #{:rd} (:successful-run runner-reg)) (some #{:archives} (:successful-run runner-reg))) - :msg (msg "add itself to [their] score area as an agenda worth 1 agenda point") + :msg (simple-msg + {:effect/type :add-self-to-score-area + :effect/value 1}) :effect (effect (as-agenda state :runner card 1))}}) (defcard "Office Supplies" @@ -2748,7 +3088,12 @@ :prompt "Choose one" :waiting-prompt true :choices ["Gain 4 [Credits]" "Draw 4 cards"] - :msg (msg (decapitalize target)) + :msg (simple-msg + (if (= target "Gain 4 [Credits]") + {:effect/type :gain-credits + :effect/count 4} + {:effect/type :draw-cards + :effect/count 4})) :async true :effect (effect (if (= target "Gain 4 [Credits]") (gain-credits state :runner eid 4) @@ -2774,8 +3119,13 @@ (defcard "Out of the Ashes" (let [ashes-run {:prompt "Choose a server" :choices (effect runnable-servers) + :msg (simple-msg + {:effect/type :rfg-to-make-a-run-on + :effect/title card + :effect/server target}) :async true - :effect (effect (make-run state side eid target card))} + :effect (effect (move state side card :rfg) + (make-run state side eid target card))} ashes-recur (fn ashes-recur [] {:optional {:req (req (not (zone-locked? state :runner :discard))) @@ -2784,15 +3134,13 @@ " available)")) :yes-ability {:async true - :msg "removes Out of the Ashes from the game to make a run" :effect - (effect (move state side card :rfg) - (wait-for (resolve-ability state side (make-eid state eid) ashes-run card nil) - (if-let [next-out-of-ashes (some #(when (and (= "Out of the Ashes" (:title %)) - (not (same-card? card %))) %) - (:discard runner))] - (continue-ability state side (ashes-recur) (get-card state next-out-of-ashes) nil) - (effect-completed state side eid))))}}})] + (effect (wait-for (resolve-ability state side (make-eid state eid) ashes-run card nil) + (if-let [next-out-of-ashes (some #(when (and (= "Out of the Ashes" (:title %)) + (not (same-card? card %))) %) + (:discard runner))] + (continue-ability state side (ashes-recur) (get-card state next-out-of-ashes) nil) + (effect-completed state side eid))))}}})] {:makes-run true :on-play (run-any-server-ability) :events [{:event :runner-turn-begins @@ -2820,7 +3168,9 @@ (defcard "Paper Tripping" {:on-play - {:msg "remove all tags" + {:msg (simple-msg + {:effect/type :remove-all-tags + :effect/count (count-tags state)}) :change-in-game-state {:req (req tagged)} :async true :effect (effect (lose-tags state side eid :all))}}) @@ -2828,7 +3178,11 @@ (defcard "Peace in Our Time" {:on-play {:req (req (not (:scored-agenda corp-reg-last))) - :msg "gain 10 [Credits]. The Corp gains 5 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 10} + {:effect/type :corp-gains-credits + :effect/count 5}) :async true :effect (effect (wait-for (gain-credits state :runner 10) (register-turn-flag! state side card :can-run nil) @@ -2877,7 +3231,11 @@ (can-pay? state side (assoc eid :source card :source-type :play) % nil [(->c :credit (play-cost state side %))])) (:deck runner)))) - :msg (msg "play " (:title target)) + :msg (simple-msg + :search-stack-for-run-event + :shuffle-stack + {:effect/type :play-card-no-additional-costs + :effect/title target}) :async true :effect (effect (trigger-event state side :searched-stack) (shuffle! state side :deck) @@ -2892,7 +3250,7 @@ :events [{:event :purge :condition :hosted :async true - :msg "trash itself" + :msg (simple-msg :trash-self) :effect (effect (wait-for (trash state :runner card {:cause :purge :cause-card card}) (update-all-agenda-points state side) @@ -2904,7 +3262,9 @@ :ability {:prompt (msg "Choose an agenda to host " (:title card) " on") :choices {:req (req (in-corp-scored? state side target))} - :msg (msg "host itself on " (:title target) " as a hosted condition counter") + :msg (simple-msg + {:effect/type :host-self-as-condition-counter + :effect/card-str target}) :async true :effect (effect (wait-for (install-as-condition-counter state side (make-eid state eid) card target) (update-all-agenda-points state side) @@ -2912,7 +3272,9 @@ (defcard "Populist Rally" {:on-play {:req (req (seq (filter #(has-subtype? % "Seedy") (all-active-installed state :runner)))) - :msg (msg "give the Corp 1 fewer [Click] to spend on [corp-pronoun] next turn") + :msg (simple-msg + {:effect/type :reduce-corp-click-next-turn + :effect/count 1}) :effect (effect (lose state :corp :click-per-turn 1))} :events [{:event :corp-turn-ends :duration :until-corp-turn-ends @@ -2921,7 +3283,9 @@ (defcard "Power Nap" {:on-play {:async true - :msg (msg "gain " (+ 2 (count (filter #(has-subtype? % "Double") (:discard runner)))) " [Credits]") + :msg (simple-msg + {:effect/type :gain-credits + :effect/count (+ 2 (count (filter #(has-subtype? % "Double") (:discard runner))))}) :effect (effect (gain-credits state side eid (+ 2 (count (filter #(has-subtype? % "Double") (:discard runner))))))}}) @@ -2931,7 +3295,9 @@ (first-event? state side :access #(agenda? (:accessed-card (first %))))) :duration :end-of-turn :unregister-once-resolved true - :msg "gain 7 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 7}) :async true :effect (effect (gain-credits state side eid 7))}]}) @@ -2955,14 +3321,18 @@ :yes-ability {:async true :cost [(->c :trash-installed (get-strength ice))] - :msg (msg "trash " (card-str state ice)) + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str ice}) :effect (effect (trash state side eid ice {:cause-card card}))}}} {:optional {:prompt (str "Trash " (:title ice) "?") :once :per-run :yes-ability {:async true - :msg (msg "trash " (card-str state ice)) + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str ice}) :effect (effect (trash state side eid ice {:cause-card card}))}}})) card nil))}]}) @@ -2989,8 +3359,9 @@ (effect-completed state side eid) (do (update! state side (assoc-in card [:special :maybe-a-bonus-tag] true)) (wait-for - (runner-install state side (make-eid state (assoc eid :source card :source-type :runner-install)) target {:msg-keys {:install-source card - :display-origin true}}) + (runner-install state side (make-eid state (assoc eid :source card :source-type :runner-install)) + target {:msg-keys {:install-source card + :display-origin true}}) (update! state side (dissoc-in card [:special :maybe-a-bonus-tag])) (effect-completed state side eid)))))} install-resource-from-heap @@ -3014,9 +3385,10 @@ (effect-completed state side eid) (do (update! state side (assoc-in card [:special :maybe-a-bonus-tag] true)) (wait-for - (runner-install state side (make-eid state (assoc eid :source card :source-type :runner-install)) target {:cost-bonus -2 - :msg-keys {:install-source card - :display-origin true}}) + (runner-install state side (make-eid state (assoc eid :source card :source-type :runner-install)) + target {:cost-bonus -2 + :msg-keys {:install-source card + :display-origin true}}) (update! state side (dissoc-in card [:special :maybe-a-bonus-tag])) (effect-completed state side eid)))))}] {:makes-run true @@ -3026,7 +3398,9 @@ :this-card-run true :mandatory true :ability {:async true - :msg "take 1 tag" + :msg (simple-msg + {:effect/type :take-tags + :effect/count 1}) :effect (effect (register-pending-event state :runner-gain-tag card install-resource-from-heap) @@ -3038,7 +3412,11 @@ (defcard "Process Automation" {:on-play - {:msg "gain 2 [Credits] and draw 1 card" + {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 2} + {:effect/type :draw-cards + :effect/count 1}) :async true :effect (effect (wait-for (gain-credits state side 2) (draw state side eid 1)))}}) @@ -3054,8 +3432,8 @@ (wait-for (lose-credits state :runner (make-eid state eid) spent) (system-msg state :runner (str "spends " spent " [Credit]")) - (system-msg state :corp (str (if correct-guess " " " in") - "correctly guesses " (decapitalize target))) + (system-msg state :corp (str (if correct-guess "correctly" "incorrectly") + " guesses " (decapitalize target))) (wait-for (trigger-event-simult state side :reveal-spent-credits nil {:runner-credits spent}) (if correct-guess @@ -3082,9 +3460,11 @@ :on-play {:prompt "Choose a server" :change-in-game-state {:req (req (seq runnable-servers))} :choices (effect runnable-servers) - :msg (msg (if (<= (count (:hand runner)) 2) - "make a run, and give +2 strength to installed icebreakers" - "make a run")) + :msg (simple-msg + :make-a-run + (when (<= (count (:hand runner)) 2) + {:effect/type :give-strength-all-icebreakers-during-run + :effect/bonus 2})) :async true :effect (effect (when (<= (count (:hand runner)) 2) (pump-all-icebreakers state side 2 :end-of-run)) @@ -3104,7 +3484,12 @@ {:choices {:card #(and (is-remote? (second (get-zone %))) (= (last (get-zone %)) :content) (not (:rezzed %)))} - :msg (msg "place " (quantify c "advancement counter") " on " (card-str state target) " and gain " (* 2 c) " [Credits]") + :msg (simple-msg + {:effect/type :place-n-advancement-counters + :effect/count c + :effect/card-str target} + {:effect/type :gain-credits + :effect/count (* c 2)}) :async true :effect (effect (wait-for (gain-credits state side (* 2 c)) @@ -3125,7 +3510,9 @@ (some #{:archives} (:successful-run runner-reg))) :change-in-game-state {:req (req (some (complement ice?) (all-installed state :corp)))} :choices {:card installed?} - :msg (msg "access " (:title target)) + :msg (simple-msg + {:effect/type :access-card + :effect/card-str target}) :async true :effect (effect (access-card state side eid target))}}) @@ -3143,9 +3530,12 @@ :effect (effect (let [cards-to-draw (get-counters (get-card state card) :power)] (continue-ability state side - {:msg (msg (if (pos? cards-to-draw) - (str "draw " (quantify cards-to-draw "card") " and gain 3 [Credits]") - "gain 3 [Credits]")) + {:msg (simple-msg + (when (pos? cards-to-draw) + {:effect/type :draw-cards + :effect/count cards-to-draw}) + {:effect/type :gain-credits + :effect/count 3}) :async true :effect (effect (if (pos? cards-to-draw) (wait-for (draw state side cards-to-draw) @@ -3157,19 +3547,20 @@ {:on-play {:prompt "Choose an identity" :rfg-instead-of-trashing true - :choices (effect (let [is-draft-id? #(.startsWith (:code %) "00") + :choices (effect (let [is-draft-id? #(str/starts-with? (:code %) "00") runner-identity (:identity runner) format (:format @state) - is-swappable #(and (= "Identity" (:type %)) - (= "Runner" (:side %)) - (= (:faction runner-identity) (:faction %)) + is-swappable #(and (identity? %) + (runner? %) + (same-faction? runner-identity %) (not (is-draft-id? %)) - (not= (:title runner-identity) (:title %)) - (or (#{:casual :quick-draft :preconstructed} format) - (legal? format :legal %))) + (not= (get-title runner-identity) (get-title %)) + (legal? format :legal %)) swappable-ids (filter is-swappable (server-cards))] (sort-by :title swappable-ids))) - :msg "change identities" + :msg (simple-msg + {:effect/type :change-identity + :effect/title (assoc target :cid -1)}) :effect (effect (let [old-runner-identity (:identity runner)] ;; Handle hosted cards (Ayla) - Part 1 (doseq [c (:hosted old-runner-identity)] @@ -3262,7 +3653,9 @@ :waiting-prompt true :choices {:card #(and (installed? %) (corp? %))} - :msg (msg "add " (card-str state target) " to HQ") + :msg (simple-msg + {:effect/type :add-card-to-hq + :effect/card-str target}) :cancel (opt-run) :effect (effect (move state :corp target :hand) (continue-ability state side (opt-run) card nil))}})) @@ -3275,8 +3668,10 @@ (ice? %)) :max 2 :all true} - :msg (msg "swap the positions of " (card-str state (first targets)) - " and " (card-str state (second targets))) + :msg (simple-msg + {:effect/type :swap-two-ice-positions + :effect/card-str (first targets) + :effect/card-str2 (second targets)}) :effect (effect (swap-ice state side (first targets) (second targets)))}}) (defcard "Retrieval Run" @@ -3301,7 +3696,10 @@ {:waiting-prompt true :prompt "Choose a piece of ice to bypass" :choices {:card ice?} - :msg (msg "make a run and bypass " (card-str state target)) + :msg (simple-msg + :make-a-run + {:effect/type :bypass-ice + :effect/card-str target}) :async true :effect (effect (register-events state side card @@ -3309,7 +3707,9 @@ [{:event :encounter-ice :automatic :bypass :req (req (same-card? target-ice (:ice context))) - :msg (msg "bypass " (:title (:ice context))) + :msg (simple-msg + {:effect/type :bypass-ice + :effect/card-str (:ice context)}) :effect (effect (bypass-ice state))}])) (make-run state side eid (second (get-zone target)) card))}) (corp-choice [choices spent] @@ -3321,7 +3721,7 @@ :effect (effect (wait-for (lose-credits state :runner (make-eid state eid) spent) (system-msg state :runner (str "spends " spent " [Credit]")) - (system-msg state :corp (str " guesses " target " [Credit]")) + (system-msg state :corp (str "guesses " target " [Credit]")) (wait-for (trigger-event-simult state side :reveal-spent-credits nil {:runner-credits spent}) (if (not= spent (str->int target)) (continue-ability state :runner (choose-ice) card nil) @@ -3360,9 +3760,12 @@ {:prompt (str "Charge " (:title rig-target) "?") :req (req (can-charge state side rig-target)) :yes-ability - {:async true - :effect (effect (charge-card state side eid rig-target)) - :msg (msg "charge " (:title rig-target))}}} + {:msg (simple-msg + {:effect/type :charge-card + :effect/card-str rig-target + :effect/count 1}) + :async true + :effect (effect (charge-card state side eid rig-target))}}} card nil)))}}) (defcard "Rip Deal" @@ -3386,12 +3789,13 @@ :show-discard true :prompt (str "Choose " (quantify cards-to-move "card") " to add from the heap to the grip") - :msg (msg "add " (enumerate-cards targets :sorted) - " from the heap to the grip") :choices {:max cards-to-move :all true :card #(and (runner? %) (in-discard? %))} + :msg (simple-msg + {:effect/type :add-cards-from-heap-to-grip + :effect/titles (sort-by get-title targets)}) :effect (effect (doseq [c targets] (move state side c :hand)) (swap! state assoc-in [:run :prevent-hand-access] true) @@ -3415,7 +3819,9 @@ {:on-play {:async true :change-in-game-state (effect (and (seq (:deck runner)) (pos? (get-in @state [:runner :click] 0)))) - :msg (msg "draw " (quantify (get-in @state [:runner :click]) "card")) + :msg (simple-msg + {:effect/type :draw-cards + :effect/count (get-in @state [:runner :click])}) :effect (effect (draw state side eid (get-in @state [:runner :click] 0)))}}) (defcard "Rumor Mill" @@ -3458,11 +3864,11 @@ (defcard "Running Hot" {:on-play - {:msg "gain [Click][Click][Click]" + {:msg (simple-msg + {:effect/type :gain-clicks + :effect/count 3}) :additional-cost [(->c :brain 1)] - :async true - :effect (effect (gain-clicks state side 3) - (effect-completed state side eid))}}) + :effect (effect (gain-clicks state side 3))}}) (defcard "Running Interference" {:makes-run true @@ -3478,7 +3884,9 @@ :automatic :bypass :req (req (first-run-event? state side :encounter-ice)) :once :per-run - :msg (msg "bypass " (card-str state current-ice)) + :msg (simple-msg + {:effect/type :bypass-ice + :effect/card-str current-ice}) :effect (effect (bypass-ice state))} {:event :encounter-ice :skippable true @@ -3490,7 +3898,9 @@ (card-str state current-ice) "?") :waiting-prompt true - :yes-ability {:msg (msg "bypass " (card-str state current-ice)) + :yes-ability {:msg (simple-msg + {:effect/type :bypass-ice + :effect/card-str current-ice}) :cost [(->c :click 1)] :effect (effect (bypass-ice state))}}} card nil))}]}) @@ -3534,9 +3944,12 @@ (and (in-discard? target) (not (zone-locked? state :runner :discard)))) (runner-can-pay-and-install? state side (assoc eid :source card) target {:cost-bonus (- tcost)}))} - :msg (msg "trash " (:title trashed) - " and install " (:title target) - ", lowering the cost by " tcost " [Credits]") + :msg (simple-msg + {:effect/type :trash-card + :effect/card-str trashed} + {:effect/type :install-with-discount + :effect/title target + :effect/discount tcost}) :effect (effect (runner-install state side (assoc eid :source card :source-type :runner-install) target {:cost-bonus (- tcost)}))} card nil))))}}) @@ -3547,7 +3960,9 @@ :choices {:req (req (program? target) (in-discard? target))} :show-discard true - :msg (msg "put " (:title target) " on the bottom of the stack") + :msg (simple-msg + {:effect/type :add-card-to-bottom-of-stack + :effect/card-str target}) :effect (effect (move state side target :deck))}] {:on-play {:prompt "Choose a program to install" :label "Install program from the heap" @@ -3587,7 +4002,11 @@ (defcard "Sell Out" {:on-play {:additional-cost [(->c :resource 1)] :async true - :msg "gain 4 [Credits] and draw 2 cards" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 4} + {:effect/type :draw-cards + :effect/count 2}) :effect (effect (wait-for (gain-credits state side 4 {:suppress-checkpoint true}) (draw state side eid 2)))}}) @@ -3613,7 +4032,7 @@ {:option "The run does not end" :ability {:display-side :runner :async true - :msg "prevent the run from ending" + :msg (simple-msg :prevent-run-ending) :effect (effect (prevent-end-run state side eid))}}])) card nil)))}}}]}) @@ -3624,7 +4043,7 @@ :req (req (= :rd (target-server context)) this-card-run) :silent true - :msg "access cards from the bottom of R&D" + :msg (simple-msg :access-from-bottom-of-rd) :effect (effect (swap! state assoc-in [:runner :rd-access-fn] reverse))} {:event :run-ends :effect (effect (swap! state assoc-in [:runner :rd-access-fn] seq))}]}) @@ -3638,7 +4057,9 @@ :mandatory true :ability {:async true - :msg "trash all cards in the server at no cost" + :msg (simple-msg + {:effect/type :trash-all-cards-in-server-at-no-cost + :effect/server (first (:server (:run @state)))}) :effect (effect (trash-cards state side eid (:content run-server) {:cause-card card}))}})]}) (defcard "Social Engineering" @@ -3648,7 +4069,9 @@ (installed? %) (ice? %))} :change-in-game-state {:req (req (some (every-pred ice? (complement rezzed?)) (all-installed state :corp)))} - :msg (msg "select " (card-str state target)) + :msg (simple-msg + {:effect/type :select + :effect/card-str target}) :effect (effect (register-events state side card @@ -3656,7 +4079,9 @@ [{:event :rez :duration :end-of-turn :req (req (same-card? (:card context) ice)) - :msg (msg "gain " (rez-cost state side (get-card state (:card context))) " [Credits]") + :msg (simple-msg + {:effect/type :gain-credits + :effect/count (rez-cost state side (get-card state (:card context)))}) :async true :effect (effect (gain-credits state :runner eid (rez-cost state side (get-card state (:card context)))))}])))}}) @@ -3665,7 +4090,7 @@ {:async true :effect (effect (wait-for (reveal-loud state side card - {:and-then "shuffle the Stack"} revealed-cards) + {:and-then " and shuffle the Stack"} revealed-cards) (shuffle! state side :deck) (effect-completed state side eid)))}) (install-program [state side eid card revealed-card revealed-cards] @@ -3712,13 +4137,19 @@ :events [{:event :encounter-ice :automatic :bypass :req (req (= 1 run-position)) - :msg (msg "bypass " (:title (:ice context))) + :msg (simple-msg + {:effect/type :bypass-ice + :effect/card-str (:ice context)}) :effect (effect (bypass-ice state))}]}) (defcard "Spec Work" {:on-play {:additional-cost [(->c :program 1)] - :msg "gain 4 [Credits] and draw 2 cards" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 4} + {:effect/type :draw-cards + :effect/count 2}) :async true :effect (effect (wait-for (gain-credits state side 4) (draw state side eid 2)))}}) @@ -3733,7 +4164,10 @@ {:makes-run true :on-play {:prompt "Choose 1 non-ice card to expose" - :msg "expose 1 card and make a run" + :msg (simple-msg + {:effect/type :expose-card + :effect/title target} + :make-a-run) :choices {:card #(and (installed? %) (not (ice? %)) (corp? %))} @@ -3762,14 +4196,19 @@ {:prompt "Choose a piece of ice protecting this server" :choices {:card #(and (ice? %) (= (first (:server run)) (second (get-zone %))))} - :msg (msg "host " (:title trojan) " on " (card-str state target)) + :msg (simple-msg + {:effect/type :host-card-on-card + :effect/title trojan + :effect/card-str target}) :effect (effect (host state side target trojan) (update-all-ice state side))} card nil)))}]}) (defcard "Steelskin Scarring" {:on-play {:async true - :msg "draw 3 cards" + :msg (simple-msg + {:effect/type :draw-cards + :effect/count 3}) :change-in-game-state {:req (req (seq (:deck runner)))} :effect (effect (draw state side eid 3))} :on-trash {:when-inactive true @@ -3778,13 +4217,17 @@ :req (req (let [zone (first (:zone (:card context)))] (#{:hand :deck} zone))) :effect (effect (continue-ability - state side {:optional {:prompt "Draw 2 cards?" - :waiting-prompt true - :yes-ability {:msg "draw 2 cards" - :async true - :effect (effect (draw state :runner eid 2))} - :no-ability - {:effect (effect (system-msg state side (str "declines to use " (:title card))))}}} + state side + {:optional + {:prompt "Draw 2 cards?" + :waiting-prompt true + :yes-ability {:msg (simple-msg + {:effect/type :draw-cards + :effect/count 2}) + :async true + :effect (effect (draw state :runner eid 2))} + :no-ability + {:effect (effect (system-msg state side (str "declines to use " (:title card))))}}} card nil))}}) (defcard "Stimhack" @@ -3797,14 +4240,18 @@ (make-run state side eid target card))} :events [{:event :run-ends :req (req this-card-run) - :msg "take 1 core damage" + :msg (simple-msg + {:effect/type :suffer-core-damage + :effect/value 1}) :async true :effect (effect (damage state side eid :brain 1 {:unpreventable true :card card}))}]}) (defcard "Strike Fund" {:on-play {:async true - :msg "gain 4 [Credits]" + :msg (simple-msg + {:effect/type :gain-credits + :effect/count 4}) :effect (effect (gain-credits state :runner nil 4) (effect-completed state side eid))} :on-trash {:when-inactive true @@ -3815,7 +4262,9 @@ :effect (effect (continue-ability state side {:optional {:prompt "Gain 2 [Credits]?" :waiting-prompt true - :yes-ability {:msg "gain 2 [Credits]" + :yes-ability {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 2}) :async true :effect (effect (gain-credits state :runner eid 2))} :no-ability @@ -3824,7 +4273,9 @@ (defcard "Sure Gamble" {:on-play - {:msg "gain 9 [Credits]" + {:msg (simple-msg + {:effect/type :gain-credits + :effect/count 9}) :async true :effect (effect (gain-credits state side eid 9))}}) @@ -3838,7 +4289,10 @@ {:on-play {:req (req (placed-virus-cards state)) :choices {:req (req (some #(same-card? % target) (placed-virus-cards state)))} - :msg (msg "place 2 virus counters on " (:title target)) + :msg (simple-msg + {:effect/type :place-virus-counters + :effect/count 2 + :effect/title target}) :async true :effect (effect (add-counter state :runner eid target :virus 2 nil))}})) @@ -3848,10 +4302,15 @@ :waiting-prompt true :prompt "Choose one" :choices (effect [(when (<= 2 (count (:hand corp))) - "Discard 2 cards from HQ") - "Draw 4 cards"]) + "Discard 2 cards from HQ") + "Draw 4 cards"]) :async true - :msg (msg "force the Corp to " (decapitalize target)) + :msg (simple-msg + (if (= target "Draw 4 cards") + {:effect/type :force-corp-draw-cards + :effect/count 4} + {:effect/type :force-corp-discard-from-hq + :effect/count 2})) :effect (effect (if (= target "Draw 4 cards") (wait-for (draw state :corp 4) (effect-completed state side eid)) @@ -3871,7 +4330,9 @@ (defcard "System Outage" {:events [{:event :corp-draw :req (req (not (first-event? state side :corp-draw))) - :msg "force the Corp to lose 1 [Credits]" + :msg (simple-msg + {:effect/type :force-corp-lose-credits + :effect/credits 1}) :async true :effect (effect (lose-credits state :corp eid 1))}]}) @@ -3909,7 +4370,9 @@ :events [{:event :successful-run :req (req (#{:hq :rd} (target-server context)) (pos? (or (:subroutines-fired context) 0))) - :msg "force the Corp to take 1 Bad Publicity" + :msg (simple-msg + {:effect/type :force-take-bad-publicity + :effect/count 1}) :async true :effect (effect (gain-bad-publicity state :corp eid 1 {:card card}))}]}) @@ -3919,8 +4382,12 @@ "Install a program from the stack or heap?" "Install a program from the stack?")) :choices (effect ["Stack" - (when (not (zone-locked? state :runner :discard)) "Heap")]) - :msg (msg "install a program from the " target) + (when-not (zone-locked? state :runner :discard) + "Heap")]) + :msg (simple-msg + (if (= "Stack" target) + :install-program-from-stack + :install-program-from-heap)) :waiting-prompt true :async true :effect (effect @@ -3948,7 +4415,9 @@ [{:event :runner-turn-ends :duration :end-of-turn :req (req (get-in (find-latest state installed-card) [:special :test-run])) - :msg (msg "move " (:title installed-card) " to the top of the stack") + :msg (simple-msg + {:effect/type :add-card-to-top-of-stack + :effect/card-str installed-card}) :effect (effect (move state side (find-latest state installed-card) :deck {:front true}))}])) (effect-completed state side eid)))}) card nil))}}) @@ -3984,7 +4453,13 @@ (same-card? card (get-in @state [:runner :play-area 0])) (preventable? context)) :condition :active - :msg (msg "prevent " (:remaining context) " " (damage-name state) " damage") + :msg (simple-msg + (let [t (case (damage-type state) + :meat :prevent-meat-damage + (:brain :core) :prevent-core-damage + :net :prevent-net-damage)] + {:effect/type t + :effect/count (:remaining context)})) :effect (effect (prevent-damage state side eid :all))}}] :on-play {:async true :change-in-game-state {:req (req (or (seq (:hand runner)) @@ -3996,8 +4471,10 @@ {:async true :prompt "Choose a server" :choices (effect runnable-servers) - :msg (msg "trash [their] grip and make a run on " target - ", preventing all damage") + :msg (simple-msg + :trash-all-cards-in-grip + {:effect/type :make-a-run-on-preventing-all-damage + :effect/server target}) :effect (effect (make-run state side eid target card))} card nil)))}}) @@ -4030,7 +4507,7 @@ (defcard "The Price of Freedom" {:on-play {:additional-cost [(->c :connection 1)] :rfg-instead-of-trashing true - :msg (msg "prevent the Corp from advancing cards during [their] next turn")} + :msg (simple-msg :prevent-corp-advancing-cards-next-turn)} :events [{:event :corp-turn-begins :duration :until-runner-turn-begins :effect (effect (register-turn-flag! @@ -4045,7 +4522,9 @@ :automatic :gain-credits :duration :end-of-turn :unregister-once-resolved true - :msg (msg "gain " (* 2 (count (:successful-run runner-reg))) " [Credits]") + :msg (simple-msg + {:effect/type :gain-credits + :effect/count (* 2 (count (:successful-run runner-reg)))}) :async true :effect (effect (gain-credits state side eid (* 2 (count (:successful-run runner-reg)))))}]))}}) @@ -4054,7 +4533,9 @@ {:prompt "Choose a piece of ice" :choices {:card (every-pred ice? installed?)} :change-in-game-state {:req (req (some ice? (all-installed state :corp)))} - :msg (msg "make " (card-str state target) " gain Sentry, Code Gate, and Barrier until the end of the turn") + :msg (simple-msg + {:effect/type :ice-gains-barrier-code-gate-sentry-end-of-turn + :effect/card-str target}) :effect (effect (let [ice target] (register-lingering-effect state side card @@ -4071,24 +4552,27 @@ (defcard "Trade-In" ;; TODO: look at me plz 👀 - (letfn [(trashed-hw [state] (last (get-in @state [:runner :discard])))] - {:on-play - {:additional-cost [(->c :hardware 1)] - :msg (msg (let [{:keys [title cost]} (trashed-hw state)] - (str "trash " title " and gain " (quot cost 2) " [Credits]"))) - :async true - :effect (effect (let [{:keys [cost]} (trashed-hw state)] - (wait-for (gain-credits state :runner (quot cost 2)) - (continue-ability - state :runner - {:prompt "Choose a piece of hardware to add to the grip" - :choices (effect (filter hardware? - (:deck runner))) - :msg (msg "add " (:title target) " from the stack to the Grip and shuffle the stack") - :effect (effect (trigger-event state side :searched-stack) - (shuffle! state side :deck) - (move state side target :hand))} - card nil))))}})) + {:on-play + {:additional-cost [(->c :hardware 1)] + :msg (simple-msg + {:effect/type :gain-credits + :effect/count (quot (:cost (cost-target eid :trash-hardware)) 2)}) + :async true + :effect (effect (let [value (quot (:cost (cost-target eid :trash-hardware)) 2)] + (wait-for (gain-credits state :runner value) + (continue-ability + state :runner + {:prompt "Choose a piece of hardware to add to the grip" + :choices (effect (filter hardware? + (:deck runner))) + :msg (simple-msg + {:effect/type :add-card-from-stack-to-grip + :effect/card-str target} + :shuffle-stack) + :effect (effect (trigger-event state side :searched-stack) + (shuffle! state side :deck) + (move state side target :hand))} + card nil))))}}) (defcard "Traffic Jam" {:static-abilities [{:type :advancement-requirement @@ -4103,12 +4587,15 @@ :interactive (effect true) :automatic :drain-credits :req (req this-card-run (= :hq (target-server context))) - :msg (msg - "take 1 tag" - (let [cc (min (:credit corp) 3)] - (when (pos? cc) - (str "and force the Corp to lose " cc " [Credits], and then gain " - (* 2 cc) " [Credits]")))) + :msg (simple-msg + {:effect/type :take-tags + :effect/count 1} + (let [cc (min (:credit corp) 3)] + (when (pos? cc) + [{:effect/type :force-corp-lose-credits + :effect/credits cc} + {:effect/type :gain-credits + :effect/count (* 2 cc)}]))) :async true :effect (effect (let [cc (min (:credit corp) 3)] (if (pos? cc) @@ -4145,7 +4632,11 @@ :req (req (= :rd (target-server context)) this-card-run (= (get-in card [:special :run-eid :eid]) (get-in @state [:run :eid :eid]))) - :msg "place 2 [Credits] on itself and access 1 additional card from R&D" + :msg (simple-msg + {:effect/type :place-credits-on-self + :effect/credits 2} + {:effect/type :access-additional-in-rd + :effect/count 1}) :async true :effect (effect (register-events @@ -4160,7 +4651,9 @@ (map unknown->kw) (filter is-remote?) (map remote->name)))) - :msg (msg "make a run on " target) + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server target}) :async true :effect (effect (make-run state side eid target card))}]}) @@ -4173,7 +4666,9 @@ (not (facedown? %)) (or (hardware? %) (program? %)))} - :msg (msg "move " (:title target) " to [their] Grip") + :msg (simple-msg + {:effect/type :add-card-to-grip + :effect/title target}) :effect (effect (move state side target :hand))}}) (defcard "Unscheduled Maintenance" @@ -4196,17 +4691,22 @@ :this-card-run true :ability {:cost [(->c :x-credits)] :async true - :change-in-game-state {:req (req (pos? (cost-value eid :x-credits)))} - :msg (msg "make the corp lose " (cost-value eid :x-credits) " [Credits]") + :change-in-game-state {:req (req (pos? (x-cost-value eid)))} + :msg (simple-msg + {:effect/type :force-corp-lose-credits + :effect/credits (x-cost-value eid)}) :effect (effect (wait-for - (lose-credits state :corp (cost-value eid :x-credits)) + (lose-credits state :corp (x-cost-value eid)) (continue-ability state side (gain-tags-ability 1) card nil)))}})]}) (defcard "VRcation" {:on-play - {:msg (msg "draw 4 cards" - (when (pos? (:click runner)) - " and lose [Click]")) + {:msg (simple-msg + {:effect/type :draw-cards + :effect/count 4} + (when (pos? (:click runner)) + {:effect/type :lose-clicks + :effect/count 1})) :change-in-game-state {:req (req (or (seq (:deck runner)) (pos? (:click runner))))} :async true @@ -4221,9 +4721,11 @@ {:target-server :hq :this-card-run true :ability - {:msg (msg "force the Corp to discard " (quantify target "card") " from HQ at random") - :prompt "How many [Click] do you want to spend?" + {:prompt "How many [Click] do you want to spend?" :choices (effect (map str (range 0 (inc (:click runner))))) + :msg (simple-msg + {:effect/type :force-corp-random-discard-from-hq + :effect/count (parse-long target)}) :async true :effect (effect (let [n (str->int target)] (wait-for [{:keys [msg]} (pay state :runner (make-eid state eid) card (->c :click n))] @@ -4235,7 +4737,9 @@ [{:event :pre-access-card :duration :end-of-game :req (req (same-card? :title burned-card (:accessed-card context))) - :msg (msg (str "remove " (:title burned-card) " from the game")) + :msg (simple-msg + {:effect/type :rfg-card + :effect/title burned-card}) :effect (effect (move state :corp (:accessed-card context) :rfg))}])] {:makes-run true :on-play run-remote-server-ability @@ -4243,7 +4747,9 @@ :req (req (not (agenda? (:accessed-card context))) (:successful run)) :once :per-run - :msg (msg "remove " (:title (:accessed-card context)) " from the game, and watch for other copies of " (:title (:accessed-card context)) " to burn") + :msg (simple-msg + {:effect/type :rfg-card + :effect/title (:accessed-card context)}) :effect (effect (move state :corp (:accessed-card context) :rfg) (register-events state side card (rfg-card-event (:accessed-card context))))}]})) @@ -4258,7 +4764,10 @@ :choices {:req (req (corp? target) (in-hand? target)) :max (effect (min 2 (count (:hand corp))))} - :msg (msg "shuffle " (enumerate-cards targets :sorted) " into R&D") + :msg (simple-msg + {:effect/type :shuffle-cards-into-rd + :effect/count (count targets) + :effect/titles (sort-by get-title targets)}) :effect (effect (doseq [t targets] (move state :corp t :deck)) (shuffle! state :corp :deck))})}}}) @@ -4267,12 +4776,16 @@ {:on-play (choose-one-helper {:player :corp} [{:option "Runner gains 6 [Credits]" - :ability {:msg "force the Runner to gain 6 [Credits]" + :ability {:msg (simple-msg + {:effect/type :force-runner-gain-credits + :effect/credits 6}) :display-side :corp :async true :effect (effect (gain-credits state :runner eid 6))}} {:option "Runner draws 4 cards" - :ability {:msg "force the Runner to draw 4 cards" + :ability {:msg (simple-msg + {:effect/type :force-runner-draw-cards + :effect/count 4}) :display-side :corp :async true :effect (effect (draw state :runner eid 4))}}])}) diff --git a/src/clj/game/cards/hardware.clj b/src/clj/game/cards/hardware.clj index bb0add40bf..62f86c7d99 100644 --- a/src/clj/game/cards/hardware.clj +++ b/src/clj/game/cards/hardware.clj @@ -33,6 +33,7 @@ lose-credits]] [game.core.hand-size :refer [hand-size runner-hand-size+]] [game.core.hosting :refer [host]] + [jinteki.i18n :refer [msg-with-cost]] [game.core.ice :refer [all-subs-broken? any-subs-broken? auto-icebreaker break-sub pump reset-all-ice update-all-ice update-all-icebreakers update-breaker-strength]] @@ -41,7 +42,7 @@ [game.core.memory :refer [caissa-mu+ expected-mu mu+ update-mu virus-mu+]] [game.core.moving :refer [as-agenda mill move swap-agendas trash trash-cards]] [game.core.optional :refer [get-autoresolve never? set-autoresolve]] - [game.core.payment :refer [build-cost-string can-pay? cost-value ->c]] + [game.core.payment :refer [build-cost-string can-pay? cost-value ->c x-cost-value]] [game.core.play-instants :refer [play-instant]] [game.core.prevention :refer [damage-name damage-type preventable? prevent-damage prevent-encounter prevent-end-run prevent-tag prevent-up-to-n-damage]] [game.core.prompts :refer [cancellable clear-wait-prompt]] @@ -173,7 +174,8 @@ (some #(event? (:card %)) targets))] (first-trash? state event-targets?))) :change-in-game-state {:silent true :req (req (seq (:deck runner)))} - :msg "draw 1 card" + :msg (msg-with-cost {:effect/type :draw-cards + :effect/count 1}) :effect (effect (draw state :runner eid 1))}] {:static-abilities [(mu+ 1)] :events [(assoc ability :event :corp-trash) @@ -1078,8 +1080,12 @@ (defcard "Gachapon" (letfn [(shuffle-end [remove-from-game shuffle-back] - {:msg (msg "shuffle " (enumerate-cards shuffle-back :sorted) " into the stack" - " and remove " (enumerate-cards remove-from-game :sorted) " from the game") + {:msg (msg (when shuffle-back + (str "shuffle " (enumerate-cards shuffle-back :sorted) " into the stack")) + (when (and shuffle-back remove-from-game) + " and ") + (when remove-from-game + (str "remove " (enumerate-cards remove-from-game :sorted) " from the game"))) :effect (effect (doseq [c remove-from-game] (move state side c :rfg)) @@ -1111,7 +1117,7 @@ :effect (effect (if finished? (if (= "Done" target) (continue-ability state side - (shuffle-end set-aside-cards to-shuffle) + (shuffle-end (seq set-aside-cards) (seq to-shuffle)) card nil) (continue-ability state side (shuffle-next (sort-by :title (concat set-aside-cards to-shuffle)) nil nil) @@ -1771,8 +1777,7 @@ :msg (msg "trash " (card-str state (first (filter program? (:hosted card)))) " for violating hosting restrictions") :async true :effect (effect (let [first-program (first (filter program? (:hosted card)))] - (system-msg state nil (card-str state (first (filter program? (:hosted card)))) " is trashed for violating hosting restrictions") - (trash-cards state side eid [first-program] {:unpreventable true :game-trash true})))} + (trash-cards state side eid [first-program] {:unpreventable true :game-trash true})))} :static-abilities [{:type :can-host :req (req (program? target) (<= (expected-mu state target) 1)) @@ -2127,9 +2132,9 @@ :effect (effect (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") + :msg (msg "prevent " (x-cost-value eid) " " (damage-type state) " damage") :async true - :effect (effect (prevent-damage state side eid (cost-value eid :x-credits)))} + :effect (effect (prevent-damage state side eid (x-cost-value eid)))} card nil))}}]}) (defcard "Record Reconstructor" @@ -2249,7 +2254,7 @@ :once :per-turn :async true :effect (effect (let [payment-eid eid - spent-credits (cost-value eid :x-credits)] + spent-credits (x-cost-value eid)] (continue-ability state side {:choices {:req (req (ice? target) diff --git a/src/clj/game/cards/ice.clj b/src/clj/game/cards/ice.clj index 30933c40dd..86f563f73d 100644 --- a/src/clj/game/cards/ice.clj +++ b/src/clj/game/cards/ice.clj @@ -783,15 +783,16 @@ (defcard "Aimor" {:subroutines [{:async true :label "Trash the top 3 cards of the stack" - :effect (effect (system-msg - state :corp - (str "uses " (:title card) " to trash " - (enumerate-cards (take 3 (:deck runner))) - " from the top of the stack and trash itself")) - (wait-for - (mill state :corp (make-eid state eid) :runner 3) - (wait-for (trash state :corp (make-eid state eid) card {:cause :subroutine}) - (encounter-ends state side eid))))}]}) + :effect (effect (when-let [top3 (seq (take 3 (:deck runner)))] + (system-msg + state :corp + (str "uses " (:title card) " to trash " + (enumerate-cards top3) + " from the top of the stack and trash itself"))) + (wait-for + (mill state :corp (make-eid state eid) :runner 3) + (wait-for (trash state :corp (make-eid state eid) card {:cause :subroutine}) + (encounter-ends state side eid))))}]}) (defcard "Akhet" (let [breakable-fn (effect (if (and (<= 3 (get-counters card :advancement)) diff --git a/src/clj/game/cards/identities.clj b/src/clj/game/cards/identities.clj index b999173427..519e85d3ea 100644 --- a/src/clj/game/cards/identities.clj +++ b/src/clj/game/cards/identities.clj @@ -1,6 +1,5 @@ (ns game.cards.identities (:require - [clojure.pprint :as pprint] [game.core.access :refer [access-bonus access-cost-bonus access-non-agenda]] [game.core.bad-publicity :refer [gain-bad-publicity]] [game.core.board :refer [all-active-installed all-installed card->server @@ -1294,7 +1293,7 @@ (defcard "Jinteki: Replicating Perfection" {:static-abilities [{:type :cannot-run-on-server :req (req (no-event? state side :run #(is-central? (:server (first %))))) - :value (effect (map first (get-remotes state)))}]}) + :value (effect (mapv first (get-remotes state)))}]}) (defcard "Jinteki: Restoring Humanity" {:events [{:event :corp-turn-ends @@ -1661,7 +1660,7 @@ {:events [{:event :approach-server :async true :interactive (effect true) - :waiting "Corp to make a decision" + :waiting-prompt true :req (req (pos? (count (:hand corp))) (not (used-this-turn? (:cid card) state))) :effect (effect (if (some ice? (:hand corp)) @@ -2164,7 +2163,7 @@ (opts-fn top-3)) card nil)))} score-ev {:event :agenda-scored - :skippabe true + :skippable true :interactive (effect true) :optional {:prompt "Look at the top 3 cards of R&D?" :req (req (seq (:deck corp))) @@ -2578,7 +2577,7 @@ (corp-install state side eid target nil {:ignore-install-cost true :msg-keys {:install-source card}})))}] :abilities [{:label "Gain 2 [Credits]" - :action :true + :action true :async true :cost [(->c :tag 1) (->c :click 1)] :effect (effect (gain-credits state side eid 2))}]}) @@ -2629,8 +2628,8 @@ (assoc swap-ability :event :agenda-stolen)]})) (defcard "Tennin Institute: The Secrets Within" - {:events [{:msg (msg "place 1 advancement token on " (card-str state target)) - :label "Place 1 advancement token on a card if the Runner did not make a successful run last turn" + {:events [{:msg (msg "place 1 advancement counter on " (card-str state target)) + :label "Place 1 advancement counter on a card if the Runner did not make a successful run last turn" :choices {:card installed?} :event :corp-turn-begins :req (req (not-last-turn? state :runner :successful-run)) diff --git a/src/clj/game/cards/operations.clj b/src/clj/game/cards/operations.clj index 68e31b8e24..0c0a5bd98c 100644 --- a/src/clj/game/cards/operations.clj +++ b/src/clj/game/cards/operations.clj @@ -113,7 +113,7 @@ :choices {:card #(and (installed? %) (f %)) :max (effect (min max-targets (count (valid-targets? state)))) - :all all?} + :all (boolean all?)} :msg (msg "trash " (enumerate-cards targets)) :effect (effect (trash-cards state side eid targets {:cause-card card}))} ab)))) diff --git a/src/clj/game/cards/programs.clj b/src/clj/game/cards/programs.clj index b9e91274d5..97a09d6447 100644 --- a/src/clj/game/cards/programs.clj +++ b/src/clj/game/cards/programs.clj @@ -42,7 +42,7 @@ [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]] [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.payment :refer [build-cost-label can-pay? cost-target x-cost-value ->c value]] [game.core.prevention :refer [preventable? prevent-damage prevent-end-run prevent-up-to-n-damage prevent-trash-installed-by-type]] [game.core.prompts :refer [cancellable]] [game.core.props :refer [add-counter]] @@ -501,9 +501,9 @@ (defcard "Atman" (auto-icebreaker {:on-install {:cost [(->c :x-credits)] - :msg (msg "place " (quantify (cost-value eid :x-credits) "power counter") " on itself") + :msg (msg "place " (quantify (x-cost-value eid) "power counter") " on itself") :async true - :effect (effect (add-counter state side eid card :power (cost-value eid :x-credits) nil))} + :effect (effect (add-counter state side eid card :power (x-cost-value eid) nil))} :abilities [(break-sub 1 1 "All" {:req (req (= (get-strength current-ice) (get-strength card)))})] :static-abilities [(breaker-strength-bonus (effect (get-counters card :power)))]})) @@ -1445,8 +1445,8 @@ {:label "+X strength for the remainder of the run (using at least 1 stealth [Credits])" :cost [(->c :x-credits 0 {:stealth 1})] :prompt "How many credits do you want to spend?" - :effect (effect (pump state side card (cost-value eid :x-credits) :end-of-run)) - :msg (msg "increase strength by " (cost-value eid :x-credits) + :effect (effect (pump state side card (x-cost-value eid) :end-of-run)) + :msg (msg "increase strength by " (x-cost-value eid) " for the remainder of the run")}]}) (defcard "Femme Fatale" @@ -1595,7 +1595,7 @@ :req (req (active-encounter? state) (< 1 (count (remove :broken (:subroutines current-ice))))) :break 1 ;technically not correct, but will only be used by the engine to check for breaking abilities - :breaks "All" + :breaks #{"All"} :break-cost [(->c :trash-can)] :cost [(->c :trash-can)] :prompt "Choose the subroutine to NOT break" @@ -2050,13 +2050,13 @@ (has-subtype? current-ice "Barrier"))) :req (req (active-encounter? state) (<= (get-strength current-ice) (get-strength card))) - :msg (msg "break " (quantify (cost-value eid :x-credits) "subroutine") + :msg (msg "break " (quantify (x-cost-value eid) "subroutine") " on " (card-str state current-ice)) :async true :effect (effect (continue-ability - state side (when (pos? (cost-value eid :x-credits)) - (break-sub nil (cost-value eid :x-credits) "Barrier")) + state side (when (pos? (x-cost-value eid)) + (break-sub nil (x-cost-value eid) "Barrier")) card nil))} (strength-pump 1 2)] :events [{:event :subroutines-broken @@ -2125,8 +2125,8 @@ :cost [(->c :x-credits)] :req (req (:runner-phase-12 @state)) :async true - :effect (effect (add-counter state side eid card :power (cost-value eid :x-credits) nil)) - :msg (msg "place " (quantify (cost-value eid :x-credits) "power counter") " on itself")} + :effect (effect (add-counter state side eid card :power (x-cost-value eid) nil)) + :msg (msg "place " (quantify (x-cost-value eid) "power counter") " on itself")} (break-sub [(->c :power 1)] 1) (strength-pump 2 2)] :events [{:event :runner-turn-ends @@ -2173,12 +2173,12 @@ :break-req (effect (active-encounter? state)) :req (req (active-encounter? state) (<= (get-strength current-ice) (get-strength card))) - :msg (msg "break " (quantify (cost-value eid :x-credits) "subroutine") + :msg (msg "break " (quantify (x-cost-value eid) "subroutine") " on " (card-str state current-ice)) :effect (effect (continue-ability - state side (when (pos? (cost-value eid :x-credits)) - (break-sub nil (cost-value eid :x-credits) "All" {:repeatable false})) + state side (when (pos? (x-cost-value eid)) + (break-sub nil (x-cost-value eid) "All" {:repeatable false})) card nil))} host-abi {:action true :label "Host 1 copy of Matryoshka" @@ -2211,7 +2211,7 @@ {:abilities [(break-sub 1 1 "All" {:additional-ability - {:msg "will trash itself when this run ends" + {:msg "to trash itself when this run ends" :effect (effect (register-events state :runner (get-card state card) @@ -2249,8 +2249,8 @@ :cost [(->c :click 2) (->c :x-credits)] :label "remove X tags" :async true - :msg (msg "remove " (quantify (cost-value eid :x-credits) "tag")) - :effect (effect (lose-tags state :runner eid (cost-value eid :x-credits)))}]}) + :msg (msg "remove " (quantify (x-cost-value eid) "tag")) + :effect (effect (lose-tags state :runner eid (x-cost-value eid)))}]}) (defcard "MKUltra" (let [events (for [event [:run :approach-ice :encounter-ice :pass-ice :run-ends @@ -2564,12 +2564,12 @@ :break-req (effect (and (active-encounter? state) (has-subtype? current-ice "Barrier"))) :async true - :effect (effect (pump state side card (cost-value eid :x-credits)) + :effect (effect (pump state side card (x-cost-value eid)) (continue-ability - state side (break-sub nil (cost-value eid :x-credits) "Barrier" {:repeatable false}) + state side (break-sub nil (x-cost-value eid) "Barrier" {:repeatable false}) (get-card state card) nil)) :msg (msg "increase its strength from " (get-strength card) - " to " (+ (cost-value eid :x-credits) (get-strength card)))}]] + " to " (+ (x-cost-value eid) (get-strength card)))}]] (assoc cdef :events (apply conj events (:events cdef)) :abilities abilities))) @@ -3172,7 +3172,8 @@ (map unknown->kw) (filter is-remote?) (map remote->name)))) - :msg "make a run on a remote server" + :label "make a run on a remote server" + :msg (msg "make a run on " target) :makes-run true :async true :effect (effect (let [initial-server target] @@ -3196,7 +3197,8 @@ (map unknown->kw) (filter is-central?) (map central->name)))) - :msg "make a run on central server" + :label "make a run on a central server" + :msg (msg "make a run on " target) :makes-run true :async true :effect (effect (let [initial-server target] @@ -3250,16 +3252,16 @@ :prompt "Choose a card to trash" :not-distinct true :choices (effect (take 3 (:deck corp))) - :msg (msg (let [card-titles (map :title (take 3 (:deck corp))) - target-position (first (positions #{target} (take 3 (:deck corp)))) - position (case target-position - 0 "top " - 1 "middle " - 2 "bottom " - "this-should-not-happen ")] - (if (= 1 (count (filter #{(:title target)} card-titles))) - (str "trash " (:title target)) - (str "trash " position (:title target))))) + :msg (msg (let [card-titles (map :title (take 3 (:deck corp))) + target-position (first (positions #{target} (take 3 (:deck corp)))) + position (case target-position + 0 "top " + 1 "middle " + 2 "bottom " + "this-should-not-happen ")] + (if (= 1 (count (filter #{(:title target)} card-titles))) + (str "trash " (:title target)) + (str "trash " position (:title target))))) :effect (effect (trash state :runner eid (assoc target :seen true) {:cause-card card}))} card nil)))}})] {:abilities [(run-server-ability :rd {:action true @@ -3579,12 +3581,12 @@ :break-req (effect (and (active-encounter? state) (not-used-once? state {:once :per-run} card) (has-subtype? current-ice "Code Gate"))) - :msg (msg "break " (quantify (cost-value eid :x-credits) "subroutine") + :msg (msg "break " (quantify (x-cost-value eid) "subroutine") " on " (card-str state current-ice)) :effect (effect (continue-ability - state side (when (pos? (cost-value eid :x-credits)) - (break-sub nil (cost-value eid :x-credits) "Code Gate")) + state side (when (pos? (x-cost-value eid)) + (break-sub nil (x-cost-value eid) "Code Gate")) card nil))} (break-sub 1 1 "Code Gate" {:label "Break 1 Code Gate subroutine (Virtual restriction)" ;; this should be prioritized when available diff --git a/src/clj/game/cards/resources.clj b/src/clj/game/cards/resources.clj index fb3381fc4a..f9643a3773 100644 --- a/src/clj/game/cards/resources.clj +++ b/src/clj/game/cards/resources.clj @@ -43,6 +43,7 @@ lose-credits]] [game.core.hand-size :refer [corp-hand-size+ hand-size runner-hand-size+]] [game.core.hosting :refer [host]] + [jinteki.i18n :refer [msg-with-cost]] [game.core.ice :refer [break-sub break-subroutine! get-strength ice-strength pump pump-ice unbroken-subroutines-choice update-all-ice update-all-icebreakers update-breaker-strength]] @@ -178,7 +179,7 @@ (rezzed? current-ice) (not (:broken (selector (:subroutines current-ice))))) :break 1 - :breaks "All" + :breaks #{"All"} :break-cost [(->c :trash-can)] :cost [(->c :trash-can)] :label (str "Break the " descriptor " subroutine") @@ -926,16 +927,21 @@ :yes-ability {:cost [(->c :credit (rez-cost state :corp (:card context))) (->c :trash-self)] :async true - :effect (effect (wait-for - (derez state :runner (:card context) {:msg-keys {:source-card card :and-then " and prevent the Corp from rezzing it for the remainder of this turn."}}) - (register-turn-flag! - state side card :can-rez - (fn [state _ card] - (if (same-card? card (:card context)) - ((constantly false) - (toast state :corp "Cannot rez the rest of this turn due to Councilman")) - true))) - (effect-completed state side eid)))}}} + :effect (effect + (wait-for (derez state :runner (:card context) + {:msg-keys + {:source-card card + #_#_:and-then + {:effect/type :prevent-corp-rezzing-remainder-of-run + :effect/card-str (:card context)}}}) + (register-turn-flag! + state side card :can-rez + (fn [state _ card] + (if (same-card? card (:card context)) + ((constantly false) + (toast state :corp "Cannot rez the rest of this turn due to Councilman")) + true))) + (effect-completed state side eid)))}}} card targets))}]}) (defcard "Counter Surveillance" @@ -2086,9 +2092,9 @@ (first-event? state side :corp-install #(and (not (ice? (:card (first %)))) (not (condition-counter? (:card (first %))))))) :yes-ability {:msg (msg (if (seq (:deck runner)) - (str "trash " + (str "trash the top card (" (:title (first (:deck runner))) - " from the stack and draw 1 card") + ") from the stack and draw 1 card") "trash no cards from the stack (it is empty)")) :async true :effect (effect (wait-for (mill state :runner :runner 1) @@ -2466,7 +2472,9 @@ :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" + :msg (msg-with-cost + {:effect/type :avoid-tags + :effect/count 1}) :effect (effect (wait-for (prevent-tag state :runner 1) (continue-ability state side @@ -2478,7 +2486,9 @@ :prompt "Pay 2 [Credits] to avoid a tag?" :ability {:async true :cost [(->c :credit 2)] - :msg "avoid 1 tag" + :msg (msg-with-cost + {:effect/type :avoid-tags + :effect/count 1}) :req (req (preventable? context)) :effect (effect (wait-for (prevent-tag state :runner 1) (continue-ability state side (prevent-another-tag) card nil)))}}] @@ -3104,7 +3114,7 @@ card (move state :corp target :rfg)] (system-msg state side (str payment-str - " and remove " (:title target) + " to remove " (:title target) " from the game")) (complete-with-result state side eid card)))))}}}) @@ -3301,7 +3311,7 @@ {:events [{:event :runner-turn-begins :async true :msg (msg (if (>= (get-counters card :power) 2) - "takes 1 core damage" + "take 1 core damage" "gain [Click]")) :effect (effect (if (>= (get-counters card :power) 2) (wait-for (add-counter state side card :power (- (get-counters card :power))) @@ -3950,8 +3960,7 @@ :async true :effect (effect (trash state side eid card))} (choose-one-helper - {:event :runner-action-phase-ends - :interactive (effect true)} + {:interactive (effect true)} [{:option "Draw 2 cards" :ability {:change-in-game-state {:silent true :req (req (seq (get-in @state [:runner :deck])))} :msg "draw 2 cards" diff --git a/src/clj/game/cards/upgrades.clj b/src/clj/game/cards/upgrades.clj index c465ef1d4c..76e5edcfe6 100644 --- a/src/clj/game/cards/upgrades.clj +++ b/src/clj/game/cards/upgrades.clj @@ -31,10 +31,11 @@ [game.core.ice :refer [all-subs-broken? get-current-ice get-run-ices pump-ice resolve-subroutine! unbroken-subroutines-choice update-all-ice update-all-icebreakers]] [game.core.installing :refer [corp-install swap-cards-async]] + [jinteki.i18n :refer [msg-with-cost]] [game.core.moving :refer [mill move remove-from-currently-drawing swap-cards swap-ice trash trash-cards]] [game.core.optional :refer [get-autoresolve set-autoresolve]] - [game.core.payment :refer [can-pay? cost-value ->c]] + [game.core.payment :refer [can-pay? cost-value ->c x-cost-value]] [game.core.play-instants :refer [play-instant]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-prop set-prop]] @@ -348,10 +349,15 @@ (rezzed? %) (not (same-card? % rezzed-card)))} :async true - :effect (effect (wait-for (derez state side (get-card state target) - {:msg-keys {:and-then (str " to give " (card-str state rezzed-card) " +3 strength for the remainder of the run")}}) - (pump-ice state side rezzed-card 3 :end-of-run) - (effect-completed state side eid)))}}} + :effect (effect (wait-for (derez state side + (get-card state target) + {:msg-keys + {:and-then + {:effect/type :give-strength-to-icebreaker-remainder-of-run + :effect/card-str rezzed-card + :effect/bonus 3}}}) + (pump-ice state side rezzed-card 3 :end-of-run) + (effect-completed state side eid)))}}} card nil)))}]}) (defcard "Breaker Bay Grid" @@ -502,9 +508,9 @@ :req (req (ice? target) (rezzed? target) (protecting-same-server? card target))} - :msg (msg "add " (cost-value eid :x-credits) + :msg (msg "add " (x-cost-value eid) " strength to " (:title target)) - :effect (effect (pump-ice state side target (cost-value eid :x-credits) :end-of-turn))}]}) + :effect (effect (pump-ice state side target (x-cost-value eid) :end-of-turn))}]}) (defcard "Crisium Grid" {:static-abilities [{:type :block-successful-run @@ -930,11 +936,11 @@ (no-event? state side :agenda-stolen)) -6 0)) - :abilities [{:label "Place 1 advancement token on a card in this server" + :abilities [{:label "Place 1 advancement counter on a card in this server" :async true :prompt "Choose a card in this server" :choices {:req (req (in-same-server? card target))} - :msg (msg "place an advancement token on " (card-str state target)) + :msg (msg "place an advancement counter on " (card-str state target)) :cost [(->c :trash-can)] :effect (effect (add-prop state side eid target :advance-counter 1 {:placed true}))}]}) @@ -1098,9 +1104,12 @@ (defcard "La Costa Grid" (let [ability {:prompt (msg "Choose a card in " (zone->name (second (get-zone card)))) :label "Place 1 advancement counter (start of turn)" - :msg (msg "place 1 advancement counter on " (card-str state target)) + :msg (msg-with-cost + {:effect/type :place-n-advancement-counters + :effect/count 1 + :effect/card-str target}) :choices {:req (req (installed? target) - (in-same-server? card target))} + (in-same-server? card target))} :async true :effect (effect (add-prop state side eid target :advance-counter 1 {:placed true}))}] {:legal-zones (effect (remove #{"HQ" "R&D" "Archives"} targets)) @@ -1528,7 +1537,7 @@ state side (when-let [rdc target] {:prompt "Choose a card in HQ" :choices {:card in-hand?} - :msg "swap a card from the top 5 of R&D with a card in HQ" + :msg "swap a card from the top 5 cards of R&D with a card in HQ" :effect (effect (move state side rdc :hand) (move state side target :deck {:index (:index rdc)}))}) card nil))} @@ -1553,7 +1562,7 @@ {:install-req (effect (remove #{"HQ" "R&D" "Archives"} targets)) :static-abilities [{:type :cannot-run-on-server :req (req (rezzed? card)) - :value (effect (second (get-zone card)))}] + :value (effect [(second (get-zone card))])}] :events [{:event :successful-run :req (req (= :hq (target-server context))) :async true diff --git a/src/clj/game/core/access.clj b/src/clj/game/core/access.clj index a9928c6bd7..6830041d87 100644 --- a/src/clj/game/core/access.clj +++ b/src/clj/game/core/access.clj @@ -1,28 +1,28 @@ (ns game.core.access (:require + [clojure.set :as clj-set] + [clojure.string :as string] [game.core.agendas :refer [update-all-advancement-requirements update-all-agenda-points]] [game.core.board :refer [all-active]] - [game.core.card :refer [agenda? condition-counter? corp? get-agenda-points get-card get-zone in-archives-root? in-deck? in-discard? in-hand? in-hq-root? in-remote-root? in-rd-root? in-scored? operation? rezzed?]] + [game.core.card :refer [agenda? condition-counter? corp? get-agenda-points get-card get-title get-zone in-archives-root? in-deck? in-discard? in-hand? in-hq-root? in-rd-root? in-remote-root? in-scored? operation? rezzed?]] [game.core.card-defs :refer [card-def]] - [game.core.cost-fns :refer [card-ability-cost trash-cost steal-cost]] - [game.core.effects :refer [any-effects register-static-abilities register-lingering-effect sum-effects unregister-lingering-effects]] + [game.core.cost-fns :refer [card-ability-cost steal-cost trash-cost]] + [game.core.effects :refer [any-effects register-lingering-effect register-static-abilities sum-effects unregister-lingering-effects]] [game.core.eid :refer [complete-with-result effect-completed make-eid]] - [game.core.engine :refer [ability-as-handler can-trigger? checkpoint register-pending-event pay queue-event register-default-events resolve-ability should-trigger? trigger-event trigger-event-simult trigger-event-sync unregister-floating-events]] + [game.core.engine :refer [ability-as-handler can-trigger? checkpoint pay queue-event register-default-events register-pending-event resolve-ability should-trigger? trigger-event-simult trigger-event-sync unregister-floating-events]] [game.core.finding :refer [find-cid]] - [game.core.flags :refer [can-access? can-access-loud can-steal? can-trash? card-flag-fn? card-flag?]] + [game.core.flags :refer [can-access-loud can-access? can-steal? can-trash? card-flag-fn? card-flag?]] [game.core.moving :refer [move trash]] - [game.core.payment :refer [add-cost-label-to-ability build-cost-string can-pay? merge-costs ->c]] + [game.core.payment :refer [->c add-cost-label-to-ability build-cost-string can-pay? merge-costs]] [game.core.revealing :refer [reveal]] [game.core.say :refer [play-sfx system-msg multi-msg]] [game.core.servers :refer [get-server-type name-zone zone->name]] [game.core.to-string :refer [card-str]] [game.core.update :refer [update!]] - [game.utils :refer [quantify same-card?]] [game.macros :refer [continue-ability effect req wait-for]] + [game.utils :refer [quantify same-card?]] [jinteki.utils :refer [add-cost-to-label]] - [clojure.set :as clj-set] - [medley.core :refer [find-first]] - [clojure.string :as string])) + [medley.core :refer [find-first]])) (defn no-trash-or-steal [state] @@ -255,7 +255,7 @@ ;; Can't steal or pay, or won't pay additional costs to steal (= target "No action") (do (when-not (find-cid (:cid card) (:deck corp)) - (system-msg state side (str "decides to not pay to steal " (:title card)))) + (system-msg state side (str "declines to pay to steal " (:title card)))) (access-end state side eid card)) ;; Steal normally diff --git a/src/clj/game/core/actions.clj b/src/clj/game/core/actions.clj index b5941a08bc..56aa2264de 100644 --- a/src/clj/game/core/actions.clj +++ b/src/clj/game/core/actions.clj @@ -568,8 +568,8 @@ (wait-for (pay state side (make-eid state eid) card total-cost) (dotimes [_ times-pump] (resolve-ability state side (dissoc pump-ability :cost :msg) (get-card state card) nil)) - (let [payment-eid async-result - payment-str (:msg payment-eid) + (let [payment-eid (make-eid state (select-keys async-result [:cost-paid])) + payment-str (:msg async-result) sub-groups-to-break (if (pos? subs-broken-at-once) (partition subs-broken-at-once subs-broken-at-once nil (remove :broken (:subroutines current-ice))) [(remove :broken (:subroutines current-ice))])] diff --git a/src/clj/game/core/charge.clj b/src/clj/game/core/charge.clj index a1f691d05c..4f24691e9e 100644 --- a/src/clj/game/core/charge.clj +++ b/src/clj/game/core/charge.clj @@ -3,16 +3,16 @@ [game.core.board :refer [all-installed]] [game.core.card :refer :all] [game.core.eid :refer [effect-completed]] - [game.core.say :refer [system-msg]] - [game.macros :refer [effect msg]] - [game.core.props :refer [add-counter]])) + [game.core.props :refer [add-counter]] + [game.macros :refer [effect]] + [jinteki.i18n :refer [simple-msg]])) (defn can-charge "A card can be charged if it has at least one power counter" ([state side] (let [cards (all-installed state side)] (some #(can-charge state side %) cards))) - ([state side card] + ([state _ card] (pos? (get-counters (get-card state card) :power)))) (defn charge-card @@ -34,5 +34,8 @@ :prompt "Choose an installed card" :choices {:card #(can-charge state side %)} :async true - :msg (msg "charge " (:title target) (when (> n 1) (str n " times"))) + :msg (simple-msg + {:effect/type :charge-card + :effect/count n + :effect/card-str target}) :effect (effect (charge-card state side eid target n))}))) diff --git a/src/clj/game/core/costs.clj b/src/clj/game/core/costs.clj index 8f43da859f..df527ea76f 100644 --- a/src/clj/game/core/costs.clj +++ b/src/clj/game/core/costs.clj @@ -1,29 +1,36 @@ (ns game.core.costs + {:clj-kondo/ignore [:unused-binding]} (:require - [game.core.bad-publicity :refer [gain-bad-publicity bad-publicity-available lose-bad-publicity]] + [clojure.string :as str] + [game.core.bad-publicity :refer [bad-publicity-available gain-bad-publicity lose-bad-publicity]] [game.core.board :refer [all-active all-active-installed all-installed all-installed-runner-type]] [game.core.card :refer [active? agenda? corp? facedown? get-card get-counters get-zone hardware? has-subtype? ice? in-hand? installed? program? resource? rezzed? runner?]] [game.core.card-defs :refer [card-def]] [game.core.damage :refer [damage]] - [game.core.eid :refer [complete-with-result make-eid]] - [game.core.engine :refer [checkpoint queue-event resolve-ability]] [game.core.effects :refer [any-effects is-disabled-reg?]] + [game.core.eid :refer [complete-with-result make-eid]] + [game.core.engine :refer [queue-event resolve-ability]] [game.core.flags :refer [is-scored?]] [game.core.gaining :refer [deduct lose]] [game.core.moving :refer [discard-from-hand flip-facedown forfeit mill move trash trash-cards]] - [game.core.payment :refer [handler label payable? value stealth-value]] + [game.core.payment :refer [handler label payable? stealth-value value]] [game.core.pick-counters :refer [pick-credit-providing-cards pick-credit-reducers pick-virus-counters-to-spend]] [game.core.props :refer [add-counter add-prop]] [game.core.revealing :refer [reveal reveal-and-queue-event]] [game.core.rezzing :refer [derez]] + [game.core.schemas :as schemas] [game.core.shuffling :refer [shuffle!]] - [game.core.tags :refer [lose-tags gain-tags]] + [game.core.tags :refer [gain-tags lose-tags]] [game.core.to-string :refer [card-str]] - [game.core.update :refer [update!]] [game.core.virus :refer [number-of-virus-counters]] [game.macros :refer [continue-ability effect req wait-for]] [game.utils :refer [enumerate-cards enumerate-str quantify same-card?]])) +(defn- complete-payment [state side eid result] + (let [payment (assoc result :paid/side side)] + (schemas/assert payment schemas/Payment) + (complete-with-result state side eid payment))) + (defn- can-forfeit? [card] (not (get-in card [:flags :cannot-forfeit]))) ;; Click @@ -57,9 +64,10 @@ ;; and so we can look through the events and figure out WHICH abilities were used ;; I don't think it will break anything (swap! state assoc-in [side :register :spent-click] true) - (complete-with-result state side eid {:paid/msg (str "spends " (label cost)) - :paid/type :click - :paid/value (value cost)}))) + (complete-payment state side eid + {:paid/msg (str "spends " (label cost)) + :paid/type :click + :paid/value (value cost)}))) ;; Lose Click (defn lose-click-label @@ -79,9 +87,10 @@ (deduct state side [:click (value cost)]) (queue-event state (if (= side :corp) :corp-spent-click :runner-spent-click) {:value (value cost)}) (swap! state assoc-in [side :register :spent-click] true) - (complete-with-result state side eid {:paid/msg (str "loses " (lose-click-label cost)) - :paid/type :lose-click - :paid/value (value cost)})) + (complete-payment state side eid + {:paid/msg (str "loses " (lose-click-label cost)) + :paid/type :lose-click + :paid/value (value cost)})) (defn- all-active-pay-credit-cards [state side eid card] @@ -197,28 +206,30 @@ (let [pay-async-result async-result] (queue-event state (if (= side :corp) :corp-spent-credits :runner-spent-credits) {:value updated-cost}) (swap! state update-in [:stats side :spent :credit] (fnil + 0) updated-cost) - (complete-with-result state side eid - {:paid/msg (str "pays " (:msg pay-async-result)) - :paid/type :credit - :paid/value (:number pay-async-result) - :paid/targets (:targets pay-async-result)}))) + (complete-payment state side eid + {:paid/msg (str "pays " (:msg pay-async-result)) + :paid/type :credit + :paid/value (:number pay-async-result) + :paid/targets (:targets pay-async-result)}))) (pos? updated-cost) (do (lose state side :credit updated-cost) (queue-event state (if (= side :corp) :corp-spent-credits :runner-spent-credits) {:value updated-cost}) (swap! state update-in [:stats side :spent :credit] (fnil + 0) updated-cost) - (complete-with-result state side eid {:paid/msg (str "pays " updated-cost " [Credits]") - :paid/type :credit - :paid/value updated-cost})) + (complete-payment state side eid + {:paid/msg (str "pays " updated-cost " [Credits]") + :paid/type :credit + :paid/value updated-cost})) :else - (complete-with-result state side eid {:paid/msg "pays 0 [Credits]" - :paid/type :credit - :paid/value 0})))))) + (complete-payment state side eid + {:paid/msg "pays 0 [Credits]" + :paid/type :credit + :paid/value 0})))))) ;; 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)) -(defmethod label :x-credits [_] (str "X [Credits]")) +(defmethod label :x-credits [_] "X [Credits]") (defmethod payable? :x-credits [cost state side eid card] (and (>= (total-available-credits state side eid card) (or (->> cost :cost/offset) 0)) @@ -250,25 +261,28 @@ (pos? (count (provider-func)))) (wait-for (resolve-ability state side (pick-credit-providing-cards provider-func eid cost stealth-value) card nil) (swap! state update-in [:stats side :spent :credit] (fnil + 0) cost) - (complete-with-result state side eid {:paid/msg (str "pays " (:msg async-result)) - :paid/type :x-credits - :paid/x-value (- (:number async-result) - offset) - :paid/value (:number async-result) - :paid/targets (:targets async-result)})) + (complete-payment state side eid + {:paid/msg (str "pays " (:msg async-result)) + :paid/type :x-credits + :paid/x-value (- (:number async-result) + offset) + :paid/value (:number async-result) + :paid/targets (:targets async-result)})) (pos? cost) (do (lose state side :credit cost) (queue-event state (if (= side :corp) :corp-spent-credits :runner-spent-credits) {:value cost}) (swap! state update-in [:stats side :spent :credit] (fnil + 0) cost) - (complete-with-result state side eid {:paid/msg (str "pays " cost " [Credits]") - :paid/type :x-credits - :paid/x-value (- cost offset) - :paid/value cost})) + (complete-payment state side eid + {:paid/msg (str "pays " cost " [Credits]") + :paid/type :x-credits + :paid/x-value (- cost offset) + :paid/value cost})) :else - (complete-with-result state side eid {:paid/msg (str "pays 0 [Credits]") - :paid/type :x-credits - :paid/x-value (- offset) - :paid/value 0}))))} + (complete-payment state side eid + {:paid/msg "pays 0 [Credits]" + :paid/type :x-credits + :paid/x-value (- offset) + :paid/value 0}))))} card nil))) ;; Expend Helper - this is a dummy cost just for cost strings @@ -284,11 +298,11 @@ (assoc (get-card state card) :seen true) {:cause :ability-cost :unpreventable true :suppress-checkpoint true}) - (complete-with-result state side eid - {:paid/msg (str "trashes " (:title card) " from HQ") - :paid/type :expend - :paid/value 1 - :paid/targets [card]}))) + (complete-payment state side eid + {:paid/msg (str "trashes " (:title card) " from HQ") + :paid/type :expend + :paid/value 1 + :paid/targets [card]}))) ;; Trash (defmethod value :trash-can [cost] (:cost/amount cost)) @@ -302,10 +316,11 @@ (wait-for (trash state side card {:cause :ability-cost :unpreventable true :suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "trashes " (:printed-title card)) - :paid/type :trash-can - :paid/value 1 - :paid/targets [card]}))) + (complete-payment state side eid + {:paid/msg (str "trashes " (:printed-title card)) + :paid/type :trash-can + :paid/value 1 + :paid/targets [card]}))) ;; like trash can, but does not fire trash can abilities (defmethod value :trash-self [cost] (:cost/amount cost)) @@ -319,10 +334,11 @@ (wait-for (trash state side card {:cause :ability-cost :unpreventable true :suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "trashes " (:printed-title card)) - :paid/type :trash-self - :paid/value 1 - :paid/targets [card]}))) + (complete-payment state side eid + {:paid/msg (str "trashes " (:printed-title card)) + :paid/type :trash-self + :paid/value 1 + :paid/targets [card]}))) ;; Forfeit (defmethod value :forfeit [cost] (:cost/amount cost)) @@ -347,7 +363,7 @@ ;; everything is queued, then we perform the actual checkpoint. (forfeit state side (make-eid state eid) agenda {:msg false :suppress-checkpoint true})) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "forfeits " (quantify (value cost) "agenda") " (" (enumerate-cards targets :sorted) ")") @@ -366,7 +382,7 @@ [_cost state side eid card] (wait-for (forfeit state side (make-eid state eid) card {:msg false :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "forfeits " (:title card)) :paid/type :forfeit-self @@ -390,19 +406,21 @@ :card select-fn} :async true :effect (effect (wait-for - (reveal state side targets) - (wait-for - (trash-cards state side (map #(assoc % :seen true) targets) - {:unpreventable true :cause :ability-cost :suppress-checkpoint true}) - (complete-with-result - state side eid - {:paid/msg (str "reveals and trashes " (quantify (count async-result) "card") - " (" (enumerate-cards targets :sorted) ")" - " from " hand) - :paid/type :trash-from-hand - :paid/value (count async-result) - :paid/targets async-result}))))} - forfeit-an-agenda {:prompt (str "Choose an Agenda to forfeit") + (reveal state side targets) + (wait-for + [trashed-cards (trash-cards state side (mapv #(assoc % :seen true) targets) + {:unpreventable true + :cause :ability-cost + :suppress-checkpoint true})] + (complete-payment + state side eid + {:paid/msg (str "reveals and trashes " (quantify (count trashed-cards) "card") + " (" (enumerate-cards targets :sorted) ")" + " from " hand) + :paid/type (if (= :corp side) :reveal-trash-from-hq :reveal-trash-from-grip) + :paid/value (count trashed-cards) + :paid/targets trashed-cards}))))} + forfeit-an-agenda {:prompt "Choose an Agenda to forfeit" :async true :choices {:max 1 :all true @@ -411,7 +429,7 @@ :effect (effect (doseq [agenda targets] (forfeit state side (make-eid state eid) agenda {:msg false :suppress-checkpoint true})) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "forfeits an agenda (" (enumerate-cards targets :sorted) ")") :paid/type :forfeit @@ -444,9 +462,10 @@ (defmethod handler :gain-tag [cost state side eid card] (wait-for (gain-tags state side (value cost) {:suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "takes " (quantify (value cost) "tag")) - :paid/type :gain-tag - :paid/value (value cost)}))) + (complete-payment state side eid + {:paid/msg (str "takes " (quantify (value cost) "tag")) + :paid/type :gain-tag + :paid/value (value cost)}))) ;; Tag (defmethod value :tag [cost] (:cost/amount cost)) @@ -457,9 +476,10 @@ (defmethod handler :tag [cost state side eid card] (wait-for (lose-tags state side (value cost) {:suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "removes " (quantify (value cost) "tag")) - :paid/type :tag - :paid/value (value cost)}))) + (complete-payment state side eid + {:paid/msg (str "removes " (quantify (value cost) "tag")) + :paid/type :tag + :paid/value (value cost)}))) ;; X Tags (defmethod value :x-tags [_] 0) @@ -471,10 +491,10 @@ (defmethod handler :x-tags [cost state side eid card] (if (<= (get-in @state [:runner :tag :base] 0) 0) - (complete-with-result + (complete-payment state side eid {:paid/msg "removes 0 tags" - :paid/type :x-tags + :paid/type :tag :paid/value 0}) (continue-ability state side @@ -483,16 +503,16 @@ :choices {:number (effect (get-in @state [:runner :tag :base] 0))} :effect (effect (let [cost target] (if (zero? target) - (complete-with-result + (complete-payment state side eid {:paid/msg "removes 0 tags" - :paid/type :x-tags + :paid/type :tag :paid/value 0}) (wait-for (lose-tags state side eid target {:suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "removes " (quantify cost "tag")) - :paid/type :x-tags + :paid/type :tag :paid/value cost})))))} card nil))) @@ -506,24 +526,27 @@ [cost state side eid card] (if-not (<= 0 (- (get-in @state [:runner :tag :base] 0) (value cost))) (wait-for (gain-bad-publicity state side (make-eid state eid) (value cost) {:suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "gains " (value cost) " bad publicity") - :paid/type :tag-or-bad-pub - :paid/value (value cost)})) + (complete-payment state side eid + {:paid/msg (str "gains " (value cost) " bad publicity") + :paid/type :gain-bad-publicity + :paid/value (value cost)})) (continue-ability state side {:prompt "Choose one" :choices [(str "Remove " (quantify (value cost) "tag")) (str "Gain " (value cost) " bad publicity")] :async true - :effect (effect (if (= target (str "Gain " (value cost) " bad publicity")) + :effect (effect (if (str/includes? target "bad publicity") (wait-for (gain-bad-publicity state side (make-eid state eid) (value cost) {:suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "gains " (value cost) " bad publicity") - :paid/type :tag-or-bad-pub - :paid/value (value cost)})) + (complete-payment state side eid + {:paid/msg (str "gains " (value cost) " bad publicity") + :paid/type :bad-publicity + :paid/value (value cost)})) (wait-for (lose-tags state side (value cost) {:suppress-checkpoint true}) - (complete-with-result state side eid {:paid/msg (str "removes " (quantify (value cost) "tag")) - :paid/type :tag-or-bad-pub - :paid/value (value cost)}))))} + (complete-payment state side eid + {:paid/msg (str "removes " (quantify (value cost) "tag")) + :paid/type :tag + :paid/value (value cost)}))))} card nil))) ;; ReturnToHand @@ -535,11 +558,11 @@ (defmethod handler :return-to-hand [cost state side eid card] (move state side card :hand) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "returns " (:title card) " to " (if (= :corp side) "HQ" "[their] grip")) - :paid/type :return-to-hand + :paid/type (if (= :corp side) :return-to-hq :return-to-grip) :paid/value 1 :paid/targets [card]})) @@ -552,7 +575,7 @@ (defmethod handler :remove-from-game [cost state side eid card] (let [c (move state side card :rfg)] - (complete-with-result + (complete-payment state side eid {:paid/msg (str "removes " (:title card) " from the game") :paid/type :remove-from-game @@ -579,7 +602,7 @@ :async true :effect (effect (doseq [t targets] (move state side (assoc-in t [:persistent :from-cid] (:cid card)) :rfg)) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "removes " (quantify (value cost) "installed program") " from the game" @@ -612,11 +635,11 @@ :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :suppress-checkpoint true :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "installed card") " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :trash-other-installed + :paid/type :trash-installed :paid/value (count async-result) :paid/targets targets})))} card nil)) @@ -643,7 +666,7 @@ :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :suppress-checkpoint true :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "installed card") " (" (enumerate-str (map #(card-str state %) targets)) ")") @@ -671,49 +694,16 @@ :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :suppress-checkpoint true :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "installed piece") " of hardware" " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :hardware + :paid/type :trash-hardware :paid/value (count async-result) :paid/targets targets})))} card nil)) -;; DerezOtherHarmonic - this may NOT target the source card (itself) -(defmethod value :derez-other-harmonic [cost] (:cost/amount cost)) -(defmethod label :derez-other-harmonic [cost] - (str "derez " (value cost) " Harmonic ice")) -(defmethod payable? :derez-other-harmonic - [cost state side eid card] - (<= 0 (- (count (filter #(and (rezzed? %) - (has-subtype? % "Harmonic") - (not (same-card? card %))) - (all-active-installed state :corp))) (value cost)))) -(defmethod handler :derez-other-harmonic - [cost state side eid card] - (continue-ability - state side - {:prompt (str "Choose " (value cost) " Harmonic ice to derez") - :choices {:all true - :max (value cost) - :card #(and (rezzed? %) - (not (same-card? % card)) - (has-subtype? % "Harmonic"))} - :async true - ;; TODO - once derez is async, fix this - :effect (effect (wait-for - (derez state side targets {:suppress-checkpoint true :no-msg true}) - (complete-with-result - state side eid - {:paid/msg (str "derezzes " (count targets) - " Harmonic ice (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :derez - :paid/value (count targets) - :paid/targets targets})))} - card nil)) - ;; TrashInstalledProgram (defmethod value :program [cost] (:cost/amount cost)) (defmethod label :program [cost] @@ -733,11 +723,11 @@ :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :suppress-checkpoint true :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "installed program") " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :program + :paid/type :trash-program :paid/value (count async-result) :paid/targets targets})))} card nil)) @@ -761,11 +751,11 @@ :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :suppress-checkpoint true :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "installed resource") " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :resource + :paid/type :trash-resource :paid/value (count async-result) :paid/targets targets})))} card nil)) @@ -773,7 +763,7 @@ ;; TrashInstalledConnection (defmethod value :connection [cost] (:cost/amount cost)) (defmethod label :connection [cost] - (str "trash " (str "trash " (quantify (value cost) "installed connection resource")))) + (str "trash " (quantify (value cost) "installed connection resource"))) (defmethod payable? :connection [cost state side eid card] (<= 0 (- (count (filter #(has-subtype? % "Connection") (all-active-installed state :runner))) (value cost)))) @@ -792,11 +782,11 @@ :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :suppress-checkpoint true :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "installed connection resource") " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :connection + :paid/type :trash-connection :paid/value (count async-result) :paid/targets targets})))} card nil)) @@ -804,7 +794,7 @@ ;; TrashRezzedIce (defmethod value :ice [cost] (:cost/amount cost)) (defmethod label :ice [cost] - (str "trash " (str "trash " (quantify (value cost) "installed rezzed ice" "")))) + (str "trash " (quantify (value cost) "installed rezzed ice" ""))) (defmethod payable? :ice [cost state side eid card] (<= 0 (- (count (filter (every-pred installed? rezzed? ice?) (all-installed state :corp))) (value cost)))) @@ -820,11 +810,11 @@ :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :suppress-checkpoint true :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "installed rezzed ice" "") " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :ice + :paid/type :trash-ice :paid/value (count async-result) :paid/targets targets})))} card nil)) @@ -853,11 +843,11 @@ :async true :effect (effect (wait-for (trash-cards state side targets {:cause :ability-cost :unpreventable true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) " rezzed Bioroid" "") " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :bioroid-run-server + :paid/type :trash-bioroid :paid/value (count async-result) :paid/targets targets})))} card nil)) @@ -872,12 +862,12 @@ (defmethod handler :trash-from-deck [cost state side eid card] (wait-for (mill state side side (value cost) {:suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "card") " from the top of " (if (= :corp side) "R&D" "the stack")) - :paid/type :trash-from-deck + :paid/type (if (= :corp side) :trash-from-rd :trash-from-stack) :paid/value (count async-result) :paid/targets async-result}))) @@ -901,14 +891,14 @@ :card select-fn} :async true :effect (effect (wait-for (trash-cards state side targets {:unpreventable true :seen false :cause :ability-cost :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "card") (when (and (= :runner side) (pos? (count async-result))) (str " (" (enumerate-str (map #(card-str state %) targets)) ")")) " from " hand) - :paid/type :trash-from-hand + :paid/type (if (= :corp side) :trash-from-hq :trash-from-grip) :paid/value (count async-result) :paid/targets async-result})))} nil nil))) @@ -923,14 +913,14 @@ (defmethod handler :randomly-trash-from-hand [cost state side eid card] (wait-for (discard-from-hand state side side (value cost) {:suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "card") (when (= side :runner) (str " (" (enumerate-cards async-result :sorted) ")")) " randomly from " (if (= :corp side) "HQ" "the grip")) - :paid/type :randomly-trash-from-hand + :paid/type (if (= :corp side) :randomly-trash-from-hq :randomly-trash-from-grip) :paid/value (count async-result) :paid/targets async-result}))) @@ -948,12 +938,12 @@ (wait-for (reveal state side to-trash) (wait-for (trash-cards state side to-trash {:unpreventable true :cause :ability-cost}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "reveals and trashes " (quantify (count async-result) "card") " (" (enumerate-cards to-trash :sorted) ")" " from " hand) - :paid/type :reveal-and-randomly-trash-from-hand + :paid/type (if (= :corp side) :randomly-reveal-trash-from-hq :randomly-reveal-trash-from-grip) :paid/value (count async-result) :paid/targets async-result}))))) @@ -966,14 +956,14 @@ [cost state side eid card] (let [cards (get-in @state [side :hand])] (wait-for (trash-cards state side cards {:unpreventable true :suppress-checkpoint true :cause :ability-cost}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes all (" (count async-result) ") cards in " (if (= :runner side) "[their] grip" "HQ") (when (and (= :runner side) (pos? (count async-result))) (str " (" (enumerate-cards async-result :sorted) ")"))) - :paid/type :trash-entire-hand + :paid/type (if (= :corp side) :trash-all-cards-in-hq :trash-all-cards-in-grip) :paid/value (count async-result) :paid/targets async-result})))) @@ -994,13 +984,13 @@ :max (value cost) :card (every-pred hardware? in-hand?)} :effect (effect (wait-for (trash-cards state side targets {:unpreventable true :suppress-checkpoint true :cause :ability-cost}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "piece") " of hardware" " (" (enumerate-cards targets :sorted) ")" " from [their] grip") - :paid/type :trash-hardware-from-hand + :paid/type :trash-hardware-in-grip :paid/value (count async-result) :paid/targets async-result})))} nil nil)) @@ -1022,12 +1012,12 @@ :max (value cost) :card (every-pred program? in-hand?)} :effect (effect (wait-for (trash-cards state side targets {:unpreventable true :cause :ability-cost :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "program") " (" (enumerate-cards targets :sorted) ")" " from the grip") - :paid/type :trash-program-from-hand + :paid/type :trash-program-in-grip :paid/value (count async-result) :paid/targets async-result})))} nil nil)) @@ -1049,12 +1039,12 @@ :max (value cost) :card (every-pred resource? in-hand?)} :effect (effect (wait-for (trash-cards state side targets {:unpreventable true :cause :ability-cost :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "trashes " (quantify (count async-result) "resource") " (" (enumerate-cards targets :sorted) ")" " from the grip") - :paid/type :trash-resource-from-hand + :paid/type :trash-resource-in-hq :paid/value (count async-result) :paid/targets async-result})))} nil nil)) @@ -1068,7 +1058,7 @@ (defmethod handler :net [cost state side eid card] (wait-for (damage state side :net (value cost) {:unpreventable true :card card :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "suffers " (count async-result) " net damage") :paid/type :net @@ -1084,7 +1074,7 @@ (defmethod handler :meat [cost state side eid card] (wait-for (damage state side :meat (value cost) {:unpreventable true :card card :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "suffers " (count async-result) " meat damage") :paid/type :meat @@ -1100,10 +1090,10 @@ (defmethod handler :brain [cost state side eid card] (wait-for (damage state side :brain (value cost) {:unpreventable true :card card :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "suffers " (count async-result) " core damage") - :paid/type :brain + :paid/type :core :paid/value (count async-result) :paid/targets async-result}))) @@ -1126,12 +1116,13 @@ :async true :effect (effect (let [cards (keep #(move state side % :deck {:shuffled true}) targets)] (shuffle! state side :deck) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "shuffles " (quantify (count cards) "card") - " (" (enumerate-cards cards :sorted) ")" + (when (= :runner side) + (str " (" (enumerate-cards cards :sorted) ")")) " into " (if (= :corp side) "R&D" "the stack")) - :paid/type :shuffle-installed-to-stack + :paid/type (if (= :corp side) :shuffle-installed-to-rd :shuffle-installed-to-stack) :paid/value (count cards) :paid/targets cards})))} nil nil)) @@ -1154,15 +1145,16 @@ :all true :card (every-pred installed? (if (= :corp side) corp? runner?))} :async true - :effect (effect (let [cards (keep #(move state side % :deck) targets)] - (complete-with-result - state side eid - {:paid/msg (str "adds " (quantify (count cards) "installed card") - " to the bottom of " deck - " (" (enumerate-str (map #(card-str state %) targets)) ")") - :paid/type :add-installed-to-bottom-of-deck - :paid/value (count cards) - :paid/targets cards})))} + :effect (effect + (let [cards (vec (keep #(move state side % :deck) targets))] + (complete-payment + state side eid + {:paid/msg (str "adds " (quantify (count cards) "installed card") + " to the bottom of " deck + " (" (enumerate-str (map #(card-str state %) targets)) ")") + :paid/type (if (= :corp side) :add-installed-to-bottom-of-rd :add-installed-to-bottom-of-stack) + :paid/value (count cards) + :paid/targets cards})))} nil nil))) ;; TurnHostedMatryoshkaFacedown @@ -1180,7 +1172,7 @@ selected (take (value cost) (filter pred (:hosted (get-card state card))))] (doseq [c selected] (flip-facedown state side c)) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "turns "(quantify (value cost) "hosted cop" "y" "ies") " of Matryoshka facedown") @@ -1202,11 +1194,11 @@ chosen (take (value cost) (shuffle hand))] (doseq [c chosen] (move state side c :deck)) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "adds " (quantify (value cost) "random card") " to the bottom of " deck) - :paid/type :add-random-from-hand-to-bottom-of-deck + :paid/type (if (= :corp side) :add-random-from-hand-to-bottom-of-rd :add-random-from-hand-to-bottom-of-stack) :paid/value (value cost) :paid/targets chosen}))) @@ -1229,7 +1221,7 @@ (same-card? (:host target) card))} :async true :effect (effect (let [cards (keep #(move state :corp % :hand) targets)] - (complete-with-result + (complete-payment state side eid {:paid/msg (str "adds " (quantify (count cards) "hosted card") " to HQ (" (enumerate-cards cards :sorted) ")") @@ -1255,12 +1247,12 @@ :async true :effect (effect (wait-for (add-counter state side target :agenda (- (value cost)) {:suppress-checkpoint true}) (queue-event state :agenda-counter-spent {:value (value cost)}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "spends " - (quantify (value cost) (str "hosted agenda counter")) + (quantify (value cost) "hosted agenda counter") " from on " (:title target)) - :paid/type :any-agenda-counter + :paid/type :agenda-counter :paid/value (value cost) :paid/targets [target]})))} nil nil)) @@ -1274,13 +1266,13 @@ (<= 0 (- (number-of-virus-counters state) (value cost)))) (defmethod handler :any-virus-counter [cost state side eid card] - (wait-for (resolve-ability state side (pick-virus-counters-to-spend (value cost)) card nil) - (complete-with-result - state side eid - {:paid/msg (str "spends " (:msg async-result)) - :paid/type :any-virus-counter - :paid/value (:number async-result) - :paid/targets (:targets async-result)}))) + (wait-for [{:keys [msg number targets]} (resolve-ability state side (pick-virus-counters-to-spend (value cost)) card nil)] + (complete-payment + state side eid + {:paid/msg (str "spends " msg) + :paid/type :virus-counter + :paid/value number + :paid/targets targets}))) ;; AdvancementCounter (defmethod value :advancement [cost] (:cost/amount cost)) @@ -1294,13 +1286,14 @@ (defmethod handler :advancement [cost state side eid card] (wait-for (add-prop state side card :advance-counter (- (value cost)) {:placed true :suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "spends " - (quantify (value cost) (str "hosted advancement counter")) + (quantify (value cost) "hosted advancement counter") " from on " (:title card)) - :paid/type :advancement - :paid/value (value cost)}))) + :paid/type :advancement-counter + :paid/value (value cost) + :paid/targets [card]}))) ;; AgendaCounter (defmethod value :agenda [cost] (:cost/amount cost)) @@ -1315,13 +1308,14 @@ [cost state side eid card] (wait-for (add-counter state side card :agenda (- (value cost)) {:suppress-checkpoint true}) (queue-event state :agenda-counter-spent {:value (value cost)}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "spends " (quantify (value cost) "hosted agenda counter") " from on " (:title card)) - :paid/type :agenda - :paid/value (value cost)}))) + :paid/type :agenda-couner + :paid/value (value cost) + :paid/targets [card]}))) ;; PowerCounter (defmethod value :power [cost] (:cost/amount cost)) @@ -1335,21 +1329,22 @@ (defmethod handler :power [cost state side eid card] (wait-for (add-counter state side card :power (- (value cost)) {:suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "spends " (quantify (value cost) "hosted power counter") " from on " (:title card)) - :paid/type :power - :paid/value (value cost)}))) + :paid/type :power-counter + :paid/value (value cost) + :paid/targets [card]}))) ;; XPowerCounter -(defmethod value :x-power [_] 0) -(defmethod label :x-power [_] "X hosted power counters") -(defmethod payable? :x-power +(defmethod value :x-power-counter [_] 0) +(defmethod label :x-power-counter [_] "X hosted power counters") +(defmethod payable? :x-power-counter [cost state side eid card] (pos? (get-counters card :power))) -(defmethod handler :x-power +(defmethod handler :x-power-counter [cost state side eid card] (continue-ability state side @@ -1359,13 +1354,14 @@ :effect (effect (let [cost target] (wait-for (add-counter state side card :power (- cost) {:suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "spends " (quantify cost "hosted power counter") " from on " (:title card)) - :paid/type :x-power - :paid/value cost}))))} + :paid/type :power-counter + :paid/value cost + :paid/targets [card]}))))} card nil)) ;; VirusCounter @@ -1388,36 +1384,53 @@ (filter #(= "Hivemind" (:title %))) (keep #(get-counters % :virus)) (reduce +))) - (wait-for (resolve-ability state side (pick-virus-counters-to-spend card (value cost)) card nil) - (complete-with-result - state side eid - {:paid/msg (str "spends " (:msg async-result)) - :paid/type :virus - :paid/value (:number async-result) - :paid/targets (:targets async-result)})) + (wait-for [{:keys [msg number targets]} (resolve-ability state side (pick-virus-counters-to-spend card (value cost)) card nil)] + (complete-payment + state side eid + {:paid/msg (str "spends " msg) + :paid/type :virus-counter + :paid/value number + :paid/targets targets})) (let [title (:title card)] (wait-for (add-counter state side card :virus (- (value cost)) {:suppress-checkpoint true}) - (complete-with-result + (complete-payment state side eid {:paid/msg (str "spends " - (quantify (value cost) (str "hosted virus counter")) + (quantify (value cost) "hosted virus counter") " from on " title) - :paid/type :virus - :paid/value (value cost)}))))) + :paid/type :virus-counter + :paid/value (value cost) + :paid/targets [card]}))))) -(defmethod value :host-bad-pub [cost] (:cost/amount cost)) -(defmethod label :host-bad-pub [cost] (str "host " (value cost) " bad publicity")) -(defmethod payable? :host-bad-pub +;; DerezOtherHarmonic - this may NOT target the source card (itself) +(defmethod value :derez-other-harmonic [cost] (:cost/amount cost)) +(defmethod label :derez-other-harmonic [cost] + (str "derez " (value cost) " Harmonic ice")) +(defmethod payable? :derez-other-harmonic [cost state side eid card] - (<= 0 (- (get-in @state [:corp :bad-publicity :base] 0) (value cost)))) -(defmethod handler :host-bad-pub + (<= 0 (- (count (filter #(and (rezzed? %) + (has-subtype? % "Harmonic") + (not (same-card? card %))) + (all-active-installed state :corp))) (value cost)))) +(defmethod handler :derez-other-harmonic [cost state side eid card] - (wait-for - (lose-bad-publicity state side (value cost) nil) - (wait-for - (add-counter state side card :bad-publicity (value cost) {:suppress-checkpoint true}) - (complete-with-result - state side eid - {:paid/msg (str "hosts " (value cost) " bad publicity on " (:title card)) - :paid/type :host-bad-pub - :paid/value (value cost)})))) + (continue-ability + state side + {:prompt (str "Choose " (value cost) " Harmonic ice to derez") + :choices {:all true + :max (value cost) + :card #(and (rezzed? %) + (not (same-card? % card)) + (has-subtype? % "Harmonic"))} + :async true + ;; TODO - once derez is async, fix this + :effect (effect (wait-for + (derez state side targets {:suppress-checkpoint true :no-msg true}) + (complete-payment + state side eid + {:paid/msg (str "derezzes " (count targets) + " Harmonic ice (" (enumerate-str (map #(card-str state %) targets)) ")") + :paid/type :derez-harmonic + :paid/value (count targets) + :paid/targets targets})))} + card nil)) diff --git a/src/clj/game/core/def_helpers.clj b/src/clj/game/core/def_helpers.clj index 9ea613df6c..2de17c8705 100644 --- a/src/clj/game/core/def_helpers.clj +++ b/src/clj/game/core/def_helpers.clj @@ -3,18 +3,18 @@ [clojure.string :as str] [game.core.access :refer [access-bonus]] [game.core.board :refer [all-installed get-all-cards]] - [game.core.card :refer [active? can-be-advanced? corp? faceup? get-card get-counters has-subtype? in-discard? in-hand? installed? operation? runner? ]] + [game.core.card :refer [can-be-advanced? corp? faceup? get-card get-counters has-subtype? in-discard? in-hand? installed? operation? runner? ]] [game.core.card-defs :as card-defs] [game.core.choose-one :refer [choose-one-helper]] [game.core.damage :refer [damage]] [game.core.drawing :refer [draw]] [game.core.eid :refer [effect-completed make-eid]] - [game.core.engine :refer [queue-event register-events resolve-ability trigger-event trigger-event-sync unregister-event-by-uuid]] + [game.core.engine :refer [queue-event register-events resolve-ability trigger-event unregister-event-by-uuid]] [game.core.effects :refer [any-effects is-disabled-reg?]] [game.core.gaining :refer [gain-credits lose-credits]] + [jinteki.i18n :refer [simple-msg]] [game.core.installing :refer [corp-install]] [game.core.moving :refer [move trash]] - [game.core.payment :refer [build-cost-string can-pay?]] [game.core.play-instants :refer [async-rfg]] [game.core.prompts :refer [cancellable clear-wait-prompt]] [game.core.props :refer [add-counter add-prop]] @@ -23,8 +23,6 @@ [game.core.say :refer [play-sfx system-msg system-say]] [game.core.servers :refer [zone->name name-zone]] [game.core.shuffling :refer [shuffle! fail-to-find!]] - [game.core.servers :refer [zone->name name-zone]] - [game.core.shuffling :refer [shuffle!]] [game.core.to-string :refer [card-str]] [game.core.toasts :refer [toast]] [game.core.tags :refer [gain-tags]] @@ -228,7 +226,9 @@ (merge {:async true :change-in-game-state {:req (req (can-run-server? state server))} :label (str "run " (zone->name server)) - :msg (str "make a run on " (zone->name server)) + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server server}) :makes-run true :effect (effect (when (seq events) (register-events state side card events)) @@ -246,7 +246,9 @@ :req (req (seq runnable-servers)) :label "Run a server" :makes-run true - :msg (msg "make a run on " target) + :msg (simple-msg + {:effect/type :make-a-run-on + :effect/server target}) :effect (effect (when (seq events) (register-events state side card events)) (when (:action ab-base) @@ -443,9 +445,9 @@ context-card (:card %)] (or (= event event-keyword) (and (#{:play-event :play-operation} event) - (and (not (same-card? card context-card)) - (has-subtype? context-card "Current") - true)))) + (not (same-card? card context-card)) + (has-subtype? context-card "Current") + true))) targets)) :value trash-or-rfg}] (update ability :static-abilities #(conj (into [] %) static-ab))) @@ -513,7 +515,6 @@ ([pred] {:label "add card from Archives to HQ" :prompt "Choose a card to add to HQ" - :does-something (effect (seq (:discard corp))) :waiting-prompt true :show-discard true :choices {:card #(and (corp? %) @@ -525,7 +526,7 @@ (defn tutor-abi "Tutor a card. Optionally, pass a restriction, which is a 1-fn the cards must pass" - ([reveal?] (tutor-abi reveal nil)) + ([reveal?] (tutor-abi reveal? nil)) ([reveal? restriction] {:change-in-game-state {:req (req (seq (get-in @state [side :deck])))} :prompt "Choose a card" @@ -605,10 +606,10 @@ (fn [] (unregister-event-by-uuid state side uuid))) (fn [] nil))) (maybe-reveal - [state side eid card target-side {:keys [event-side forced skip-reveal] :as args}] + [state side eid card target-side {:keys [event-side forced skip-reveal] :as inner-args}] (if skip-reveal (effect-completed state side eid) - (reveal-loud state (or event-side side) eid card args (get-in @state [target-side :hand]))))] + (reveal-loud state (or event-side side) eid card inner-args (get-in @state [target-side :hand]))))] {:async true :effect (effect (wait-for (maybe-reveal state side card target-side args) @@ -625,7 +626,8 @@ ([advanceable-only] (place-advancement-counter advanceable-only 1)) ([advanceable-only qty] (place-advancement-counter advanceable-only qty "a card" nil)) ([advanceable-only qty card-line pred] - (let [label (str "Place " (quantify qty "advancement counter") " on " card-line (if advanceable-only " that can be advanced"))] + (let [label (str "Place " (quantify qty "advancement counter") " on " card-line + (when advanceable-only " that can be advanced"))] {:label label :prompt label :choices {:req (req (corp? target) @@ -673,4 +675,5 @@ (let [ability# (->> ~@(reverse (cons body more)) (add-default-abilities ~title))] (swap! card-defs-cache assoc ~title ability#) - ability#))))) + ability#))) + ~title)) diff --git a/src/clj/game/core/diffs.clj b/src/clj/game/core/diffs.clj index acf0f53f02..22adccc09e 100644 --- a/src/clj/game/core/diffs.clj +++ b/src/clj/game/core/diffs.clj @@ -582,8 +582,8 @@ [{} {}]))) (defn- get-message-diff [old-state new-state side] - (let [old-messages {:log (into [] (keep #(or (side %) (:public %)) (:log old-state)))} - new-messages {:log (into [] (keep #(or (side %) (:public %)) (:log @new-state)))}] + (let [old-messages {:log (into [] (keep #(or (side %) (:public %))) (:log old-state))} + new-messages {:log (into [] (keep #(or (side %) (:public %))) (:log @new-state))}] (fake-log-diff old-messages new-messages))) (defn- diff-and-patch-log [old-state new-state message-diff] diff --git a/src/clj/game/core/drawing.clj b/src/clj/game/core/drawing.clj index bb73706a97..7364c40d63 100644 --- a/src/clj/game/core/drawing.clj +++ b/src/clj/game/core/drawing.clj @@ -56,7 +56,7 @@ (defn draw "Draw n cards from :deck to :hand." ([state side eid n] (draw state side eid n nil)) - ([state side eid n {:keys [suppress-event no-update-draw-stats]}] + ([state side eid n {:keys [suppress-event no-update-draw-stats loud]}] (if (zero? n) (effect-completed state side eid) (wait-for (trigger-event-simult state side (make-eid state eid) (if (= side :corp) :pre-corp-draw :pre-runner-draw) nil {:count n}) @@ -81,35 +81,41 @@ (not (pos? deck-count))) (effect-completed state side eid) (let [to-draw (take draws-after-prevent (get-in @state [side :deck])) - set-aside-eid eid] - (let [drawn (set-aside-for-me state side set-aside-eid to-draw) - drawn-count (count drawn)] - (swap! state update-in [side :register :drawn-this-turn] (fnil #(+ % drawn-count) 0)) - (if (not no-update-draw-stats) - (swap! state update-in [:stats side :gain :card] (fnil + 0) n)) - (if suppress-event - (do + set-aside-eid eid + drawn (set-aside-for-me state side set-aside-eid to-draw) + drawn-count (count drawn)] + (swap! state update-in [side :register :drawn-this-turn] (fnil #(+ % drawn-count) 0)) + (when (not no-update-draw-stats) + (swap! state update-in [:stats side :gain :card] (fnil + 0) n)) + (if suppress-event + (do + (doseq [c (get-set-aside state side set-aside-eid)] + (move state side c :hand)) + (when loud + (system-msg state side {:msg/type :msg-draw-cards + :msg/count drawn-count})) + (effect-completed state side eid)) + (let [draw-event (if (= side :corp) :corp-draw :runner-draw)] + (swap! state update-in [side :register :currently-drawing] conj drawn) + (doseq [c drawn] + (when-let [on-draw (:on-draw (card-def c))] + (register-pending-event + state draw-event c (assoc on-draw :location :set-aside)))) + (queue-event state draw-event {:cards drawn + :count drawn-count}) + (wait-for + (checkpoint state nil (make-eid state eid) nil) (doseq [c (get-set-aside state side set-aside-eid)] (move state side c :hand)) - (effect-completed state side eid)) - (let [draw-event (if (= side :corp) :corp-draw :runner-draw)] - (swap! state update-in [side :register :currently-drawing] conj drawn) - (doseq [c drawn] - (when-let [on-draw (:on-draw (card-def c))] - (register-pending-event - state draw-event c (assoc on-draw :location :set-aside)))) - (queue-event state draw-event {:cards drawn - :count drawn-count}) - (wait-for - (checkpoint state nil (make-eid state eid) nil) - (doseq [c (get-set-aside state side set-aside-eid)] - (move state side c :hand)) - (wait-for (trigger-event-sync state side (make-eid state eid) (if (= side :corp) :post-corp-draw :post-runner-draw) {:count drawn-count}) - (let [eid (make-result eid (-> @state side :register :currently-drawing (peek)))] - (swap! state update-in [side :register :currently-drawing] pop) - (effect-completed state side eid)))))) - (when (safe-zero? (remaining-draws state side)) - (prevent-draw state side)))))))))) + (when loud + (system-msg state side {:msg/type :msg-draw-cards + :msg/count drawn-count})) + (wait-for (trigger-event-sync state side (make-eid state eid) (if (= side :corp) :post-corp-draw :post-runner-draw) {:count drawn-count}) + (let [eid (make-result eid (-> @state side :register :currently-drawing (peek)))] + (swap! state update-in [side :register :currently-drawing] pop) + (effect-completed state side eid)))))) + (when (safe-zero? (remaining-draws state side)) + (prevent-draw state side))))))))) (defn maybe-draw ([state side eid card n] (maybe-draw state side eid card n nil)) diff --git a/src/clj/game/core/effects.clj b/src/clj/game/core/effects.clj index 06d36c37a1..6f80988878 100644 --- a/src/clj/game/core/effects.clj +++ b/src/clj/game/core/effects.clj @@ -53,9 +53,12 @@ ([state side eid] (get-effect-value state side eid nil)) ([state side eid targets] (fn [{:keys [value card]}] - (if (fn? value) - (value state side eid card targets) - value)))) + (let [ret (if (fn? value) + (value state side eid card targets) + value)] + (if (sequential? ret) + (seq ret) + ret))))) (defn get-effects "Filters and then 'executes' the effects of a given type." @@ -65,7 +68,7 @@ (let [eid (make-eid state) targets (cons target targets)] (->> (get-effect-maps state side eid effect-type targets) - (mapv (get-effect-value state side eid targets)))))) + (keep (get-effect-value state side eid targets)))))) (defn get-tagged-effect-value "Returns a function that returns the value of a given effect. If the :value is an fn, diff --git a/src/clj/game/core/engine.clj b/src/clj/game/core/engine.clj index 5236a42a65..316c2c81b6 100644 --- a/src/clj/game/core/engine.clj +++ b/src/clj/game/core/engine.clj @@ -1,27 +1,35 @@ (ns game.core.engine (:require - [clj-uuid :as uuid] - [clojure.string :as string] - [com.noahbogart.cond-plus :refer [cond+]] - [game.core.board :refer [clear-empty-remotes get-all-cards all-installed all-installed-runner - all-installed-runner-type all-active-installed]] - [game.core.card :refer [active? facedown? faceup? get-card get-cid get-title ice? in-discard? in-hand? in-rfg? in-set-aside? installed? rezzed? program? console? unique?]] - [game.core.card-defs :refer [card-def]] - [game.core.effects :refer [get-effect-maps unregister-lingering-effects is-disabled? is-disabled-reg? update-disabled-cards]] - [game.core.eid :refer [complete-with-result effect-completed make-eid]] - [game.core.finding :refer [find-cid]] - [game.core.payment :refer [build-spend-msg can-pay? handler]] - [game.core.prompt-state :refer [add-to-prompt-queue]] - [game.core.prompts :refer [clear-wait-prompt show-prompt show-select show-wait-prompt]] - [game.core.say :refer [system-msg multi-msg system-say n-last-logs]] - [game.core.update :refer [update!]] - [game.core.winning :refer [check-win-by-agenda]] - [game.macros :refer [continue-ability effect wait-for]] - [game.utils :refer [dissoc-in distinct-by enumerate-str in-coll? remove-once same-card? server-cards side-str to-keyword]] - [jinteki.utils :refer [other-side]] - [game.core.memory :refer [update-mu]] - [game.core.to-string :refer [card-str]] - [taoensso.timbre :as timbre])) + [clj-uuid :as uuid] + [clojure.string :as string] + [com.noahbogart.cond-plus :refer [cond+]] + [game.core.board :refer [all-active-installed all-installed + all-installed-runner-type clear-empty-remotes]] + [game.core.card :refer [active? console? facedown? faceup? get-card get-cid + get-title in-discard? in-hand? in-rfg? + in-set-aside? installed? program? rezzed? unique?]] + [game.core.card-defs :refer [card-def]] + [game.core.effects :refer [get-effect-maps is-disabled-reg? is-disabled? + unregister-lingering-effects + update-disabled-cards]] + [game.core.eid :refer [complete-with-result effect-completed make-eid]] + [game.core.finding :refer [find-cid]] + [game.core.memory :refer [update-mu]] + [game.core.payment :refer [build-spend-msg can-pay? handler]] + [game.core.prompt-state :refer [add-to-prompt-queue]] + [game.core.prompts :refer [clear-wait-prompt show-prompt show-select + show-wait-prompt]] + [game.core.say :refer [multi-msg n-last-logs system-msg system-say]] + [game.core.schemas :as schemas] + [game.core.to-string :refer [card-str]] + [game.core.update :refer [update!]] + [game.core.winning :refer [check-win-by-agenda]] + [game.macros :refer [continue-ability effect wait-for]] + [game.utils :refer [dissoc-in distinct-by enumerate-str in-coll? + remove-once same-card? server-cards side-str to-keyword]] + [jinteki.i18n :refer [->effect-msg simple-msg]] + [jinteki.utils :refer [other-side]] + [taoensso.timbre :as timbre])) ;; resolve-ability docs @@ -279,6 +287,10 @@ (let [ability (assoc-in ability [:eid :source] card) ab (select-ability-kw ability) ability-fn (get @ability-types ab)] + ; NOTE (noah, 2026-09): + ; i don't think we want to actually validate all ability maps, that'll lead to weird bugs and we don't have good + ; observability right now. but during dev, please turn this one. + ; (schemas/assert ability schemas/Ability) (cond ab (ability-fn state side ability card targets) choices (check-choices state side ability card targets) @@ -304,13 +316,17 @@ (defn- get-side-message [state side {:keys [eid] :as ability} card targets payment-str] (when-let [message (:msg ability)] - (let [desc (if (or (= :cost message) (string? message)) - message - (message state side eid card targets)) - cost-spend-msg (build-spend-msg payment-str "use")] + (let [side (or (:player ability) side) + desc (cond + (string? message) message + (= :cost message) (->effect-msg {:msg/type :satisfy-card + :msg/payments (vals (:cost-paid eid)) + :msg/title (get-title card)}) + :else (message state side eid card targets))] (cond - (= :cost desc) (str payment-str " to satisfy " (get-title card)) - desc (str cost-spend-msg (get-title card) " to " desc))))) + (map? desc) (assoc desc :side side) + desc (str (build-spend-msg payment-str "use") + (get-title card) " to " desc))))) (defn print-msg "Prints the ability message" @@ -333,14 +349,14 @@ ([state side eid ability card] (do-nothing state side eid ability card nil)) ([state side eid ability card payment-str] (when-not (get-in ability [:change-in-game-state :silent]) - (print-msg state side (assoc ability :msg "do nothing") card [] payment-str)) + (print-msg state side (assoc ability :msg (simple-msg :do-nothing)) card [] payment-str)) (effect-completed state side eid))) (defn- change-in-game-state? "Concession for NCIGS going - uses a 'change-in-game-state' key to check when a card has no potential to do anything through resolving (different to req)" [state side {:keys [eid] :as ability} card targets] - (or (= nil (get-in ability [:change-in-game-state :req])) + (or (nil? (get-in ability [:change-in-game-state :req])) ((get-in ability [:change-in-game-state :req]) state side eid card targets))) (defn- do-effect @@ -358,15 +374,17 @@ ([cost-paid] cost-paid) ([cost-paid1 cost-paid2] (let [costs-paid (concat (vals cost-paid1) (vals cost-paid2))] - (reduce (fn [acc cur] - (let [existing (get acc (:paid/type cur)) - cost-obj {:paid/type (:paid/type cur) - :paid/value (+ (:paid/value existing 0) (:paid/value cur 0)) - :paid/x-value (+ (:paid/x-value existing 0) (:paid/x-value cur 0)) - :paid/targets (seq (concat (:paid/targets existing) (:paid/targets cur)))}] - (assoc acc (:paid/type cur) cost-obj))) - {} - costs-paid))) + (->> costs-paid + (reduce (fn [acc cur] + (let [existing (get acc (:paid/type cur)) + cost-obj {:paid/type (:paid/type cur) + :paid/side (:paid/side cur) + :paid/value (+ (:paid/value existing 0) (:paid/value cur 0)) + :paid/x-value (+ (:paid/x-value existing 0) (:paid/x-value cur 0)) + :paid/targets (seq (concat (:paid/targets existing) (:paid/targets cur)))}] + (assoc! acc (:paid/type cur) cost-obj))) + (transient {})) + (persistent!)))) ([cost-paid1 cost-paid2 & costs-paid] (reduce merge-costs-paid (merge-costs-paid cost-paid1 cost-paid2) costs-paid))) diff --git a/src/clj/game/core/ice.clj b/src/clj/game/core/ice.clj index cae95aebf9..99d3857288 100644 --- a/src/clj/game/core/ice.clj +++ b/src/clj/game/core/ice.clj @@ -592,36 +592,36 @@ card ice)) message (when (seq broken-subs) (break-subroutines-msg ice broken-subs breaker args))] - (wait-for (pay state side card total-cost) - (if-let [payment-str (:msg async-result)] - (do (when (not (string/blank? message)) - (system-msg state :runner (str payment-str " to " message))) - (doseq [sub broken-subs] - (break-subroutine! state (get-card state ice) sub breaker) - (resolve-ability state side (make-eid state {:source card - :source-type :ability}) - (:additional-ability args) - card nil)) - (let [ice (get-card state ice) - on-break-subs (when ice (:on-break-subs (card-def ice))) - event-args (when on-break-subs {:card-abilities (ability-as-handler ice on-break-subs)})] - (when (same-card? ice (get-current-ice state)) - (set-current-ice state ice)) - (if (seq broken-subs) - (wait-for - (trigger-event-simult state side :subroutines-broken event-args (break-subs-event-context state ice broken-subs breaker)) - (let [ice (get-card state ice) - card (get-card state card)] - (if (and ice - card - (not early-exit) - (:repeatable args) - (pos? (count (unbroken-subroutines-choice ice))) - (can-pay? state side eid (get-card state card) nil cost)) - (continue-ability state side (break-subroutines ice breaker cost n args) card nil) - (effect-completed state side eid)))) - (effect-completed state side eid)))) - (effect-completed state side eid))))))}))) + (wait-for [payment (pay state side card total-cost)] + (if-not payment + (effect-completed state side eid) + (do (when (not (string/blank? message)) + (system-msg state :runner (str (:msg payment) " to " message))) + (doseq [sub broken-subs] + (break-subroutine! state (get-card state ice) sub breaker) + (resolve-ability state side (make-eid state {:source card + :source-type :ability}) + (:additional-ability args) + card nil)) + (let [ice (get-card state ice) + on-break-subs (when ice (:on-break-subs (card-def ice))) + event-args (when on-break-subs {:card-abilities (ability-as-handler ice on-break-subs)})] + (when (same-card? ice (get-current-ice state)) + (set-current-ice state ice)) + (if (seq broken-subs) + (wait-for + (trigger-event-simult state side :subroutines-broken event-args (break-subs-event-context state ice broken-subs breaker)) + (let [ice (get-card state ice) + card (get-card state card)] + (if (and ice + card + (not early-exit) + (:repeatable args) + (pos? (count (unbroken-subroutines-choice ice))) + (can-pay? state side eid (get-card state card) nil cost)) + (continue-ability state side (break-subroutines ice breaker cost n args) card nil) + (effect-completed state side eid)))) + (effect-completed state side eid)))))))))}))) (defn add-stealth-to-label [cost] (when-let [cost (find-first #(= :credit (:cost/type %)) (flatten [cost]))] diff --git a/src/clj/game/core/initializing.clj b/src/clj/game/core/initializing.clj index 98b34d0d02..4549c64789 100644 --- a/src/clj/game/core/initializing.clj +++ b/src/clj/game/core/initializing.clj @@ -139,7 +139,8 @@ (not no-mu)) (init-mu-cost state c)) (if (and resolve-effect (is-ability? cdef)) - (resolve-ability state side (assoc eid :source-type :ability) (dissoc cdef :cost :additional-cost) c nil) + (resolve-ability state side (assoc eid :source-type :ability) + (dissoc cdef :cost :additional-cost :events :leave-play :static-abilities) c nil) (effect-completed state side eid)) (when-let [in-play (:in-play cdef)] (apply gain state side in-play)) diff --git a/src/clj/game/core/moving.clj b/src/clj/game/core/moving.clj index 43c474488e..e1533ab270 100644 --- a/src/clj/game/core/moving.clj +++ b/src/clj/game/core/moving.clj @@ -659,7 +659,8 @@ forfeit-ev (if (= :corp side) :corp-forfeit-agenda :runner-forfeit-agenda) moved-card (move state (to-keyword (:side card)) card :rfg)] (when msg - (system-msg state side (str "forfeits " (get-title card)))) + (system-msg state side {:msg/type :msg-forfeit-agenda + :msg/title (get-title card)})) (update-all-agenda-points state side) (check-win-by-agenda state side) (queue-event state forfeit-ev {:card card}) diff --git a/src/clj/game/core/pick_counters.clj b/src/clj/game/core/pick_counters.clj index 1f0696756f..950d1cea02 100644 --- a/src/clj/game/core/pick_counters.clj +++ b/src/clj/game/core/pick_counters.clj @@ -1,28 +1,40 @@ (ns game.core.pick-counters (:require [game.core.bad-publicity :refer [spend-bad-publicity]] - [game.core.card :refer [get-card get-counters has-subtype? installed? runner?]] + [game.core.card :refer [get-card get-counters get-title has-subtype? + installed? runner?]] [game.core.card-defs :refer [card-def]] - [game.core.eid :refer [effect-completed make-eid complete-with-result]] [game.core.effects :refer [any-effects]] - [game.core.engine :refer [resolve-ability queue-event]] + [game.core.eid :refer [complete-with-result make-eid]] + [game.core.engine :refer [queue-event resolve-ability]] [game.core.gaining :refer [lose]] - [game.core.props :refer [add-counter]] [game.core.update :refer [update!]] [game.macros :refer [continue-ability effect wait-for]] [game.utils :refer [enumerate-str in-coll? quantify same-card?]])) +(defn ->pick-targets [selected-cards from-credits-pool bad-pub-spent] + (-> [{:pick-counters/type :credit-pool + :value from-credits-pool}] + (into (when (and bad-pub-spent (pos? bad-pub-spent)) + [{:pick-counters/type :bad-publicity + :value bad-pub-spent}])) + (into (for [[_ selected] selected-cards + :let [{:keys [card number]} selected]] + {:pick-counters/type :card + :title (get-title card) + :value number})))) + (defn- pick-counter-triggers - [state side eid current-cards selected-cards counter-type counter-count message credits] - (if-let [[_ selected] (first current-cards)] - (if-let [{:keys [card number]} selected] - (do (queue-event state :counter-added {:card (get-card state card) :counter-type counter-type :amount number}) - (pick-counter-triggers state side eid (rest current-cards) selected-cards counter-type counter-count message credits)) - (pick-counter-triggers state side eid (rest current-cards) selected-cards counter-type counter-count message credits)) - (complete-with-result state side eid {:number counter-count - :msg message - :credits-spent-from-pool credits - :targets (keep #(:card (second %)) selected-cards)}))) + [state side eid selected-cards counter-type counter-count message credits bad-pub-spent] + (doseq [[_ selected] selected-cards + :when selected + :let [{:keys [card number]} selected]] + (queue-event state :counter-added {:card (get-card state card) + :counter-type counter-type + :amount number})) + (complete-with-result state side eid {:number counter-count + :msg message + :targets (->pick-targets selected-cards credits bad-pub-spent)})) (defn pick-virus-counters-to-spend "Pick virus counters to spend. For use with Freedom Khumalo and virus breakers, and any other relevant cards. @@ -35,7 +47,7 @@ ([specific-card target-count selected-cards counter-count] {:async true :prompt (str "Choose a card with virus counters (" - counter-count (str " of " target-count) + counter-count " of " target-count " virus counters)") :choices {:card #(and (if specific-card (or (same-card? % specific-card) @@ -59,7 +71,7 @@ title (:title card)] (str (quantify number "virus counter") " from " title)) (vals selected-cards)))] - (pick-counter-triggers state side eid selected-cards selected-cards :virus counter-count message 0))))) + (pick-counter-triggers state side eid selected-cards :virus counter-count message 0 0))))) :cancel {:async true :effect (if target-count (effect (doseq [{:keys [card number]} (vals selected-cards)] @@ -69,17 +81,17 @@ title (:title card)] (str (quantify number "virus counter") " from " title)) (vals selected-cards)))] - (complete-with-result state side eid {:number counter-count :msg message}))))}})) + (complete-with-result state side eid {:number counter-count + :msg message + :targets (vals selected-cards)}))))}})) -(defn- trigger-spend-credits-from-cards - [state side eid cards] - (if (seq cards) - (do (queue-event state :spent-credits-from-card {:card (first cards)}) - (trigger-spend-credits-from-cards state side eid (rest cards))) - (effect-completed state side eid))) +(defn- queue-spend-from-cards + [state cards] + (doseq [card cards] + (queue-event state :spent-credits-from-card {:card card}))) (defn- queue-spend-from-bad-pub - [state side spent] + [state spent] (when (and spent (pos? spent)) (queue-event state :bad-publicity-spent {:value spent}))) @@ -114,7 +126,7 @@ :effect (effect (complete-with-result state side eid {:reduction counter-count :targets (keep #(:card (second %)) selected-cards)}))} {:async true - :prompt (str "Choose a cost-reducing card") + :prompt "Choose a cost-reducing card" :choices {:card #(in-coll? (map :cid discount-provider) (:cid %))} :effect (effect (let [pay-credits-type (-> target card-def :interactions :pay-credits :type) pay-function (if (= :custom pay-credits-type) @@ -188,10 +200,10 @@ (let [cards (->> (vals selected-cards) (map :card) (remove #(-> (card-def %) :interactions :pay-credits :cost-reduction)))] - (wait-for (trigger-spend-credits-from-cards state side cards) - (queue-spend-from-bad-pub state side bad-pub-spent) - ;; Now we trigger all of the :counter-added events we'd neglected previously - (pick-counter-triggers state side eid selected-cards selected-cards :credit target-count message remainder)))) + (queue-spend-from-cards state cards) + (queue-spend-from-bad-pub state bad-pub-spent) + ;; Now we trigger all of the :counter-added events we'd neglected previously + (pick-counter-triggers state side eid selected-cards :credit target-count message remainder bad-pub-spent))) (continue-ability state side (pick-credit-providing-cards provider-func eid target-count stealth-target selected-cards uses bad-pub-available bad-pub-spent) @@ -210,9 +222,10 @@ (continue-ability state side (pick-credit-providing-cards - provider-func eid target-count stealth-target selected-cards (when (and (should-auto-repeat? state side) - (> bad-pub-available 1)) - target) + provider-func eid target-count stealth-target selected-cards + (when (and (should-auto-repeat? state side) + (> bad-pub-available 1)) + target) uses (dec bad-pub-available) (inc bad-pub-spent)) card targets) (let [target pre-chosen @@ -247,7 +260,9 @@ (continue-ability state side (pick-credit-providing-cards - provider-func eid target-count stealth-target selected-cards (when (should-auto-repeat? state side) target) uses (dec bad-pub-available) (inc bad-pub-spent)) + provider-func eid target-count stealth-target selected-cards + (when (should-auto-repeat? state side) target) + uses (dec bad-pub-available) (inc bad-pub-spent)) card targets) (let [pay-credits-type (-> target card-def :interactions :pay-credits :type) pay-function (if (= :custom pay-credits-type) diff --git a/src/clj/game/core/play_instants.clj b/src/clj/game/core/play_instants.clj index 3b0e242e37..cf0eb45589 100644 --- a/src/clj/game/core/play_instants.clj +++ b/src/clj/game/core/play_instants.clj @@ -36,7 +36,9 @@ (let [play-msg (if ignore-cost "play " (build-spend-msg payment-str "play"))] - (system-msg state side (str play-msg title (when as-flashback (str " from " (if (= side :corp) "Archives" "the heap"))) (when ignore-cost " at no cost"))) + (system-msg state side (str play-msg title + (when as-flashback (str " from " (if (= side :corp) "Archives" "the heap"))) + (when ignore-cost " at no cost"))) (implementation-msg state card) (set-last-played-or-rezzed state card) (if-let [sfx (:play-sound (card-def card))] diff --git a/src/clj/game/core/prevention.clj b/src/clj/game/core/prevention.clj index 53fdacde29..a083cbf411 100644 --- a/src/clj/game/core/prevention.clj +++ b/src/clj/game/core/prevention.clj @@ -184,10 +184,9 @@ :effect (effect (trigger-prevention state side eid key prevention))}}) (defn- resolve-keyed-prevention-for-side - [state side eid key {:keys [prompt waiting option data-type] :as args}] + [state side eid key {:keys [prompt waiting-prompt option data-type] :as args}] (let [remainder (get-in @state [:prevent key :remaining]) prompt (if (string? prompt) prompt (prompt state remainder)) - waiting (if (string? waiting) waiting (waiting state remainder)) option (if (string? option) option (option state remainder))] (if (or (if (= data-type :sequential) (not (seq remainder)) @@ -208,7 +207,7 @@ state side (choose-one-helper {:prompt prompt - :waiting-prompt waiting} + :waiting-prompt waiting-prompt} (concat (mapv #(build-prevention-option % key) preventions) [(when-not (some :mandatory preventions) {:option option @@ -284,7 +283,7 @@ :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" + :waiting-prompt {:msg/type :waiting-trash-prevention-triggers} :option (fn [state remainder] (str "Continue trashing " (quantify (count (get-in @state [:prevent :trash :remaining])) "card")))})) (defn resolve-trash-prevention @@ -350,10 +349,8 @@ [state] (case (damage-type state) :meat "meat" - :brain "core" - :core "core" - :net "net" - "neat")) + (:brain :core) "core" + :net "net")) (defn prevent-damage [state side eid n] @@ -388,7 +385,7 @@ (if (= side :runner) (str "Prevent " (damage-pending state) " " (damage-name state) " damage?") (str "There is " (damage-pending state) " pending " (damage-name state) " damage"))) - :waiting "your opponent to resolve pre-damage triggers" + :waiting-prompt {:msg/type :waiting-pre-damage-triggers} :option "Pass priority"})) ;; NOTE - PRE-DAMAGE EFFECTS HAPPEN BEFORE DAMAGE EFFECTS, AND ARE THE CONSTANT ABILITIES (IE GURU DAVINDER, MURESH BODYSUIT, THE CLEANERS, ETC) @@ -402,7 +399,7 @@ (if (= side :runner) (str "Prevent " (damage-pending state) " " (damage-name state) " damage?") (str "There is " (damage-pending state) " pending " (damage-name state) " damage"))) - :waiting "your opponent to resolve damage triggers" + :waiting-prompt {:msg/type :waiting-damage-triggers} :option "Pass priority"})) (defn resolve-damage-prevention @@ -425,7 +422,7 @@ (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" + :waiting-prompt {:msg/type :waiting-prevent-when-encountered} :option (fn [state remainder] (str "Allow " (get-in @state [:prevent :encounter :title])))})) (defn resolve-encounter-prevention @@ -445,7 +442,7 @@ (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" + :waiting-prompt {:msg/type :waiting-prevent-run-ending} :option "Allow the run to end"})) (defn resolve-end-run-prevention @@ -473,7 +470,7 @@ (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" + :waiting-prompt {:msg/type :waiting-prevent-jacking-out} :option "Allow the Runner to jack out"})) (defn resolve-jack-out-prevention @@ -512,7 +509,7 @@ 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" + :waiting-prompt {:msg/type :waiting-prevent-expose} :option (fn [state remainder] (str "Allow " (quantify (count remainder) "card") " to be exposed"))})) (defn resolve-expose-prevention @@ -545,7 +542,7 @@ {: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" + :waiting-prompt {:msg/type :waiting-prevent-bad-publicity} :option (fn [state remainder] (str "Allow " remainder " bad publicity"))})) (defn resolve-bad-pub-prevention @@ -581,7 +578,7 @@ {: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" + :waiting-prompt {:msg/type :waiting-prevent-tags} :option (fn [state remainder] (str "Allow " (quantify remainder "remaining tag")))})) (defn resolve-tag-prevention diff --git a/src/clj/game/core/prompts.clj b/src/clj/game/core/prompts.clj index 22ae0e2d53..a09b696b47 100644 --- a/src/clj/game/core/prompts.clj +++ b/src/clj/game/core/prompts.clj @@ -1,13 +1,14 @@ (ns game.core.prompts (:require [clj-uuid :as uuid] - [clojure.string :as str] [game.core.board :refer [get-all-cards]] [game.core.eid :refer [effect-completed make-eid]] - [game.core.prompt-state :refer [add-to-prompt-queue remove-from-prompt-queue]] + [game.core.prompt-state :refer [add-to-prompt-queue + remove-from-prompt-queue]] [game.core.toasts :refer [toast]] [game.macros :refer [when-let*]] [game.utils :refer [pluralize side-str]] + [jinteki.i18n :refer [->msg-map]] [medley.core :refer [find-first]])) (defn choice-parser @@ -61,10 +62,12 @@ {:eid (select-keys eid [:eid]) :card card :prompt-type :waiting - :msg (str "Waiting for " - (if (true? waiting-prompt) - (str (side-str side) " to make a decision") - waiting-prompt))})) + :msg (let [msg (if (keyword? waiting-prompt) + waiting-prompt + (if (= :corp side) + {:msg/type :waiting-corp-default} + {:msg/type :waiting-runner-default}))] + (->msg-map msg))})) (add-to-prompt-queue state side newitem))))) (defn show-prompt-with-dice diff --git a/src/clj/game/core/rezzing.clj b/src/clj/game/core/rezzing.clj index daeda79e34..8b51245756 100644 --- a/src/clj/game/core/rezzing.clj +++ b/src/clj/game/core/rezzing.clj @@ -1,25 +1,30 @@ (ns game.core.rezzing (:require - [clojure.string :as string] - [game.core.card :refer [asset? condition-counter? get-card ice? rezzed? upgrade?]] - [game.core.card-defs :refer [card-def]] - [game.core.cost-fns :refer [rez-additional-cost-bonus rez-cost]] - [game.core.effects :refer [is-disabled? unregister-static-abilities update-disabled-cards]] - [game.core.eid :refer [complete-with-result effect-completed make-eid]] - [game.core.engine :refer [register-pending-event queue-event checkpoint pay register-events resolve-ability trigger-event unregister-events]] - [game.core.flags :refer [can-host? can-rez?]] - [game.core.ice :refer [update-ice-strength]] - [game.core.initializing :refer [card-init deactivate]] - [game.core.moving :refer [trash-cards]] - [game.core.payment :refer [build-spend-msg can-pay? merge-costs ->c]] - [game.core.revealing :refer [set-last-played-or-rezzed]] - [game.core.runs :refer [continue]] - [game.core.say :refer [play-sfx system-msg implementation-msg]] - [game.core.toasts :refer [toast]] - [game.core.to-string :refer [card-str]] - [game.core.update :refer [update!]] - [game.macros :refer [continue-ability effect wait-for]] - [game.utils :refer [enumerate-str make-timestamp to-keyword]])) + [clojure.string :as string] + [game.core.card :refer [asset? condition-counter? get-card ice? rezzed? + upgrade?]] + [game.core.card-defs :refer [card-def]] + [game.core.cost-fns :refer [rez-additional-cost-bonus rez-cost]] + [game.core.effects :refer [is-disabled? unregister-static-abilities + update-disabled-cards]] + [game.core.eid :refer [complete-with-result effect-completed make-eid]] + [game.core.engine :refer [checkpoint pay queue-event register-events + register-pending-event resolve-ability + unregister-events]] + [game.core.flags :refer [can-host? can-rez?]] + [game.core.ice :refer [update-ice-strength]] + [game.core.initializing :refer [card-init deactivate]] + [game.core.moving :refer [trash-cards]] + [game.core.payment :refer [->c build-spend-msg can-pay? merge-costs]] + [game.core.revealing :refer [set-last-played-or-rezzed]] + [game.core.runs :refer [continue]] + [game.core.say :refer [implementation-msg play-sfx system-msg]] + [game.core.to-string :refer [card-str]] + [game.core.toasts :refer [toast]] + [game.core.update :refer [update!]] + [game.macros :refer [continue-ability effect wait-for]] + [game.utils :refer [enumerate-str make-timestamp to-keyword]] + [jinteki.i18n :refer [->use-card-msg]])) (defn get-rez-cost [state side card {:keys [ignore-cost alternative-cost cost-bonus]}] @@ -200,23 +205,29 @@ ;; I suggest only using this if the rhs thing is part of the same instruction. ;; include-cost-from-eid [eid] - include the last payment str from the eid as if it was for this [state side eid cards {:keys [and-then] :as msg-keys}] - (let [card-strs (enumerate-str (map #(card-str state % {:visible true}) cards)) - prepend-cost-str (get-in msg-keys [:include-cost-from-eid :latest-payment-str]) + (let [prepend-cost-str (get-in msg-keys [:include-cost-from-eid :latest-payment-str]) source-card (:source eid) - title (or (:title source-card) (:printed-title source-card))] + cards (mapv #(assoc % :visible true) cards)] (system-msg state side (cond - (not source-card) (str "derezzes " card-strs and-then) - prepend-cost-str (str prepend-cost-str " to use " title " to derez " card-strs and-then) - :else (str "uses " title " to derez " card-strs and-then))))) + source-card (->use-card-msg + source-card + (cond-> [{:effect/type :derez-cards + :effect/card-strs cards}] + (map? and-then) (conj and-then)) + (when prepend-cost-str (seq (vals (:cost-paid eid))))) + (= 1 (count cards)) {:msg/type :msg-derez-card + :msg/card-str (first cards)} + :else {:msg/type :msg-derez-cards + :msg/card-strs cards})))) (defn derez "Derez a number of corp cards." ([state side eid cards] (derez state side eid cards nil)) ([state side eid cards {:keys [suppress-checkpoint no-event no-msg msg-keys] :as args}] - (let [cards (vec (keep #(let [c (get-card state %)] - (and (rezzed? c) c)) + (let [cards (vec (keep #(when-let [c (get-card state %)] + (when (rezzed? c) c)) (if (sequential? cards) (flatten cards) [cards])))] diff --git a/src/clj/game/core/runs.clj b/src/clj/game/core/runs.clj index bbfc725368..a8fba683bc 100644 --- a/src/clj/game/core/runs.clj +++ b/src/clj/game/core/runs.clj @@ -55,12 +55,12 @@ ([state side card] (get-runnable-zones state side (make-eid state) card nil)) ([state side card args] (get-runnable-zones state side (make-eid state) card args)) ([state side eid card {:keys [zones ignore-costs]}] - (let [restricted-zones (distinct (flatten (get-effects state side :cannot-run-on-server))) - permitted-zones (remove (set restricted-zones) (or zones (get-zones state)))] + (let [restricted-zones (into [] (comp cat (distinct)) (get-effects state side :cannot-run-on-server)) + permitted-zones (into [] (remove (set restricted-zones)) (or zones (get-zones state)))] (if ignore-costs permitted-zones - (filter #(can-pay? state :runner eid card nil (total-run-cost state side card {:server (unknown->kw %)})) - permitted-zones))))) + (filterv #(can-pay? state :runner eid card nil (total-run-cost state side card {:server (unknown->kw %)})) + permitted-zones))))) (defn can-run-server? [state server] @@ -128,46 +128,44 @@ (when click-run (swap! state assoc-in [:runner :register :made-click-run] true) (play-sfx state side "click-run")) - (wait-for - (pay state :runner (make-eid state eid) nil costs) - (let [payment-str (:msg async-result)] - (if-not payment-str - (effect-completed state side eid) - (let [s [(if (keyword? server) server (last (server->zone state server)))] - ices (get-in @state (concat [:corp :servers] s [:ices])) - n (count ices)] - (when (not-empty payment-str) - (system-msg state :runner (str (build-spend-msg payment-str "make a run on" "makes a run on") - (zone->name (unknown->kw server)) - (when ignore-costs ", ignoring all costs")))) - ;; s is a keyword for the server, like :hq or :remote1 - (let [run-id (make-eid state)] - (swap! state assoc - :per-run nil - :run {:run-id run-id - :server s - :position n - :corp-auto-no-action false - :phase :initiation - :eid eid - :current-ice nil - :events nil - :source-card (select-keys card [:code :cid :zone :title :side :type :art :implementation])}) - (when card - (update! state side (assoc-in card [:special :run-id] run-id)))) - (show-run-prompts state (str "running on " (zone->name (unknown->kw server))) card) - (wait-for - (gain-run-credits state side - (make-eid state eid) - (get-in @state [:runner :next-run-credit] 0)) - (swap! state assoc-in [:run :bad-publicity-available] (count-bad-pub state)) - (swap! state assoc-in [:runner :next-run-credit] 0) - (swap! state update-in [:runner :register :made-run] conj (first s)) - (swap! state update-in [:stats side :runs :started] (fnil inc 0)) - (queue-event state :run {:server s - :position n - :cost-args cost-args}) - (end-of-phase-checkpoint state nil (make-eid state eid) :end-of-initiation))))))))))) + (wait-for [{payment-str :msg cost-paid :cost-paid} (pay state :runner (make-eid state eid) nil costs)] + (if-not payment-str + (effect-completed state side eid) + (let [s [(if (keyword? server) server (last (server->zone state server)))] + ices (get-in @state (concat [:corp :servers] s [:ices])) + n (count ices)] + (when (not-empty payment-str) + (system-msg state :runner (str (build-spend-msg payment-str "make a run on" "makes a run on") + (zone->name (unknown->kw server)) + (when ignore-costs ", ignoring all costs")))) + ;; s is a keyword for the server, like :hq or :remote1 + (let [run-id (make-eid state)] + (swap! state assoc + :per-run nil + :run {:run-id run-id + :server s + :position n + :corp-auto-no-action false + :phase :initiation + :eid eid + :current-ice nil + :events nil + :source-card (select-keys card [:code :cid :zone :title :side :type :art :implementation])}) + (when card + (update! state side (assoc-in card [:special :run-id] run-id)))) + (show-run-prompts state (str "running on " (zone->name (unknown->kw server))) card) + (wait-for + (gain-run-credits state side + (make-eid state eid) + (get-in @state [:runner :next-run-credit] 0)) + (swap! state assoc-in [:run :bad-publicity-available] (count-bad-pub state)) + (swap! state assoc-in [:runner :next-run-credit] 0) + (swap! state update-in [:runner :register :made-run] conj (first s)) + (swap! state update-in [:stats side :runs :started] (fnil inc 0)) + (queue-event state :run {:server s + :position n + :cost-args cost-args}) + (end-of-phase-checkpoint state nil (make-eid state eid) :end-of-initiation)))))))))) (defmethod continue :initiation [state side _] @@ -670,7 +668,7 @@ {:prompt (str "You are prevented from breaching " (zone->name server) " this run.") :choices ["OK"] :async true - :effect (effect (system-msg state :runner (str "is prevented from breaching " (zone->name server) " this run.")) + :effect (effect (system-msg state :runner (str "is prevented from breaching " (zone->name server) " this run")) (handle-end-run state side eid))} nil nil) diff --git a/src/clj/game/core/say.clj b/src/clj/game/core/say.clj index 7294b19cbc..e6275c71f7 100644 --- a/src/clj/game/core/say.clj +++ b/src/clj/game/core/say.clj @@ -1,8 +1,11 @@ (ns game.core.say (:require [cljc.java-time.instant :as inst] + [clojure.pprint :as pprint] [clojure.string :as str] - [game.core.toasts :refer [toast]])) + [game.core.toasts :refer [toast]] + [jinteki.cards :refer [all-cards]] + [jinteki.i18n :refer [->msg-map]])) (defn make-message "Create a message map, along with timestamp if none is provided." @@ -52,10 +55,11 @@ (= side :corp) corp-pronoun (= side :runner) runner-pronoun :else "their")] - (-> text - (str/replace #"(\[pronoun\])|(\[their\])" user-pronoun) - (str/replace #"\[corp-pronoun\]" corp-pronoun) - (str/replace #"\[runner-pronoun\]" runner-pronoun)))) + (when text + (-> text + (str/replace #"(\[pronoun\])|(\[their\])" user-pronoun) + (str/replace #"\[corp-pronoun\]" corp-pronoun) + (str/replace #"\[runner-pronoun\]" runner-pronoun))))) (defn- log [state message] @@ -63,23 +67,25 @@ (defn say "Prints a message to the log as coming from the given user." - ([state side args] (say state side args :public)) - ([state side {:keys [user text]} log-side] + ([state side msg] (say state side msg nil)) + ([state side {:keys [user text]} {:keys [log-side] + :or {log-side :public}}] (let [author (or user (get-in @state [side :user])) - message (make-message {:user author :text (insert-pronouns state side text)})] - (let [log-sides (flatten [log-side])] - (log state (zipmap log-sides (repeat message))))))) - -(defn- multi-say - [state side message-map] - (let [message (update-vals message-map #(make-system-message (insert-pronouns state side %)))] - (log state message))) + message (make-message {:user author + :text (if (string? text) + (insert-pronouns state side text) + (update text :raw-text #(insert-pronouns state side %)))}) + log-sides (if (vector? log-side) log-side [log-side])] + (log state (zipmap log-sides (repeat message)))))) (defn system-say "Prints a system message to log (`say` from user __system__)" ([state side text] (system-say state side text nil)) - ([state side text {:keys [hr log-side]}] - (say state side (make-system-message (str text (when hr "[hr]"))) (or log-side :public)))) + ([state side text {:keys [hr log-side] :as args}] + (say state side (make-system-message (merge {:username nil} + (if (string? text) {:raw-text text} text))) + args) + (when hr (say state side (make-system-message {:raw-text "[hr]"}) args)))) (defn unsafe-say "Prints a reagent hiccup directly to the log. Do not use for any user-generated content!" @@ -87,18 +93,178 @@ (let [message (make-system-message text)] (log state {:public message}))) +(defonce store (atom #{})) + +(comment + (reset! store #{})) + +(def card-names + (->> (keys @all-cards) + (sort) + (reverse) + (str/join "|") + (#(str "(" % ")")) + (re-pattern))) + +(def type-names + (->> (vals @all-cards) + (map :type) + (sort) + (reverse) + (str/join "|") + (#(str "(" % ")")) + (re-pattern))) + +(comment + (->> @store + (filter #(str/includes? % "and draw"))) + + (->> @store + (map #(-> % + + ;; names + (str/replace "realloc()" "{$card}") + (str/replace "Ghost Runner" "{$card}") + (str/replace "Masterwork (v37)" "{$card}") + (str/replace "Gemilang Arena: Burning Bright" "{$card}") + (str/replace "AirbladeX (JSRF Ed.)" "{$card}") + (str/replace #"Dewi Subrotoputri: (Pedagogical Dhalang|Shadow Guide)" "{\\$card}") + (str/replace #"Hoshiko Shiro: (Untold Protagonist|Mahou Shoujo)" "{\\$card}") + (str/replace #"(Subsurface Labs|Tenure Floors|Disposal Grounds): Méliès U" "{\\$card}") + (str/replace #"[Cc]orp ?" "{\\$side} ") + (str/replace #"[Rr]unner ?" "{\\$side} ") + (str/replace card-names "{\\$card}") + (str/replace type-names "{\\$type}") + (str/replace #"\{\$type\}s" "{\\$type}") + (str/replace #"(the )?[Gg]rip" "the grip") + (str/replace #"(the )?[Ss]tack" "the stack") + (str/replace #"(the )?[Hh]eap" "the heap") + (str/replace #"(Archives|HQ|R&D)" "{\\$central}") + (str/replace #"Server \d+" "{\\$server}") + + ;; common subtypes + (str/replace #"(Code Gate|Barrier|Sentry|Harmonic)" "{\\$subtype}") + + ;; contains numbers + (str/replace #"position \d+" "{\\$position}") + (str/replace #"turn \d+" "{\\$turn}") + (str/replace #"sabotage \d+" "{\\$sabotage}") + (str/replace #"guess \d+" "{\\$guess}") + (str/replace #"(-|\+)?\d+-cost card" "{\\$target-cost-card}") + + ;; quantities + (str/replace #"\d+ +\[[Cc]redits?\]" "{\\$credits}") + (str/replace #" all +\[[Cc]redits?\]" " {\\$all-credits}") + (str/replace #"\d+ *\[[Rr]ecurring [Cc]redits?\]" "{\\$recurring-credits}") + (str/replace #"(-|\+)?\d+ fewer (\[[Cc]licks?\])+" "{\\$fewer-clicks}") + (str/replace #"(-|\+)?\d+ allotted (\[[Cc]licks?\])+" "{\\$allotted-clicks}") + (str/replace #" all +(\[[Cc]licks?\])+" " {\\$all-clicks}") + (str/replace #" all +remaining +(\[[Cc]licks?\])+" " {\\$all-remaining-clicks}") + (str/replace #" ((\d+|a) )?(\[[Cc]licks?\])+" " {\\$clicks}") + (str/replace #"\d+ +(\[tags?\])+" "{\\$tags}") + (str/replace #" (\d+|a) +cards?" " {\\$cards}") + (str/replace #"\d+ +(\[cards?\])+" "{\\$cards}") + (str/replace #"\d+ +random +(\[cards?\])+" "{\\$random-cards}") + (str/replace #"\d+ +random +cards?" "{\\$random-cards}") + (str/replace #"\d+ +(copy|copies)" "{\\$copies}") + (str/replace #" all +(\[cards?\])+" " {\\$all-cards}") + (str/replace #"\d+ +virus counters?" "{\\$virus-counters}") + (str/replace #"\d+ +(meat) damage" "{\\$meat-damages}") + (str/replace #"\d+ +(net) damage" "{\\$net-damages}") + (str/replace #"\d+ +(brain|core) damage" "{\\$core-damages}") + (str/replace #"\d+ +tags?" "{\\$tags}") + (str/replace #" all +tags?" " {\\$all-tags}") + (str/replace #"\d+ +turns?" "{\\$turns}") + (str/replace #"\d+ +extra turns?" "{\\$extra-turns}") + (str/replace #" (\d+|a) +additional turns?" " {\\$additional-turns}") + (str/replace #" all( +\d+)? +subroutines?" " {\\$all-subroutines}") + (str/replace #"\d+ +subroutines?" "{\\$subroutines}") + (str/replace #"\d+ +unbroken +subroutines? on \{\$card\} \(.*?\)" "{\\$unbroken-subroutines} on {\\$card} ({\\$subs})") + (str/replace #"\d+ +\{\$subtype\} ice" "{\\$subtype} {\\$ices}") + (str/replace #"\d+ +\{\$subtype\} subroutines?" "{\\$subtype} {\\$subroutines}") + (str/replace #"\d+ +bad publicity" "{\\$bad-publicity}") + (str/replace #" all +bad publicity" " {\\$all-bad-publicity}") + (str/replace #" (\d+|an) additional +bad publicity" " {\\$additional-bad-publicity}") + (str/replace #"\d +installed cards?" "{\\$installed-cards}") + (str/replace #"\d +installed programs?" "{\\$installed-programs}") + (str/replace #" (-|\+)?(\d+|an) +agenda( points?)?" " {\\$agendas}") + (str/replace #" \{\$agendas\} worth \{\$agendas\}" " {\\$type} worth {\\$agenda-points}") + (str/replace #" an \{\$type\} worth \d+ points" " an agenda worth {\\$agenda-points}") + (str/replace #" ((\d+|a) )?pieces? of ice" " {\\$ices}") + (str/replace #" (\d|an) +installed non-ice" " {\\$installed-non-ice}") + (str/replace #" (\d+|an) +advancement counters?" " {\\$advancement-counters}") + (str/replace #" (\d+|an) +additional advancement counters?" " {\\$advancement-counters}") + (str/replace #" (\d+|a) +hosted advancement counters?" " {\\$hosted-advancement-counters}") + (str/replace #" all +hosted advancement counters?" " {\\$all-hosted-advancement-counters}") + (str/replace #" (\d+|a) +agenda counters?" " {\\$agenda-counters}") + (str/replace #" all +agenda counters?" " {\\$all-agenda-counters}") + (str/replace #" (\d+|a) +hosted agenda counters?" " {\\$hosted-agenda-counters}") + (str/replace #" all +hosted agenda counters?" " {\\$all-hosted-agenda-counters}") + (str/replace #" (\d+|a) +charge counters?" " {\\$charge-counters}") + (str/replace #" (\d+|a) +power counters?" " {\\$power-counters}") + (str/replace #" (\d+|a) +hosted power counters?" " {\\$hosted-power-counters}") + (str/replace #" all +hosted power counters?" " {\\$all-hosted-power-counters}") + (str/replace #" (\d+|a) +hosted virus counters?" " {\\$virus-counters}") + (str/replace #" all virus counters?" " {\\$all-virus-counters}") + (str/replace #" (\d+|an) +additional cards?" " {\\$additional-cards}") + (str/replace #" (\d|a) +counter" " {\\$counters}") + + ;; phrases + (str/replace #"(for|at) no cost" "at no cost") + (str/replace #"(-|\+)?\d+ strength" "{\\$str}") + (str/replace #"give \{\$card\} (-|\+)?\d+ strength" "give {\\$card} {\\$str}") + (str/replace #"link strength to \d+" "link strength to {\\$link-str}") + (str/replace #"trash( the)? top (cards?|\{\$cards?\})( (of|from) (\{\$central\}|the stack))?" + "trash the top {\\$cards} from {\\$deck}") + (str/replace #"trashes \d+ installed rezzed ice" "trashes {\\$installed-rezzed-ices}") + (str/replace #"start a psi game \(.*?\)" "start a psi game ({\\$ability-text})") + (str/replace #"increase( its)? strength from \d+ to \d+" "increase its strength from {\\$start-str} to {\\$end-str}") + (str/replace #"increase( the)? strength of \{\$card\} to \d+" "increase the strength of {\\$card} to {\\$end-str}") + (str/replace #"increase( the)? strength of \{\$card\} from \d+ to \d+" "increase the strength of {\\$card} from {\\$start-str} to {\\$end-str}") + + (str/replace #"\{\$card\}(,?( and)? \{\$card\})+" "{\\$enumerated-cards}") + + ;; subroutines + (str/replace #"to break( \d+)? \{\$subroutines\} on \{\$card\} \((.*?)\)\." "to break {\\$subroutines} on {\\$card} ({\\$subroutine-text}).") + (str/replace #"to break( \d+)? \{\$subtype\} \{\$subroutines\} on \{\$card\} \((.*?)\)\." "to break {\\$subtype} {\\$subroutines} on {\\$card} ({\\$subroutine-text}).") + (str/replace #"to resolve the subroutine \((.*?)\) from" "to resolve the subroutine ({\\$subroutine-text}) from") + + ;; traces + (str/replace #"initiate a trace with strength \d+ \((.*)\)" "initiate a trace with {\\$trace-strength} ({\\$trace-text})") + (str/replace #"initiate a trace with strength \d+" "initiate a trace with {\\$trace-strength}") + (str/replace #"increase trace strength to \d+" "increase trace strength to {\\$trace-strength}") + + )) + (sort) + (distinct) + (vec) + (pprint/pprint))) + +(defn ^:private prep-msg + [state side msg] + (let [username (get-in @state [side :user :username])] + (cond + (map? msg) (->msg-map msg {:username username + :side side + :msg/username username}) + (string? msg) {:username username + :side side + :raw-text (str username " " msg ".")} + :else (throw (ex-info "malformed message" {:side side + :msg msg}))))) + (defn system-msg "Prints a message to the log without a username." - ([state side text] (system-msg state side text nil)) - ([state side text args] - (let [username (get-in @state [side :user :username])] - (system-say state side (str username " " text ".") args)))) + ([state side msg] (system-msg state side msg nil)) + ([state side msg args] + (let [msg' (prep-msg state side msg)] + (system-say state side msg' args)))) (defn multi-msg [state side message-map] - (let [username (get-in @state [side :user :username]) - message-map (update-vals message-map #(str username " " % "."))] - (multi-say state side message-map))) + (doseq [[log-side text] message-map + :let [message (prep-msg state side text)]] + (system-say state side message {:log-side log-side}))) (defn enforce-msg "Prints a message related to a rules enforcement on a given card. diff --git a/src/clj/game/core/schemas.cljc b/src/clj/game/core/schemas.cljc new file mode 100644 index 0000000000..3a2ab4e638 --- /dev/null +++ b/src/clj/game/core/schemas.cljc @@ -0,0 +1,644 @@ +(ns game.core.schemas + "this must not pull in any game.core namespaces, it's gotta be standalone" + (:refer-clojure :exclude [assert]) + (:require + [clojure.string :as str] + [game.core.card :refer [card?]] + [malli.core :as m] + [malli.error :as me] + [malli.util :as mu])) + +(defmacro assert + "malli schema assert that throws an ex-info. schema comes last to allow for threading" + [value schema] + `(let [schema# ~schema + value# ~value] + (if (m/validate schema# value#) + value# + (let [msg# (->> (me/humanize (m/explain schema# value#)) + (str/join \newline))] + (throw (ex-info msg# {:schema '~(symbol (resolve schema)) + :value value#})))))) + +;; engine schemas + +(def Card + [:fn {:error/message "should be a card"} card?]) + +(def Cost + (m/schema + [:map {:closed true} + [:cost/type :keyword] + [:cost/amount :int] + [:cost/additional :boolean] + [:cost/stealth [:maybe [:or :int [:enum :all-stealth]]]] + [:cost/maximum [:maybe [:or :int fn?]]] + [:cost/offset [:maybe :int]] + [:cost/args [:maybe :map]]])) + +(def PickCounters + (m/schema + [:map {:closed true} + [:pick-counters/type :keyword] + [:value :int] + [:title {:optional true} :string]])) + +(def Payment + (m/schema + [:map {:closed true} + [:paid/type :keyword] + [:paid/side [:enum :corp :runner]] + [:paid/msg {:optional true} :string] + [:paid/value :int] + [:paid/x-value {:optional true} :int] + [:paid/targets {:optional true} [:maybe [:sequential [:or Card PickCounters]]]]])) + +(def Eid + (m/schema + [:map {:closed true} + [:eid :int] + [:source {:optional true} [:maybe Card]] + [:source-type {:optional true} :keyword] + [:source-info {:optional true} + [:maybe [:map {:closed true} + [:ability-idx {:optional true} :int] + [:ability-targets [:maybe [:sequential :any]]]]]] + [:action {:optional true} [:maybe [:or :keyword :string]]] + [:additional-costs {:optional true} [:maybe [:sequential Cost]]] + [:cost-paid {:optional true} [:maybe [:map-of :keyword Payment]]] + [:latest-payment-str {:optional true} [:maybe :string]] + [:result {:optional true} :any]])) + +(def Ability + (m/schema + [:schema {:registry + {::msg-core [:or :string fn? [:enum :cost]] + ::msg [:or + [:ref ::msg-core] + [:map + [:corp {:optional true} [:maybe [:ref ::msg-core]]] + [:runner {:optional true} [:maybe [:ref ::msg-core]]]]] + ::cigs [:or fn? + [:map {:closed true} + [:req fn?] + [:silent {:optional true} [:maybe [:or :boolean fn?]]] + [:pay-cost {:optional true} [:maybe :boolean]]]] + ::choices [:or fn? + [:enum :credit :counter] + [:sequential [:or Card :string :nil]] + [:map + [:number fn?] + [:default {:optional true} fn?]] + [:map + [:card {:optional true} fn?] + [:req {:optional true} fn?] + [:all {:optional true} [:or :boolean fn?]] + [:min {:optional true} [:or :int fn?]] + [:max {:optional true} [:or :int fn?]] + [:not-self {:optional true} :boolean]]] + ::waiting-prompt [:maybe [:or :boolean [:map [:msg/type :keyword]]]] + ::once [:maybe [:enum :per-turn :per-run :per-encounter]] + ::ability + [:map {:closed true} + [:req {:optional true} [:maybe fn?]] + [:effect {:optional true} [:maybe fn?]] + [:cancel-effect {:optional true} [:maybe [:ref ::ability]]] + [:msg {:optional true} [:maybe [:ref ::msg]]] + [:implementation {:optional true} :string] + [:eid {:optional true} Eid] + [:ability-name {:optional true} :string] + [:trash? {:optional true} :boolean] + [:show-discard {:optional true} :boolean] + [:show-opponent-discard {:optional true} :boolean] + [:action {:optional true} :boolean] + [:cost-label {:optional true} [:maybe [:or :string fn?]]] + ;; icebreakers + [:break-req {:optional true} fn?] + [:break {:optional true} [:or :int fn?]] + [:breaks {:optional true} [:set :string]] + [:break-cost {:optional true} [:maybe [:or :int Cost [:sequential Cost]]]] + [:auto-break-sort {:optional true} [:maybe :int]] + [:break-cost-bonus {:optional true} [:maybe fn?]] + [:pump {:optional true} :int] + [:pump-bonus {:optional true} [:maybe fn?]] + [:auto-pump-sort {:optional true} [:maybe :int]] + [:auto-pump-ignore {:optional true} [:maybe :boolean]] + [:heap-breaker-pump {:optional true} [:or :int :keyword]] + [:heap-breaker-break {:optional true} [:or :int :keyword]] + [:auto-break-creds-per-sub {:optional true} [:maybe :int]] + ;; subroutine + [:dynamic {:optional true} [:maybe :keyword]] + [:fired {:optional true} [:maybe :boolean]] + [:cost-bonus {:optional true} [:maybe [:or :int fn?]]] + [:base-play-cost {:optional true} [:maybe [:or Cost [:sequential Cost]]]] + [:play-cost-bonus {:optional true} [:maybe fn?]] + [:condition {:optional true} :keyword] + [:display-side {:optional true} [:enum :corp :runner]] + [:change-in-game-state {:optional true} [:ref ::cigs]] + [:automatic {:optional true} :keyword] + [:rfg-instead-of-trashing {:optional true} :boolean] + [:trash-after-resolving {:optional true} :boolean] + [:unregister-once-resolved {:optional true} :boolean] + [:once-per-instance {:optional true} :boolean] + [:offer-bad-pub? {:optional true} [:maybe :int]] + [:keep-menu-open {:optional true} [:or :keyword :boolean]] + [:cost {:optional true} [:maybe [:sequential Cost]]] + [:fake-cost {:optional true} [:maybe [:sequential Cost]]] + [:label {:optional true} [:maybe [:or :string fn?]]] + [:async {:optional true} true?] + [:player {:optional true} [:enum :corp :runner]] + [:prompt {:optional true} [:or :string fn?]] + [:prompt-type {:optional true} :keyword] + [:waiting-prompt {:optional true} [:ref ::waiting-prompt]] + [:card {:optional true} [:maybe Card]] + [:cards {:optional true} [:sequential Card]] + [:choices {:optional true} [:ref ::choices]] + [:not-distinct {:optional true} :boolean] + [:cancel {:optional true} [:maybe [:ref ::ability]]] + [:interactive {:optional true} [:maybe [:or :boolean fn?]]] + [:silent {:optional true} [:maybe [:or :boolean fn?]]] + [:once {:optional true} [:maybe [:enum :per-turn :per-run :per-encounter]]] + [:once-key {:optional true} :keyword] + [:install-req {:optional true} fn?] + [:legal-zones {:optional true} [:sequential :string]] + [:makes-run {:optional true} :boolean] + [:when-inactive {:optional true} :boolean] + [:additional-ability {:optional true} [:maybe [:ref ::ability]]] + [:location {:optional true} :keyword] + [:source {:optional true} [:or :string :uuid]] ;; wtf + [:cid {:optional true} :string] ;; wtf + [:skippable {:optional true} :boolean] + [:autoresolve {:optional true} [:or :boolean fn?]] + [:optional {:optional true} [:ref ::optional]] + [:psi {:optional true} [:ref ::psi]] + [:trace {:optional true} [:ref ::trace]]] + ::psi + [:map {:closed true} + [:req {:optional true} fn?] + [:equal {:optional true} [:ref ::ability]] + [:once {:optional true} [:ref ::once]] + [:not-equal {:optional true} [:ref ::ability]]] + ::trace + [:map {:closed true} + [:req {:optional true} fn?] + [:label {:optional true} [:maybe :string]] + [:msg {:optional true} [:ref ::msg]] + [:base [:or :int fn?]] + [:successful {:optional true} [:ref ::ability]] + [:unsuccessful {:optional true} [:ref ::ability]] + [:kicker {:optional true} [:ref ::ability]] + [:kicker-min {:optional true} :int]] + ::optional + [:map {:closed true} + [:req {:optional true} fn?] + [:prompt [:or :string fn?]] + [:waiting-prompt {:optional true} [:ref ::waiting-prompt]] + [:once {:optional true} [:ref ::once]] + [:change-in-game-state {:optional true} [:ref ::cigs]] + [:interactive {:optional true} [:maybe [:or :boolean fn?]]] + [:player {:optional true} [:enum :corp :runner]] + [:yes-ability {:optional true} [:ref ::ability]] + [:no-ability {:optional true} [:ref ::ability]] + [:end-effect {:optional true} fn?] + [:autoresolve {:optional true} [:or :boolean fn?]]]}} + [:ref ::ability]])) + +;; i18n schemas + +;; standalone +(def $username [:username :string]) +(def $do-ability [:do-ability :string]) +(def $payment [:payment :string]) + +;; :effect +(def $add-count [:effect/add-count :int]) +(def $bonus [:effect/bonus :int]) +(def $card-str [:effect/card-str Card]) +(def $card-str2 [:effect/card-str2 Card]) +(def $card-strs [:effect/card-strs [:sequential Card]]) +(def $cards [:effect/cards :int]) +(def $choice [:effect/choice :string]) +(def $count [:effect/count :int]) +(def $credits [:effect/credits :int]) +(def $discount [:effect/discount :int]) +(def $position [:effect/position :int]) +(def $seen [:effect/seen [:sequential Card]]) +(def $server [:effect/server [:or :string :keyword]]) +(def $server-n [:effect/server-n :int]) +(def $title [:effect/title [:or :string Card]]) +(def $titles [:effect/titles [:sequential [:or :string Card]]]) +(def $top-count [:effect/top-count :int]) +(def $turn [:effect/turn :int]) +(def $turns [:effect/turns :int]) +(def $unseen-cnt [:effect/unseen-cnt :int]) +(def $value [:effect/value :int]) + +(def effect-registry (atom {})) +(defn register-effect + [kw & kvs] + (swap! effect-registry assoc kw (m/schema (into [:map] kvs))) + kw) + +;; generic + +(register-effect :do-nothing) +(register-effect :select $card-str) +(register-effect :play-card $title) +(register-effect :play-card-no-additional-costs $title) + +;; trashing + +(register-effect :trash-self) +(register-effect :trash-card $card-str) +(register-effect :trash-card-at-no-cost $card-str) +(register-effect :trash-n-cards $count) +(register-effect :trash-cards $count $card-strs) +(register-effect :trash-accessed-card $title) +(register-effect :trash-all-cards-in-grip) +(register-effect :trash-all-agendas-by-type) +(register-effect :trash-all-assets-by-type) +(register-effect :trash-all-events-by-type) +(register-effect :trash-all-hardware-by-type) +(register-effect :trash-all-ice-by-type) +(register-effect :trash-all-operations-by-type) +(register-effect :trash-all-resource-by-type) +(register-effect :trash-all-upgrade-by-type) +(register-effect :trash-all-cards-in-server-at-no-cost $server) + +;; credits + +(register-effect :gain-credits $count) +(register-effect :corp-gains-credits $count) +(register-effect :runner-gains-credits $count) + +;; drawing cards + +(register-effect :draw-cards $count) + +;; clicks + +(register-effect :gain-clicks $count) +(register-effect :lose-clicks $count) + +;; tags + +(register-effect :avoid-tags $count) +(register-effect :take-tags $count) +(register-effect :remove-tags $count) +(register-effect :remove-all-tags $count) + +;; runner shuffling + +(register-effect :shuffle-grip-into-stack) +(register-effect :shuffle-grip-and-heap-into-stack) +(register-effect :shuffle-self-into-stack) +(register-effect :shuffle-cards-into-stack $count $titles) +(register-effect :shuffle-stack) + +;; corp shuffling + +(register-effect :shuffle-cards-in-server-into-rd $server) +(register-effect :shuffle-cards-into-rd $count $titles) + +;; score area stuff + +(register-effect :forfeit $title) +(register-effect :add-self-to-score-area $value) +(register-effect :give-bad-publicity $count) + +;; moving cards + +(register-effect :add-self-to-grip) +(register-effect :add-card-to-grip $title) +(register-effect :add-card-to-hq $card-str) +(register-effect :add-card-from-stack-to-grip $card-str) +(register-effect :add-card-to-top-of-stack $card-str) +(register-effect :add-card-to-bottom-of-stack $card-str) +(register-effect :add-card-to-top-of-rd $title) +(register-effect :add-card-to-bottom-of-rd $title) +(register-effect :add-cards-from-heap-to-grip $titles) +(register-effect :force-add-all-hq-cards-to-top-of-rd) +(register-effect :move-seen-unseen-into-grip $seen $unseen-cnt) +(register-effect :move-seen-into-grip $seen) +(register-effect :move-unseen-into-grip $unseen-cnt) +(register-effect :move-seen-unseen-into-hq $seen $unseen-cnt) +(register-effect :move-seen-into-hq $seen) +(register-effect :move-unseen-into-hq $unseen-cnt) + +;; remove from the game (rfg) + +(register-effect :rfg-card $title) + +;; reveal + +(register-effect :expose-card $title) +(register-effect :reveal-n-cards-in-hq $count) +(register-effect :reveal-cards-in-hq $count $titles) +(register-effect :reveal-cards-in-grip $count $titles) +(register-effect :reveal-top-of-stack $title) + +(register-effect :disable-corp-id) +(register-effect :disable-runner-id) + +;; turns + +(register-effect :take-additional-turn) +(register-effect :reduce-corp-max-hand-size-bad-publicity) +(register-effect :reduce-corp-click-next-turn $count) + +;; rearrange stuff + +(register-effect :rearrange-installed-ice) +(register-effect :rearrange-top-n-cards-rd $count) +(register-effect :trash-or-rearrange-top-of-stack $count) +(register-effect :swap-two-ice-positions $card-str $card-str2) + +;; advancement counters + +(register-effect :place-n-advancement-counters $count $card-str) +(register-effect :remove-advancement-counters $count $card-str) +(register-effect :place-virus-counters $count $title) +(register-effect :charge-card $card-str $count) + +(register-effect :place-credits-on-self $credits) +(register-effect :place-credits-on-self-for-trash-costs $credits) + +(register-effect :look-at-top-cards-add-to-grip $top-count $add-count) + +(register-effect :guess $choice) + +(register-effect :reveal-copies-of-self $count) + +;; forcing + +(register-effect :force-take-bad-publicity $count) +(register-effect :force-trash-installed-ice $server) +(register-effect :force-corp-trash-top-of-rd $count) +(register-effect :force-corp-trash-additional-top-of-rd $count) +(register-effect :force-corp-rez $title) +(register-effect :force-corp-trash $title) +(register-effect :force-corp-pay-credits $credits) +(register-effect :force-corp-lose-credits $credits) +(register-effect :force-corp-draw-cards $count) +(register-effect :force-corp-discard-from-hq $count) +(register-effect :force-corp-random-discard-from-hq $count) + +(register-effect :force-runner-draw-cards $count) +(register-effect :force-runner-gain-credits $credits) + +(register-effect :each-player-draws-cards $count) + +;; runner installs + +(register-effect :runner-install-card $title) + +(register-effect :install-with-discount $title $discount) +(register-effect :install-from-grip $title) +(register-effect :install-from-grip-with-discount $title $discount) + +(register-effect :install-from-stack $title) +(register-effect :install-from-stack-with-discount $title $discount) +(register-effect :install-program-from-heap) +(register-effect :install-program-from-stack) + + +;; hosting + +(register-effect :host-self-as-condition-counter $card-str) +(register-effect :host-card-on-card $title $card-str) + +;; rezzing + +(register-effect :rez-card $card-str) +(register-effect :derez-card $card-str) +(register-effect :derez-cards $card-strs) + +;; make a run + +(register-effect :make-a-run) +(register-effect :make-a-run-on $server) +(register-effect :make-a-run-on-preventing-all-damage $server) +(register-effect :run-on-with-no-rezzed-ice $server) +(register-effect :rfg-to-make-a-run-on $title $server) + +;; redirect run + +(register-effect :redirect-run-to-archives) +(register-effect :redirect-run-to-hq) +(register-effect :redirect-run-to-rd) + +;; icebreaker strength + +(register-effect :give-strength-to-icebreaker-during-run $bonus $title) +(register-effect :give-strength-to-icebreaker-remainder-of-run $bonus $card-str) +(register-effect :give-strength-all-icebreakers-during-run $bonus) + +;; ice + +(register-effect :ice-gains-barrier-code-gate-sentry-end-of-turn $card-str) + +(register-effect :bypass-ice $card-str) + +(register-effect :prevent-run-ending) +(register-effect :prevent-ice-rezzed-during-run) +(register-effect :prevent-corp-rez-card-during-turn $card-str) +(register-effect :prevent-corp-rez-non-ice-on-runner-turn) +(register-effect :increase-rez-cost-first-unrezzed-approached-ice $credits) + +;; prevention + +(register-effect :prevent-core-damage $count) +(register-effect :prevent-net-damage $count) +(register-effect :prevent-meat-damage $count) + +(register-effect :prevent-corp-advancing-cards-next-turn) + +;; damage + +(register-effect :suffer-meat-damage $value) +(register-effect :suffer-net-damage $value) +(register-effect :suffer-brain-damage $value) +(register-effect :suffer-core-damage $value) +(register-effect :prevent-damage-until-next-turn) + +;; access + +(register-effect :access-another-card) +(register-effect :access-card $card-str) +(register-effect :access-additional-in-hq $count) +(register-effect :access-additional-in-rd $count) +(register-effect :access-from-bottom-of-rd) + +;; searching + +(register-effect :search-stack-for-connection-resource) +(register-effect :search-stack-for-run-event) +(register-effect :search-stack-for-virtual-resource) + +;; Specific card abilities + +(register-effect :trash-all-installed-corp) +(register-effect :turn-all-installed-runner-facedown) +(register-effect :change-identity $title) + +;; payments + +(register-effect :payment-click $value) +(register-effect :payment-credit $value) +(register-effect :payment-x-credit $value) +(register-effect :payment-credit-pool $value) +(register-effect :payment-hosted-credit $value $title) +(register-effect :payment-bad-publicity $value) +(register-effect :payment-extend $title) +(register-effect :payment-trash-can) +(register-effect :payment-trash-self $title) +(register-effect :payment-forfeit $count $titles) +(register-effect :payment-gain-tags $count) +(register-effect :payment-tag $count) +(register-effect :payment-bad-publicity $count) +(register-effect :payment-return-to-grip $title) +(register-effect :payment-return-to-hq $title) +(register-effect :payment-return-from-game $title) +(register-effect :payment-rfg-program $count $titles) +(register-effect :payment-trash-installed $count $titles) +(register-effect :payment-trash-hardware $count $titles) +(register-effect :payment-trash-program $count $titles) +(register-effect :payment-trash-resource $count $titles) +(register-effect :payment-trash-connection $count $titles) +(register-effect :payment-trash-ice $count $titles) +(register-effect :payment-trash-bioroid $count $titles) +(register-effect :payment-trash-from-stack $count) +(register-effect :payment-trash-from-rd $count) +(register-effect :payment-trash-from-grip $count $titles) +(register-effect :payment-trash-from-hq $count) +(register-effect :payment-reveal-trash-from-grip $count $titles) +(register-effect :payment-reveal-trash-from-hq $count $titles) +(register-effect :payment-random-trash-from-grip $count $titles) +(register-effect :payment-random-trash-from-hq $count) +(register-effect :payment-random-reveal-trash-from-grip $count $titles) +(register-effect :payment-random-reveal-trash-from-hq $count $titles) +(register-effect :payment-trash-all-cards-in-hq $count) +(register-effect :payment-trash-all-cards-in-grip $count $titles) +(register-effect :payment-trash-hardware-in-grip $count $titles) +(register-effect :payment-trash-program-in-grip $count $titles) +(register-effect :payment-trash-resource-in-grip $count $titles) +(register-effect :payment-meat $value) +(register-effect :payment-net $value) +(register-effect :payment-core $value) +(register-effect :payment-shuffle-installed-into-stack $count $titles) +(register-effect :payment-shuffle-installed-into-rd $count) +(register-effect :payment-add-installed-bottom-stack $count $titles) +(register-effect :payment-add-installed-bottom-rd $count $titles) +(register-effect :payment-add-random-from-hand-to-bottom-of-stack $count) +(register-effect :payment-add-random-from-hand-to-bottom-of-rd $count) +(register-effect :payment-hosted-to-hq $count $titles) +(register-effect :payment-any-agenda-counter $count $title) +(register-effect :payment-any-virus-counter $count $title) +(register-effect :payment-derez-harmonic $count $titles) + +;; card strs + +(register-effect :card-str-runner-seen $title) +(register-effect :card-str-runner-unknown) +(register-effect :card-str-runner-hosted-seen $title) +(register-effect :card-str-runner-hosted-unknown) + +(register-effect :card-str-corp-scored $title) +(register-effect :card-str-corp-rfg $title) +(register-effect :card-str-corp-play-area $title) +(register-effect :card-str-corp-destroyed $title) + +(register-effect :card-str-corp-hosted-seen $title) +(register-effect :card-str-corp-hosted-known $title) +(register-effect :card-str-corp-hosted-unknown $server $server-n) +(register-effect :card-str-corp-installed-remote-seen $title $server-n) +(register-effect :card-str-corp-installed-remote-known $title $server-n) +(register-effect :card-str-corp-installed-remote-unknown $server-n) +(register-effect :card-str-corp-installed-central-seen $title $server) +(register-effect :card-str-corp-installed-central-known $title $server) +(register-effect :card-str-corp-installed-central-unknown $server) +(register-effect :card-str-corp-installed-ice-seen $title $server $position $server-n) +(register-effect :card-str-corp-installed-ice-known $title $server $position $server-n) +(register-effect :card-str-corp-installed-ice-unknown $server $position $server-n) + +(def EffectMsg + (m/schema + `[:multi {:dispatch :effect/type} + ~@@effect-registry])) + +(comment + (me/humanize (m/explain EffectMsg {:effect/type :add-card-to-hq + :effect/title {:title "hello"}})) + ,) + +(def Msg + (m/schema + [:map + [:msg/type :keyword]])) + +(def msg-registry (atom {})) +(defn strip-effect-ns + [[k & args]] + (into [(keyword "msg" (name k))] args)) + +(defn register-msg + [kw & kvs] + (let [schema (into [:map] (mapv strip-effect-ns kvs))] + (swap! msg-registry assoc kw (mu/merge Msg schema))) + kw) + +(register-msg :use-card) +(register-msg :pay-use-card) +(register-msg :satisfy-card) + +(register-msg :increase-trace-strength $username $payment $value) +(register-msg :corp-start-of-turn $username $turn $credits $cards) +(register-msg :corp-end-of-turn $username $turn $credits $cards) +(register-msg :runner-start-of-turn $username $turn $credits $cards) +(register-msg :runner-end-of-turn $username $turn $credits $cards) + +(register-msg :mandatory-start-of-turn-draw $username) + +(register-msg :no-further-actions $username) + +(register-msg :skip-discard-step $username) + +(register-msg :corp-discard-cards-from-hand-eot $username $cards) +(register-msg :runner-discard-cards-from-hand-eot $username $cards) +(register-msg :extra-turns-remaining $username $turns) + +(register-msg :tie) +(register-msg :win $username) +(register-msg :concede $username) +(register-msg :win-decked $username) +(register-msg :win-flatline $username) +(register-msg :clear-win $username) + +(register-msg :mulligan-take $username) +(register-msg :mulligan-keep $username) + +(register-msg :msg-draw-cards $username $count) +(register-msg :msg-forfeit-agenda $username $title) +(register-msg :msg-trash-card $username $card-str) +(register-msg :msg-trash-cards $username $count $titles) + +(register-msg :msg-derez-card $username $card-str) +(register-msg :msg-derez-cards $username $card-strs) +(register-msg :msg-rfg-n-cards-from-stack $username $count $card-strs) + +(register-msg :waiting-corp-default) +(register-msg :waiting-runner-default) +(register-msg :waiting-trash-prevention-triggers) +(register-msg :waiting-pre-damage-triggers) +(register-msg :waiting-damage-triggers) +(register-msg :waiting-prevent-when-encountered) +(register-msg :waiting-prevent-run-ending) +(register-msg :waiting-prevent-jacking-out) +(register-msg :waiting-prevent-expose) +(register-msg :waiting-prevent-bad-publicity) +(register-msg :waiting-prevent-tags) + +(def MsgMap + (m/schema + `[:multi {:dispatch :msg/type} + ~@@msg-registry])) diff --git a/src/clj/game/core/servers.clj b/src/clj/game/core/servers.cljc similarity index 91% rename from src/clj/game/core/servers.clj rename to src/clj/game/core/servers.cljc index d67e744f85..2270b627f4 100644 --- a/src/clj/game/core/servers.clj +++ b/src/clj/game/core/servers.cljc @@ -14,6 +14,15 @@ [num] (str "Server " num)) +(defn central->name + "Converts a central zone keyword to a string." + [zone] + (case (if (keyword? zone) zone (last zone)) + (:hand :hq) "HQ" + (:deck :rd) "R&D" + (:discard :archives) "Archives" + #_:else nil)) + (defn remote->name "Converts a remote zone to a string" [zone] @@ -23,20 +32,22 @@ (let [num (last (string/split s #":remote"))] (remote-num->name num))))) -(defn central->name - "Converts a central zone keyword to a string." +(defn auxiliary->name + "converts one of the 'auxilary' zones keyword to a string." [zone] - (case (if (keyword? zone) zone (last zone)) - (:hand :hq) "HQ" - (:deck :rd) "R&D" - (:discard :archives) "Archives" - nil)) + (case (if (keyword? zone) zone (first zone)) + :scored "score area" + :rfg "removed from game" + :play-area "play area" + :destroyed "destroyed" + #_:else nil)) (defn zone->name "Converts a zone to a string." [zone] (or (central->name zone) - (remote->name zone))) + (remote->name zone) + (auxiliary->name zone))) (defn name-zone "Gets a string representation for the given zone." @@ -86,6 +97,10 @@ (and (is-central? (second zone)) (= :content (last zone)))) +(defn is-auxiliary? + [zone] + (some? (auxiliary->name zone))) + (defn central->zone "Converts a central server keyword like :discard into a corresponding zone vector" [zone] diff --git a/src/clj/game/core/set_up.clj b/src/clj/game/core/set_up.clj index cbedec54a9..d9c2b1a003 100644 --- a/src/clj/game/core/set_up.clj +++ b/src/clj/game/core/set_up.clj @@ -43,7 +43,7 @@ (when-let [mul (:mulligan cdef)] (mul state side (make-eid state) card nil)))) (swap! state assoc-in [side :keep] :mulligan) - (system-msg state side "takes a mulligan") + (system-msg state side {:msg/type :mulligan-take}) (trigger-event state side :pre-first-turn) (when (and (= side :corp) (-> @state :runner :identity :title)) (clear-wait-prompt state :runner) @@ -55,7 +55,7 @@ "Choose not to mulligan." [state side _] (swap! state assoc-in [side :keep] :keep) - (system-msg state side "keeps [their] hand") + (system-msg state side {:msg/type :mulligan-keep}) (trigger-event state side :pre-first-turn) (when (and (= side :corp) (-> @state :runner :identity :title)) (clear-wait-prompt state :runner) diff --git a/src/clj/game/core/to_string.clj b/src/clj/game/core/to_string.clj deleted file mode 100644 index de151c08d9..0000000000 --- a/src/clj/game/core/to_string.clj +++ /dev/null @@ -1,31 +0,0 @@ -(ns game.core.to-string - (:require [game.core.card :refer [get-card rezzed? ice? installed? corp? card-index get-title]] - [game.core.servers :refer [is-root? zone->name]])) - -(defn card-str - "Gets a string description of an installed card, reflecting whether it is rezzed, - in/protecting a server, facedown, or hosted." - ([state card] (card-str state card nil)) - ([state {:keys [zone host facedown] :as card} {:keys [visible maybe-visible]}] - (str (if (corp? card) - (let [installed-ice (and (ice? card) (installed? card))] - ;; Corp card messages - (str (cond - (or (rezzed? card) (:seen card) visible) (get-title card) - maybe-visible (str "facedown " (get-title card)) - :else (if installed-ice "ice" "a card")) - ;; Hosted cards do not need "in server 1" messages, host has them - (when-not host - (str (cond - installed-ice " protecting " - (is-root? zone) " in the root of " - :else " in ") - ;; TODO add naming of scoring area of corp/runner - (zone->name (or (second zone) zone)) ;; handles [:hand] as well as [:servers :hq] - (when installed-ice - (str " at position " (card-index state card))))))) - ;; Runner card messages - (if (or facedown visible) - "a facedown card" - (get-title card))) - (when host (str " hosted on " (card-str state (get-card state host))))))) diff --git a/src/clj/game/core/to_string.cljc b/src/clj/game/core/to_string.cljc new file mode 100644 index 0000000000..ec8e26b616 --- /dev/null +++ b/src/clj/game/core/to_string.cljc @@ -0,0 +1,159 @@ +(ns game.core.to-string + (:require + [clojure.string :as str] + [game.core.card :refer [#?(:clj card-index) + #?(:clj get-card) + condition-counter? corp? get-title ice? in-current? + in-discard? in-hand? in-play-area? in-scored? + installed? rezzed?]] + [game.core.servers :refer [#?(:clj auxiliary->name) + is-auxiliary? is-root? zone->name]])) + +#?(:clj (defn card-str + "Gets a string description of an installed card, reflecting whether it is rezzed, + in/protecting a server, facedown, or hosted." + ([state card] (card-str state card nil)) + ([state {:keys [zone host facedown] :as card} {:keys [visible maybe-visible]}] + (str (if (corp? card) + (let [installed-ice (and (ice? card) (installed? card))] + ;; Corp card messages + (str (cond + (or (rezzed? card) (:seen card) visible) (get-title card) + maybe-visible (str "facedown " (get-title card)) + :else (if installed-ice "ice" "a card")) + ;; Hosted cards do not need "in server 1" messages, host has them + (when-not host + (str (cond + installed-ice " protecting " + (is-root? zone) " in the root of " + :else " in ") + (zone->name (or (second zone) zone)) ;; handles [:hand] as well as [:servers :hq] + (when installed-ice + (str " at position " (card-index state card))))))) + ;; Runner card messages + (str (if (or facedown visible) + "a facedown card" + (get-title card)) + (when (= :scored (first zone)) + (str " the Runner's " (auxiliary->name zone))))) + (when host (str " hosted on " (card-str state (get-card state host)))))))) + +(defn remote-num + "Converts a remote zone to a string" + [zone] + (let [kw (if (keyword? zone) zone (last zone)) + s (str kw)] + (when (str/starts-with? s ":remote") + (subs s 7)))) + +(defn card-str-edn + "Gets a string description of an installed card, reflecting whether it is rezzed, + in/protecting a server, facedown, or hosted." + [{:keys [zone facedown host visible maybe-visible] :as card}] + (if (corp? card) + ;; Corp card messages + (let [location (cond + host :host + (and (ice? card) (installed? card)) :ice + (is-root? zone) :central + (is-auxiliary? zone) :auxiliary + :else :remote) + visibility (cond + (= location :auxiliary) :known + (or (rezzed? card) (:seen card) visible) :seen + maybe-visible :known + :else :unknown) + title (get-title card) + zone' (if host + (let [h (first (remove #(= [:onhost] (or (second (:zone %)) (:zone %))) (iterate :host card)))] + (or (second (:zone h)) (:zone h))) + (or (second zone) zone)) + server-name (str/lower-case (zone->name zone')) + server-name (if (= "r&d" server-name) "rd" server-name) + server-n (remote-num zone') + position (:index card)] + (case #{location visibility} + #{:auxiliary :known} + (case (first zone') + :scored {:card/str :card-str-corp-scored + :title title} + :rfg {:card/str :card-str-corp-rfg + :title title} + :play-area {:card/str :card-str-corp-play-area + :title title} + :destroyed {:card/str :card-str-corp-destroyed + :title title}) + #{:host :seen} {:card/str :card-str-corp-hosted-seen + :title title + :server server-name + :server-n server-n} + #{:host :known} {:card/str :card-str-corp-hosted-known + :title title + :server server-name + :server-n server-n} + #{:host :unknown} {:card/str :card-str-corp-hosted-unknown + :server server-name + :server-n server-n} + ;; "NGO Front in Server 5" + #{:remote :seen} {:card/str :card-str-corp-installed-remote-seen + :title title + :server-n server-n} + ;; "facedown Obokata Protocol in Server 2" + #{:remote :known} {:card/str :card-str-corp-installed-remote-known + :title title + :server-n server-n} + ;; "a card in Server 1" + #{:remote :unknown} {:card/str :card-str-corp-installed-remote-unknown + :server-n server-n} + ;; "Prisec in root of HQ" + #{:central :seen} {:card/str :card-str-corp-installed-central-seen + :title title + :server server-name} + ;; "facedown Ryon Knight in root of R&D" + #{:central :known} {:card/str :card-str-corp-installed-central-known + :title title + :server server-name} + ;; "a card in root of Archives" + #{:central :unknown} {:card/str :card-str-corp-installed-central-unknown + :server server-name} + ;; "Ice Wall protecting HQ at position 1" + #{:ice :seen} {:card/str :card-str-corp-installed-ice-seen + :title title + :server server-name + :server-n server-n + :position position} + #{:ice :known} {:card/str :card-str-corp-installed-ice-known + :title title + :server server-name + :server-n server-n + :position position} + #{:ice :unknown} {:card/str :card-str-corp-installed-ice-unknown + :server server-name + :server-n server-n + :position position})) + ;; Runner card messages + (let [title (get-title card) + installed?' (installed? card) + discard?' (in-discard? card) + visible (or (not facedown) + installed?' + discard?' + (in-current? card) + (in-play-area? card) + (condition-counter? card))] + (cond + (in-scored? card) {:card/str :card-str-runner-scored + :title title} + (in-hand? card) {:card/str :card-str-runner-grip + :title title} + discard?' {:card/str :card-str-runner-discard + :title title} + (and host visible) {:card/str :card-str-runner-hosted-seen + :title title} + host {:card/str :card-str-runner-hosted-unknown} + visible (if installed?' + {:card/str :card-str-runner-installed-seen + :title title} + {:card/str :card-str-runner-seen + :title title}) + :else {:card/str :card-str-runner-unknown})))) diff --git a/src/clj/game/core/turns.clj b/src/clj/game/core/turns.clj index 46f86b164a..413f04c91e 100644 --- a/src/clj/game/core/turns.clj +++ b/src/clj/game/core/turns.clj @@ -1,36 +1,42 @@ (ns game.core.turns (:require - [game.core.agendas :refer [update-all-advancement-requirements]] - [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 update-lingering-effect-durations any-effects]] - [game.core.eid :refer [effect-completed make-eid]] - [game.core.engine :refer [checkpoint queue-event trigger-event trigger-event-simult unregister-floating-events update-floating-event-durations resolve-durations]] - [game.core.flags :refer [card-flag-fn? clear-turn-register!]] - [game.core.gaining :refer [gain lose]] - [game.core.hand-size :refer [hand-size]] - [game.core.ice :refer [update-all-ice update-breaker-strength]] - [game.core.moving :refer [move]] - [game.core.say :refer [system-msg]] - [game.core.set-aside :refer [clean-set-aside!]] - [game.core.toasts :refer [toast]] - [game.core.update :refer [update!]] - [game.core.winning :refer [flatline]] - [game.macros :refer [continue-ability effect wait-for]] - [game.utils :refer [dissoc-in enumerate-str quantify]] - [jinteki.utils :refer [other-side]] - [clojure.string :as string])) + [game.core.agendas :refer [update-all-advancement-requirements]] + [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 [any-effects update-lingering-effect-durations]] + [game.core.eid :refer [effect-completed make-eid]] + [game.core.engine :refer [checkpoint queue-event resolve-durations + trigger-event trigger-event-simult + 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]] + [game.core.ice :refer [update-all-ice update-breaker-strength]] + [game.core.moving :refer [move]] + [game.core.say :refer [system-msg]] + [game.core.set-aside :refer [clean-set-aside!]] + [game.core.toasts :refer [toast]] + [game.core.update :refer [update!]] + [game.core.winning :refer [flatline]] + [game.macros :refer [continue-ability effect wait-for]] + [game.utils :refer [dissoc-in enumerate-str quantify]] + [jinteki.utils :refer [other-side]])) (defn- turn-message "Prints a message for the start or end of a turn, summarizing credits and cards in hand." [state side start-of-turn] - (let [pre (if start-of-turn "started" "is ending") - hand (if (= side :runner) "[their] Grip" "HQ") - cards (count (get-in @state [side :hand])) - credits (get-in @state [side :credit]) - text (str pre " [their] turn " (:turn @state) " with " credits " [Credit] and " (quantify cards "card") " in " hand)] - (system-msg state side text {:hr (not start-of-turn)}))) + (let [msg-type (case [(= :corp side) (boolean start-of-turn)] + [true true] :corp-start-of-turn + [true false] :corp-end-of-turn + [false true] :runner-start-of-turn + [false false] :runner-end-of-turn) + m {:msg/type msg-type + :msg/turn (:turn @state) + :msg/cards (count (get-in @state [side :hand])) + :msg/credits (get-in @state [side :credit])}] + (system-msg state side m {:hr (not start-of-turn)}))) (defn end-phase-12 "End phase 1.2 and trigger appropriate events for the player." @@ -46,7 +52,7 @@ (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") + (do (system-msg state side {:msg/type :mandatory-start-of-turn-draw}) (wait-for (draw state side 1 nil) (trigger-event-simult state side eid :corp-mandatory-draw nil nil))) (effect-completed state nil eid)) @@ -65,14 +71,14 @@ (if (and (get-in @state [:corp-phase-12 :corp]) (get-in @state [:corp-phase-12 :runner])) (end-phase-12 state :corp eid _) - (do (system-msg state side "has no further action") + (do (system-msg state side {:msg/type :no-further-actions}) (effect-completed state side eid)))) (:runner-phase-12 @state) (do (swap! state assoc-in [:runner-phase-12 side] true) (if (and (get-in @state [:runner-phase-12 :corp]) (get-in @state [:runner-phase-12 :runner])) (end-phase-12 state :runner eid _) - (do (system-msg state side "has no further action") + (do (system-msg state side {:msg/type :no-further-actions}) (effect-completed state side eid)))) :else nil))) @@ -144,7 +150,7 @@ (effect-completed state side eid)) (any-effects state side :skip-discard) (do - (system-msg state side (str "skips [their] discard step this turn")) + (system-msg state side {:msg/type :skip-discard-step}) (effect-completed state side eid)) (> cur-hand-size max-hand-size) (continue-ability @@ -155,17 +161,17 @@ :all true} :waiting-prompt true :async true - :effect (effect (system-msg state side - (str "discards " - (if (= :runner side) - (enumerate-str (map :title targets)) - (quantify (count targets) "card")) - " from " (if (= :runner side) "[their] Grip" "HQ") - " at end of turn")) - (let [discard (seq (map #(move state side % :discard) targets)) - ev (if (= side :runner) :runner-discard-to-hand-size :corp-discard-to-hand-size)] - (queue-event state ev {:cards discard}) - (checkpoint state nil eid {:durations [ev]})))} + :effect (effect + (let [m (if (= :corp side) + {:msg/type :corp-discard-cards-from-hand-eot + :msg/cards (count targets)} + {:msg/type :runner-discard-cards-from-hand-eot + :msg/cards (count targets)})] + (system-msg state side m)) + (let [discard (mapv #(move state side % :discard) targets) + ev (if (= :corp side) :corp-discard-to-hand-size :runner-discard-to-hand-size)] + (queue-event state ev {:cards discard}) + (checkpoint state nil eid {:durations [ev]})))} nil nil) :else (effect-completed state side eid)))) @@ -212,7 +218,8 @@ (when (pos? extra-turns) (start-turn state side nil) (swap! state update-in [side :extra-turns] dec) - (system-msg state side (string/join ["will have " (quantify extra-turns "extra turn") " remaining."])))) + (system-msg state side {:msg/type :extra-turns-remaining + :msg/turns extra-turns}))) (effect-completed state side eid))))) (defn post-discard-pass-priority @@ -224,14 +231,14 @@ (if (and (get-in @state [:corp-post-discard :corp]) (get-in @state [:corp-post-discard :runner])) (end-turn-continue state :corp eid _) - (do (system-msg state side "has no further action") + (do (system-msg state side {:msg/type :no-further-actions}) (effect-completed state side eid)))) (:runner-post-discard @state) (do (swap! state assoc-in [:runner-post-discard side] true) (if (and (get-in @state [:runner-post-discard :corp]) (get-in @state [:runner-post-discard :runner])) (end-turn-continue state :runner eid _) - (do (system-msg state side "has no further action") + (do (system-msg state side {:msg/type :no-further-actions}) (effect-completed state side eid)))) :else nil))) diff --git a/src/clj/game/core/winning.clj b/src/clj/game/core/winning.clj index af14235354..35a24de032 100644 --- a/src/clj/game/core/winning.clj +++ b/src/clj/game/core/winning.clj @@ -15,7 +15,7 @@ (let [started (get-in @state [:stats :time :started]) now (inst/now) duration (duration/to-minutes (duration/between started now))] - (system-msg state side "wins the game") + (system-msg state side {:msg/type :win}) (play-sfx state side "game-end") (swap! state (fn [state] (-> state @@ -40,7 +40,7 @@ (let [started (get-in @state [:stats :time :started]) now (inst/now) duration (duration/to-minutes (duration/between started now))] - (system-say state nil "The game is a tie!") + (system-say state nil {:msg/type :tie}) (play-sfx state nil "game-end") (swap! state (fn [state] (-> state @@ -53,7 +53,7 @@ (defn win-decked "Records a win via decking the corp." [state] - (system-msg state :corp "is decked") + (system-msg state :corp {:msg/type :win-decked}) (win state :runner "Decked")) (defn flatline @@ -61,14 +61,14 @@ [state] (when-not (:winner state) (swap! state assoc :winner-declared true) - (system-msg state :runner "is flatlined") + (system-msg state :runner {:msg/type :win-flatline}) (win state :corp "Flatline"))) (defn concede "Trigger game concede by specified side. Takes a third argument for use with user commands." ([state side _] (concede state side)) ([state side] - (system-msg state side "concedes") + (system-msg state side {:msg/type :concede}) (win state (if (= side :corp) :runner :corp) "Concede"))) (defn clear-win @@ -76,7 +76,7 @@ [state side] (swap! state assoc-in [side :clear-win] true) (when (and (-> @state :runner :clear-win) (-> @state :corp :clear-win)) - (system-msg state side "cleared the win condition") + (system-msg state side {:msg/type :clear-win}) (swap! state dissoc-in [:runner :clear-win]) (swap! state dissoc-in [:corp :clear-win]) (swap! state dissoc :winner :loser :winning-user :losing-user :reason :winning-deck-id :losing-deck-id :end-time :winner-declared))) diff --git a/src/clj/game/utils.clj b/src/clj/game/utils.cljc similarity index 90% rename from src/clj/game/utils.clj rename to src/clj/game/utils.cljc index 1ab81e321a..210721a06b 100644 --- a/src/clj/game/utils.clj +++ b/src/clj/game/utils.cljc @@ -3,11 +3,10 @@ [game.core.card :refer [get-title]] [jinteki.cards :refer [all-cards]] [clojure.string :as str] - [clj-uuid :as uuid] [cljc.java-time.instant :as inst])) (defn make-cid [] - (uuid/to-string (uuid/v4))) + (str (random-uuid))) (defn make-timestamp [] (inst/now)) @@ -20,7 +19,7 @@ (and title card) card (or (= title "Corp Basic Action Card") (= title "Runner Basic Action Card")) {} :else (when strict? - (throw (Exception. (str "Tried to select server-card for " title)))))))) + (throw (ex-info (str "Tried to select server-card for " title) {}))))))) (defn server-cards [] @@ -51,10 +50,12 @@ (step coll #{}))) (defn string->num [s] - (try - (let [num (bigdec s)] - (if (and (> num Integer/MIN_VALUE) (< num Integer/MAX_VALUE)) (int num) num)) - (catch Exception _ nil))) + #?(:clj (try + (let [num (bigdec s)] + (if (and (> num Integer/MIN_VALUE) (< num Integer/MAX_VALUE)) (int num) num)) + (catch Exception _ nil)) + :cljs (cond (number? s) s + (string? s) (parse-long s)))) (def safe-split (fnil str/split "")) @@ -126,7 +127,7 @@ ([strings sep] (if (<= (count strings) 2) (str/join (str " " sep " ") strings) - (str (str/join ", " (butlast strings)) ", " sep " " (last strings))))) + (str (apply str (interpose ", " (butlast strings))) ", " sep " " (last strings))))) (defn enumerate-cards "Enumerates a collection of cards, optionally in sorted order" diff --git a/src/clj/tasks/translations.clj b/src/clj/tasks/translations.clj index f846b2417b..28e64eb145 100644 --- a/src/clj/tasks/translations.clj +++ b/src/clj/tasks/translations.clj @@ -9,26 +9,10 @@ (fluent.bundle FluentBundle FluentBundle$Builder FluentResource) (fluent.functions.cldr CLDRFunctionFactory) (fluent.syntax.AST - Attribute - CallArguments - Commentary$Comment - Commentary$GroupComment - Commentary$ResourceComment Identifiable - Identifier - InlineExpression$FunctionReference - InlineExpression$MessageReference - InlineExpression$TermReference - InlineExpression$VariableReference - Literal$StringLiteral Message - NamedArgument Pattern - PatternElement$Placeable - PatternElement$TextElement - SelectExpression - Term - Variant) + PatternElement$TextElement) (fluent.syntax.parser FTLParser FTLStream) (java.io File) (java.util Locale Optional))) @@ -80,7 +64,8 @@ [n content])))))] (reset! fluent-dictionary {}) (doseq [[lang content] langs] - (swap! fluent-dictionary assoc lang (build lang content))))) + (swap! fluent-dictionary assoc lang (build lang content)))) + (println "Loaded")) (defn get-messages [lang] (into {} (get @fluent-dictionary lang))) @@ -91,9 +76,10 @@ language has defined entries not in :en." [& args] (load-dictionary!) - (let [en-keys (keys (get-messages "en"))] - (doseq [lang (or (seq (map name args)) - (keys (dissoc @fluent-dictionary "en"))) + (let [en-keys (keys (get-messages "en")) + langs (or (seq (map name args)) + (keys (dissoc @fluent-dictionary "en")))] + (doseq [lang (sort langs) :let [lang-keys (keys (get-messages lang))]] (println "Checking" lang) (when-let [diff (seq (set/difference @@ -206,232 +192,3 @@ (comment (unused-translations)) - -;; formatting - -(defn- indent [ctx] - (print (str/join (repeat (:indent ctx) " ")))) - -(defprotocol FTLPrinter - (-ftl-print [this ctx])) - -(extend-protocol FTLPrinter - Commentary$Comment - (-ftl-print - [this _ctx] - (print "# ") - (print (Commentary$Comment/.text this))) - - Commentary$GroupComment - (-ftl-print - [this _ctx] - (newline) - (print "## ") - (print (Commentary$GroupComment/.text this))) - - Commentary$ResourceComment - (-ftl-print - [this _ctx] - (newline) - (print "### ") - (print (Commentary$ResourceComment/.text this))) - - Literal$StringLiteral - (-ftl-print - [this _ctx] - (pr (Literal$StringLiteral/.value this))) - - Identifier - (-ftl-print - [this _ctx] - (print (Identifier/.key this))) - - InlineExpression$MessageReference - (-ftl-print - [this ctx] - (-ftl-print (InlineExpression$MessageReference/.identifier this) ctx)) - - InlineExpression$TermReference - (-ftl-print - [this ctx] - (print "{-") - (-ftl-print (InlineExpression$TermReference/.identifier this) ctx) - (when-let [attr (Optional/.orElse (InlineExpression$TermReference/.attributeID this) nil)] - (-ftl-print attr ctx)) - (when-let [args (Optional/.orElse (InlineExpression$TermReference/.arguments this) nil)] - (let [positional (not-empty (CallArguments/.positional args)) - named (not-empty (CallArguments/.named args))] - (print "(") - (let [ctx (assoc ctx :arg true)] - (when positional - (run! (fn [arg] (if (string? arg) - (print arg) - (-ftl-print arg ctx))) - (interpose ", " positional))) - (when (and positional named) - (print ", ")) - (when named - (run! (fn [arg] (if (string? arg) - (print arg) - (-ftl-print arg ctx))) - (interpose ", " named)))) - (print ")"))) - (print "}")) - - InlineExpression$FunctionReference - (-ftl-print - [this ctx] - (-ftl-print (InlineExpression$FunctionReference/.identifier this) ctx) - (when-let [args (Optional/.orElse (InlineExpression$FunctionReference/.arguments this) nil)] - (let [positional (not-empty (CallArguments/.positional args)) - named (not-empty (CallArguments/.named args))] - (print "(") - (let [ctx (assoc ctx :arg true)] - (when positional - (run! (fn [arg] (if (string? arg) - (print arg) - (-ftl-print arg ctx))) - (interpose ", " positional))) - (when (and positional named) - (print ", ")) - (when named - (run! (fn [arg] (if (string? arg) - (print arg) - (-ftl-print arg ctx))) - (interpose ", " named)))) - (print ")")))) - - NamedArgument - (-ftl-print - [this ctx] - (-ftl-print (NamedArgument/.name this) ctx) - (print ": ") - (-ftl-print (NamedArgument/.value this) ctx)) - - InlineExpression$VariableReference - (-ftl-print - [this ctx] - (print "$") - (-ftl-print (InlineExpression$VariableReference/.identifier this) ctx)) - - Variant - (-ftl-print - [this ctx] - (when (Variant/.isDefault this) - (print "*")) - (print "[") - (-ftl-print (Variant/.keyable this) ctx) - (print "]") - (print " ") - (-ftl-print (Variant/.value this) ctx)) - - SelectExpression - (-ftl-print - [this ctx] - (-ftl-print (SelectExpression/.selector this) ctx) - (print " ->") - (newline) - (run! (fn [variant] - (indent ctx) - (-ftl-print variant ctx) - (newline)) - (SelectExpression/.variants this)) - (indent (update ctx :indent #(max 0 (dec %))))) - - PatternElement$TextElement - (-ftl-print - [this ctx] - (let [s (PatternElement$TextElement/.value this)] - (print s) - (when (str/includes? s "\n") - (indent ctx)))) - - PatternElement$Placeable - (-ftl-print - [this ctx] - (print "{") - (-ftl-print (PatternElement$Placeable/.expression this) ctx) - (print "}")) - - Pattern - (-ftl-print - [this ctx] - (let [ctx (update ctx :indent inc)] - (run! (fn [pat] (-ftl-print pat ctx)) (Pattern/.elements this)))) - - Attribute - (-ftl-print - [this ctx] - (let [ctx (update ctx :indent inc)] - (indent ctx) - (print ".") - (-ftl-print (Attribute/.identifier this) ctx) - (print " = ") - (-ftl-print (Attribute/.pattern this) ctx) - (newline))) - - Term - (-ftl-print - [this ctx] - (Optional/.map (Term/.comment this) (fn [cmnt] - (-ftl-print cmnt ctx) - (newline))) - (print "-") - (-ftl-print (Term/.identifier this) ctx) - (print " = ") - (-ftl-print (Term/.value this) ctx) - (when-let [attrs (not-empty (Term/.attributes this))] - (newline) - (run! (fn [attr] (-ftl-print attr ctx)) attrs)) - (newline)) - - Message - (-ftl-print - [this ctx] - (Optional/.map (Message/.comment this) (fn [cmnt] - (-ftl-print cmnt ctx) - (newline))) - (-ftl-print (Message/.identifier this) ctx) - (print " = ") - (-ftl-print (Optional/.orElseThrow (Message/.pattern this)) ctx) - (newline)) - - FluentResource - (-ftl-print - [this ctx] - (let [entries (FluentResource/.entries this)] - (doseq [entry entries] - (-ftl-print entry ctx) - (newline))))) - -(comment - (let [res (FTLParser/parse (FTLStream/of "a = {NUMBER($percent, style:\"percent\")}") false)] - (prn res) - (-ftl-print res {:indent 0}))) - -(defn ftl-print - ([resource] - (ftl-print resource {:indent 0})) - ([resource config] - (-ftl-print resource config))) - -(defn format-i18n-files - [& args] - (load-dictionary!) - (let [langs (or (seq (map name args)) - (keys @fluent-dictionary))] - (doseq [lang langs - :let [f (io/file "resources" "public" "i18n" (str lang ".ftl")) - contents (-> (slurp f) - (str/replace "\\u" "__u") - (str/replace "\\U" "__U")) - ast (FTLParser/parse (FTLStream/of contents) false) - formatted-file (-> (with-out-str (ftl-print ast)) - (str/replace "__u" "\\u") - (str/replace "__U" "\\U") - (str/trim) - (str "\n"))]] - (spit f formatted-file)))) - -(comment - (format-i18n-files)) diff --git a/src/cljc/game/core/card.cljc b/src/cljc/game/core/card.cljc index e0d32ffd58..4b7434af07 100644 --- a/src/cljc/game/core/card.cljc +++ b/src/cljc/game/core/card.cljc @@ -65,6 +65,10 @@ uniqueness zone]) +(defn card? + [obj] + (contains? obj :cid)) + (defn get-cid "Gets the cid of a given card when wrapped in an effect-handler map" [card] @@ -319,6 +323,10 @@ [card] (:uniqueness card)) +(defn same-faction? + [card other-card] + (= (:faction card) (:faction other-card))) + (defn corp-installable-type? "Is the card of an acceptable type to be installed in a server" [card] diff --git a/src/cljc/jinteki/i18n.cljc b/src/cljc/jinteki/i18n.cljc index c412d7d0fd..782322ec99 100644 --- a/src/cljc/jinteki/i18n.cljc +++ b/src/cljc/jinteki/i18n.cljc @@ -1,52 +1,73 @@ (ns jinteki.i18n (:refer-clojure :exclude [format]) (:require - [noahtheduke.fluent :as fluent] - #?@(:clj [[clojure.java.io :as io] - [clojure.string :as str]]) - #?(:cljs - [reagent.core :as r]))) + #?(:clj [clojure.java.io :as io]) + #?(:cljs [reagent.core :as r]) + #?(:cljs [nr.appstate :refer [app-state]]) + #?(:cljs [cljs.core.async :refer [take!] :refer-macros [go]]) + #?(:cljs [nr.ajax :refer [GET]]) + [clojure.string :as str] + [game.core.card :refer [get-title]] + [game.core.schemas :as schemas :refer [EffectMsg MsgMap]] + [game.core.to-string :refer [card-str-edn]] + [noahtheduke.fluent :as fluent])) + +#?(:clj (set! *warn-on-reflection* true)) + +(def language-cursor + #?(:clj (delay "en") + :cljs (r/cursor app-state [:options :language]))) (defonce fluent-dictionary #?(:clj (atom nil) :cljs (r/atom {}))) (defn insert-lang! [lang content] - (swap! fluent-dictionary assoc lang {:content content + (swap! fluent-dictionary assoc lang {#?@(:clj [:content content]) :ftl (let [lang (if (= "la-pig" lang) "en" lang)] (fluent/build lang content))})) #?(:clj (defn load-dictionary! [dir] - ;; List of supported language files (based on resources/public/i18n directory) - (let [languages ["en" "es" "ca" "fr" "it" "ja" "ko" "la-pig" "pl" "pt" "ru" "zh-simp" "zh-trad"] - errors (volatile! []) - ;; Try loading each language directly as a resource (works in both jar and filesystem) - langs (->> languages - (keep (fn [lang] - (try (let [res-path (str dir "/" lang ".ftl") - ;; Try to load from classpath (including jar) - res (io/resource res-path)] - ;; Skip empty placeholder files - (when-let [content (-> res slurp not-empty)] - [lang content])) - (catch Throwable _ - (println "Error reading lang " lang "in resource" (str dir "/" lang ".ftl")) - (vswap! errors conj lang) - nil)))))] - (doseq [[lang content] langs] - (try (insert-lang! lang content) - (catch Throwable t - (println "Error inserting i18n data for" lang) - (println (ex-message t)) - (vswap! errors conj lang)))) - @errors))) + (when-let [dir (io/file (io/resource dir))] + ;; List of supported language files (based on what we support in the frontend) + (let [langs (->> ["ca" "en" "es" "fr" "it" "ja" "ko" "la-pig" "pl" "pt" "ru" "zh-simp" "zh-trad"] + (keep (fn [lang] + (let [lang-dir (io/file dir lang)] + (when (.exists lang-dir) + (let [content (->> (file-seq lang-dir) + (filter #(str/ends-with? (str %) ".ftl")) + (map slurp) + (str/join "\n"))] + ;; Skip empty placeholder files + (when-not (str/blank? content) + [lang content]))))))) + errors (volatile! [])] + (doseq [[lang content] langs] + (try (insert-lang! lang content) + (catch Throwable t + (println "Error inserting i18n data for" lang) + (println (ex-message t)) + (vswap! errors conj lang)))) + @errors)))) #?(:clj (comment (load-dictionary! "public/i18n"))) +#?(:cljs + (go (let [lang (get-in @app-state [:options :language] "en")] + (take! (GET (str "/data/language/" lang)) + (fn [response] + (when (= 200 (:status response)) + (insert-lang! lang (:json response))))) + (when-not (= "en" lang) + (take! (GET "/data/language/en") + (fn [response] + (when (= 200 (:status response)) + (insert-lang! "en" (:json response))))))))) + (defn get-content [lang] (get-in @fluent-dictionary [lang :content])) @@ -58,9 +79,12 @@ (defn get-translation [bundle id params] (when bundle - (when-let [ret (fluent/format bundle id params)] - (when-not (identical? "undefined" ret) - ret)))) + #?(:clj (fluent/format bundle id params) + :cljs (try (fluent/format bundle id params) + (catch js/ReferenceError ex + (js/console.log "get-translation id: " id ", params: " params) + (js/console.log ex) + nil))))) (defn format ([lang-cursor resource] (format lang-cursor resource nil)) @@ -75,3 +99,293 @@ target-translation {:translation target-translation :target-language true} fallback {:translation fallback :target-language nil} :else {:translation (get-translation (get-bundle "en") id params) :target-language nil})))) + +;; UI functions + +(defn tr-with-info + ([resource] (tr-with-info resource nil)) + ([resource params] + (format language-cursor resource params))) + +(defn tr + ([resource] (tr resource nil)) + ([resource params] + (:translation (tr-with-info resource params)))) + +(defn- i18n-keys + "put params into tr-element as data if needed" + [params] + (into {} (map (fn [[k v]] [(keyword (str "data-i18n-param-" (name k))) (str v)])) params)) + +(defn- embed-content + [translation content] + ;; note - it's very awkward to do translations where there is an embedded element on the inside + ;; this will allow us to do that by passing in a content map, like: + ;; {:link [:a {:href ....}]} + (if (or (not content) (empty? content)) + translation + (loop [elements [translation] + [pattern & patterns] (keys content)] + (let [insert (get content pattern) + reg (re-pattern (str "\\[" (name pattern) "]")) + new-elements (mapcat #(if-not (string? %) + [%] (interpose insert (str/split % reg))) + elements)] + (if-not patterns + (into [:span] new-elements) + (recur new-elements patterns)))))) + +(defn tr-element-with-embedded-content + ([element resource content] (tr-element-with-embedded-content element resource content nil)) + ([element resource content params] + ;; note - sometimes a nil value will be passed into a tr, or the key is computed on the frontend + ;; and the 'else' value is nil (ie no run phase), so we need to make sure there is no black + ;; screen in a case like this. + (if (seq resource) + (let [{translation :translation + success? :target-language} (tr-with-info resource params)] + [element + (merge {:data-i18n-key (first resource) :data-i18n-success success?} (i18n-keys params)) + (or (embed-content translation content) "-")]) + [element {:data-i18n-failure true} "[no resource]"]))) + +(defn tr-element + ([element resource] (tr-element element resource nil)) + ([element resource params] (tr-element-with-embedded-content element resource nil params))) + +(defn tr-span + ([resource] (tr-element :span resource nil)) + ([resource params] (tr-element :span resource params))) + +(defn clean-input + [s] + (assert (seq s) "Given empty string") + (-> (or s "") + (str/replace " " "-") + (str/replace "&" "-") + (str/replace "'" "-") + (str/replace "." "") + (str/lower-case))) + +(defn tr-fix-server-name + [s] + (let [cleaned (clean-input s)] + (if-let [[_ num] (re-matches #"server-(\d+)" cleaned)] + {:msg "server-num" :num num} + {:msg cleaned}))) + +(defn tr-type [s] (tr [:card-type_name s] {:type (clean-input s)})) +(defn tr-side [s] (tr [:side_name s] {:side (clean-input s)})) +(defn tr-faction [s] (tr [:faction_name s] {:faction (clean-input s)})) +(defn tr-format [s] (tr [:format_name s] {:format (clean-input s)})) +(defn tr-sort-order [s] (tr [:sort_order_name s] {:sort (clean-input s)})) +(defn tr-room-type [s] (tr [:lobby_type s] {:type (clean-input s)})) +(defn tr-pronouns [s] (tr [:pronouns s] {:pronoun (clean-input s)})) +(defn tr-set [s] + (let [s (if (#{"0" "1" "2" "3" "4" "5" "6" "7" "8" "9"} (first s)) + (str "a" s) + s)] + (tr [:set_name s] {:name (clean-input s)}))) +(defn tr-game-prompt [s] (tr [:game_prompt s] (tr-fix-server-name s))) + +(defn tr-data [k data] + (or (get-in data [:localized k]) (k data))) + +;; game log messages + +(defn join-with-and + [ms] + (str/join (tr :join-with-and) ms)) + +(defn get-card-title + [m] + (if (:cid m) (get-title m) m)) + +(defn join-list + [ms] + (->> ms + (mapv get-card-title) + (str/join (tr :join-list)))) + +(defonce server-names (atom {})) +(reset! server-names {}) + +(defn format-server-name + [server-name] + (let [lang @language-cursor] + (or (get-in @server-names [lang server-name]) + (let [s (name server-name) + sn (case s + ("HQ" "hq") "hq" + ("R&D" "rd") "rd" + ("Archives" "archives") "archives" + #_:else + (cond + (str/starts-with? s "Server") (subs s 7) + (str/starts-with? s "remote") (str (parse-long (subs s 6))))) + tr (tr :server-name {:server sn})] + (-> (swap! server-names assoc-in [lang server-name] tr) + (get-in [lang server-name])))))) + +(defn format-card-str + [card] + (let [m (card-str-edn card)] + (tr (:card/str m) m))) + +(defn format-payment-msg + [m] + (tr (:payment/type m) (update-keys m name))) + +(defn build-pay-msg + [ms] + (when-let [ms (seq ms)] + (->> ms + (mapv format-payment-msg) + (join-with-and)))) + +(defn format-msg-args + [m] + (let [new-m (reduce-kv + (fn [m k v] + (assoc! m (name k) + (cond + (#{:msg/payments + :effect/card-str :effect/card-strs + :msg/card-str :msg/card-strs} k) nil + (= :effect/server k) (format-server-name v) + (= :effect/title k) (get-card-title v) + (sequential? v) (join-list v) + :else v))) + (transient {}) m) + card-str (or (:effect/card-str m) + (:msg/card-str m)) + card-str2 (or (:effect/card-str2 m) + (:msg/card-str2 m)) + card-strs (or (:effect/card-strs m) + (:msg/card-strs m))] + (cond-> new-m + (:msg/payments m) (assoc! "payment" (build-pay-msg (:msg/payments m))) + card-str (assoc! "card-str" (format-card-str card-str)) + card-str2 (assoc! "card-str2" (format-card-str card-str2)) + card-strs (assoc! "card-strs" (->> card-strs + (mapv format-card-str) + (str/join (tr :join-list)))) + true (persistent!)))) + +(defn format-effect-msgs + [{effect-type :effect/type :as m}] + (tr effect-type (format-msg-args m))) + +(defn build-ability-msg + [ms] + (when-let [ms (seq ms)] + (->> ms + (mapv format-effect-msgs) + (join-with-and)))) + +(defn build-msg + [{msg-type :msg/type :as m}] + (let [m (if (:msg/effect-msgs m) + (assoc m :do-ability (build-ability-msg (:msg/effect-msgs m))) + m)] + (tr msg-type (format-msg-args m)))) + +;; definitions + +(defn process-effect-msgs + [effect-msgs] + (let [fs (cond (sequential? effect-msgs) (vec effect-msgs) + (map? effect-msgs) [effect-msgs])] + (not-empty (mapv #(schemas/assert % EffectMsg) fs)))) + +(defn payment->msg + [{:paid/keys [type value targets side] :as payment}] + (schemas/assert payment schemas/Payment) + (case type + :credit + (if-let [picks (seq (filter #(contains? % :pick-counters/type) targets))] + (vec (for [pick picks] + (case (:pick-counters/type pick) + :card {:payment/type "payment-hosted-credit" + :payment/value (:value pick) + :payment/title (:title pick)} + :bad-publicity {:payment/type "payment-bad-publicity" + :payment/value (:value pick)} + :credit-pool {:payment/type "payment-credit-pool" + :payment/value (:value pick)}))) + [{:payment/type "payment-credit" + :payment/value value}]) + :x-credits + [{:payment/type "payment-x-credit" + :payment/value (:x-value payment)}] + :trash-from-hand + [{:payment/type (if (= :corp side) + "payment-trash-from-hq" + "payment-trash-from-grip") + :payment/value (count targets) + :payment/titles (when-let [t (not-empty targets)] (mapv get-title t))}] + #_ :else + [{:payment/type (str "payment-" (name type)) + :payment/value value + :payment/count value + :payment/title (get-title (first targets)) + :payment/titles (when-let [t (not-empty targets)] (mapv get-title t))}])) + +(defn process-payments + [payments] + (when-let [ps (seq payments)] + (into [] (mapcat payment->msg) ps))) + +(defn ->effect-msg + [{:msg/keys [effect-msgs payments] :as base-msg}] + (let [effect-msgs (process-effect-msgs effect-msgs) + payments (process-payments payments) + args (dissoc base-msg :msg/effect-msgs :msg/payments)] + (cond-> {:msg/effect-msgs effect-msgs} + payments (assoc :msg/payments payments) + (not-empty args) (merge args)))) + +(defn msg-map->effect-msg [m] + (when m + (if (keyword? m) + {:effect/type m} + (schemas/assert m EffectMsg)))) + +(defn ->use-card-msg + ([card effect-msgs] (->use-card-msg card effect-msgs nil nil)) + ([card effect-msgs payments] (->use-card-msg card effect-msgs payments nil)) + ([card effect-msgs payments args] + (let [effect-msgs (flatten effect-msgs)] + (cond-> (->effect-msg {:msg/type (if (seq payments) :pay-use-card :use-card) + :msg/effect-msgs (vec (keep msg-map->effect-msg effect-msgs)) + :msg/payments payments + :msg/title (get-title card)}) + (map? args) (merge args))))) + +#?(:clj + (defmacro simple-msg + "wraps `game.macros/effect`, calls `->use-card-msg` with each opts as effect-msg map. + + can also be given a keyword, which is wrapped as `{:effect/type opt}`." + [opt & opts] + `(game.macros/effect + (->use-card-msg ~'card [~opt ~@opts])))) + +#?(:clj + (defmacro msg-with-cost + "wraps `game.macros/effect`, calls `->use-card-msg` with each opts as effect-msg map. + + can also be given a keyword, which is wrapped as `{:effect/type opt}`." + [opt & opts] + `(game.macros/effect + (->use-card-msg ~'card [~opt ~@opts] (vals (:cost-paid ~'eid)))))) + +(defn ->msg-map + ([m] (->msg-map m nil)) + ([m args] + (when m + (if (keyword? m) + {:msg/type m} + (-> m + (merge args) + (schemas/assert MsgMap)))))) diff --git a/src/cljc/jinteki/validator.cljc b/src/cljc/jinteki/validator.cljc index f809ec8eb2..bc6af99c1b 100644 --- a/src/cljc/jinteki/validator.cljc +++ b/src/cljc/jinteki/validator.cljc @@ -220,7 +220,8 @@ ([status card] (legal? :standard status card)) ([fmt status card] - (contains? (get-in card [:format fmt]) status))) + (or (contains? (get-in card [:format fmt]) status) + (#{:casual :quick-draft :preconstructed} fmt)))) (defn legal-line? ([status line] diff --git a/src/cljs/nr/about.cljs b/src/cljs/nr/about.cljs index 53b4330830..81d73b054f 100644 --- a/src/cljs/nr/about.cljs +++ b/src/cljs/nr/about.cljs @@ -4,7 +4,7 @@ [cljs.core.async :refer [format store-scroll-top tr-non-game-toast]] - [jinteki.i18n :as i18n] [jinteki.card-backs :as card-backs] [reagent-modals.modals :as reagent-modals] [reagent.core :as r])) diff --git a/src/cljs/nr/appstate.cljs b/src/cljs/nr/appstate.cljs index 4d5c34eb95..c599df90a0 100644 --- a/src/cljs/nr/appstate.cljs +++ b/src/cljs/nr/appstate.cljs @@ -1,13 +1,8 @@ (ns nr.appstate (:require - [cljs.core.async :refer [int]] - [nr.ajax :refer [GET]] [nr.local-storage :as ls] - [clojure.string :as str] - [clojure.set :as set] [reagent.core :as r])) (defn- migrate-legacy-localStorage-keys! @@ -18,7 +13,6 @@ "lobby_sounds" "lobby-sounds" "volume" "sounds-volume"})) - ;; These "new-formats" will automatically be added into the visible-formats for a player exactly once (def new-formats #{"quick-draft" "chimera"}) @@ -78,10 +72,5 @@ :current-game nil :block-game-creation false}))) -(go (let [lang (get-in @app-state [:options :language] "en") - response (slug get-image-path image-or-face influence-dots non-game-toast render-safe-html restricted-span rotated-span diff --git a/src/cljs/nr/chat.cljs b/src/cljs/nr/chat.cljs index de81e49560..06babde190 100644 --- a/src/cljs/nr/chat.cljs +++ b/src/cljs/nr/chat.cljs @@ -13,7 +13,7 @@ [nr.gameboard.card-preview :refer [card-preview-mouse-out card-preview-mouse-over]] [nr.news :refer [news]] - [nr.translations :refer [tr tr-element tr-pronouns tr-span]] + [jinteki.i18n :refer [tr tr-element tr-pronouns tr-span]] [nr.utils :refer [non-game-toast render-message format-date-time day-word-with-time-formatter tr-non-game-toast scroll-to-bottom!]] diff --git a/src/cljs/nr/deck_status.cljs b/src/cljs/nr/deck_status.cljs index 21872146f0..6cb8856ad1 100644 --- a/src/cljs/nr/deck_status.cljs +++ b/src/cljs/nr/deck_status.cljs @@ -1,7 +1,7 @@ (ns nr.deck-status (:require [jinteki.validator :refer [calculate-deck-status trusted-deck-status]] - [nr.translations :refer [tr tr-format tr-span]] + [jinteki.i18n :refer [tr tr-format tr-span]] [nr.utils :refer [slug->format]] [clojure.string :as str])) diff --git a/src/cljs/nr/deckbuilder.cljs b/src/cljs/nr/deckbuilder.cljs index dfd37f0554..a9d6e5e2b9 100644 --- a/src/cljs/nr/deckbuilder.cljs +++ b/src/cljs/nr/deckbuilder.cljs @@ -11,7 +11,7 @@ [nr.auth :refer [authenticated] :as auth] [nr.cardbrowser :refer [cards-channel factions filter-title image-url] :as cb] [nr.deck-status :refer [deck-status-span]] - [nr.translations :refer [tr tr-span tr-element tr-faction tr-format tr-side tr-sort-order tr-type tr-data]] + [jinteki.i18n :refer [tr tr-span tr-element tr-faction tr-format tr-side tr-sort-order tr-type tr-data]] [nr.utils :refer [alliance-dots banned-span cond-button deck-points-card-span dots-html buildable-format->slug format-date-time influence-dot influence-dots mdy-formatter non-game-toast diff --git a/src/cljs/nr/end_of_game_stats.cljs b/src/cljs/nr/end_of_game_stats.cljs index 2a545a7d03..e81b60c4c7 100644 --- a/src/cljs/nr/end_of_game_stats.cljs +++ b/src/cljs/nr/end_of_game_stats.cljs @@ -1,5 +1,5 @@ (ns nr.end-of-game-stats - (:require [nr.translations :refer [tr tr-side tr-element]] + (:require [jinteki.i18n :refer [tr tr-side tr-element]] [nr.utils :refer [map-longest]])) (defn computed-stat diff --git a/src/cljs/nr/game_row.cljs b/src/cljs/nr/game_row.cljs index 27f2c44a0d..7b6ef90b81 100644 --- a/src/cljs/nr/game_row.cljs +++ b/src/cljs/nr/game_row.cljs @@ -10,7 +10,7 @@ [nr.auth :refer [authenticated] :as auth] [nr.player-view :refer [player-view]] [nr.sounds :refer [resume-sound]] - [nr.translations :refer [tr tr-format tr-span]] + [jinteki.i18n :refer [tr tr-format tr-span]] [nr.utils :refer [slug->format]] [nr.ws :as ws] [reagent.core :as r])) diff --git a/src/cljs/nr/gameboard/actions.cljs b/src/cljs/nr/gameboard/actions.cljs index fc2cff0d4f..f3b47b0c88 100644 --- a/src/cljs/nr/gameboard/actions.cljs +++ b/src/cljs/nr/gameboard/actions.cljs @@ -2,16 +2,17 @@ (:require [differ.core :as differ] [goog.functions :as gfn] + [jinteki.i18n :refer [tr-span]] [nr.appstate :refer [app-state current-gameid]] [nr.gameboard.card-preview :refer [put-game-card-in-channel zoom-channel]] [nr.gameboard.replay :refer [init-replay]] [nr.gameboard.state :refer [check-lock? game-state get-side last-state parse-state]] - [nr.translations :refer [tr-span]] - [nr.utils :refer [toastr-options]] [nr.sounds :refer [play-sfx]] + [nr.utils :refer [toastr-options]] [nr.ws :as ws] [reagent.core :as r] + [reagent.dom.server :as rdom-server] [reagent.ratom :as ratom])) (defn reset-game! [state] @@ -133,7 +134,7 @@ (js/escape (str "Please describe the circumstances of your error here.\n\n\nStack Trace:\n```clojure\n" error "\n```")))] - (reagent.dom.server/render-to-string + (rdom-server/render-to-string [:div msg [:br] [:a.button {:type "button" @@ -153,7 +154,7 @@ (let [msg (if (vector? msg) [tr-span msg] msg)] (when-let [f (aget js/toastr (if (= "exception" toast-type) "error" toast-type))] (f (if (= "exception" toast-type) (build-exception-msg msg (:last-error @game-state)) - (if (vector? msg) (reagent.dom.server/render-to-string msg) msg)))))) + (if (vector? msg) (rdom-server/render-to-string msg) msg)))))) (defonce side (r/cursor game-state [:side])) (defonce me-toasts (ratom/reaction (get-in @game-state [@side :toast]))) diff --git a/src/cljs/nr/gameboard/board.cljs b/src/cljs/nr/gameboard/board.cljs index e1f207ee2d..215ab35c1e 100644 --- a/src/cljs/nr/gameboard/board.cljs +++ b/src/cljs/nr/gameboard/board.cljs @@ -27,7 +27,7 @@ [nr.gameboard.right-pane :refer [content-pane]] [nr.gameboard.state :refer [game-state not-spectator? replay-side]] [nr.sounds :refer [update-audio]] - [nr.translations :refer [tr tr-data tr-game-prompt tr-side tr-element tr-span]] + [jinteki.i18n :refer [tr tr-data tr-game-prompt tr-side tr-element tr-span]] [nr.utils :refer [banned-span card-colors-class card-colors-custom-style checkbox-button cond-button get-image-path image-or-face map-longest precon-decklist-links @@ -1004,7 +1004,7 @@ tr-vec (if is-runner [:game_stack-count (str "Stack (" deck-count-number ")")] [:game_rnd-count (str "R&D (" deck-count-number ")")]) - title (tr (if is-runner [:game_stack "Stack"] [:game_rnd "R&D"])) + title (tr (if is-runner [:game_stack "Stack"] [:game_rnd "R&D"]) {:cnt deck-count-number}) ref (if is-runner "stack" "rd") menu-ref (keyword (str ref "-menu")) content-ref (keyword (str ref "-content"))] diff --git a/src/cljs/nr/gameboard/diagrams.cljs b/src/cljs/nr/gameboard/diagrams.cljs index 81a6020dd5..1735d45a41 100644 --- a/src/cljs/nr/gameboard/diagrams.cljs +++ b/src/cljs/nr/gameboard/diagrams.cljs @@ -1,7 +1,7 @@ (ns nr.gameboard.diagrams (:require [nr.appstate :refer [app-state]] - [nr.translations :refer [tr tr-span tr-element]] + [jinteki.i18n :refer [tr tr-span tr-element]] [nr.utils :refer [render-icons]])) (defn- bullet diff --git a/src/cljs/nr/gameboard/log.cljs b/src/cljs/nr/gameboard/log.cljs index bb1a49d98a..754d96a4a0 100644 --- a/src/cljs/nr/gameboard/log.cljs +++ b/src/cljs/nr/gameboard/log.cljs @@ -9,7 +9,7 @@ [nr.gameboard.card-preview :refer [card-preview-mouse-out card-preview-mouse-over zoom-channel]] [nr.gameboard.state :refer [game-state not-spectator?]] - [nr.translations :refer [tr tr-span]] + [jinteki.i18n :refer [tr tr-span]] [nr.utils :refer [player-highlight-option-class render-message render-player-highlight scroll-to-bottom!]] [nr.ws :as ws] @@ -292,19 +292,14 @@ (defn format-system-timestamp [timestamp text corp runner] (if (get-in @app-state [:options :log-timestamps]) (render-message (render-player-highlight text corp runner (str "[" (string/replace (.toLocaleTimeString (js/Date. timestamp)) #"\s\w*" "") "]"))) - (render-message (render-player-highlight text corp runner)) - ) - ) + (render-message (render-player-highlight text corp runner)))) (defn format-user-timestamp [timestamp user] (if (get-in @app-state [:options :log-timestamps]) [:div.timestamp-wrapper [:div.username (:username user)] - [:div.timestamp "[" (string/replace (.toLocaleTimeString (js/Date. timestamp)) #"\s\w*" "") "]"] - ] - [:div.username (:username user)] - ) - ) + [:div.timestamp "[" (string/replace (.toLocaleTimeString (js/Date. timestamp)) #"\s\w*" "") "]"]] + [:div.username (:username user)])) (defn log-messages [] (let [log (r/cursor game-state [:log]) diff --git a/src/cljs/nr/gameboard/player_stats.cljs b/src/cljs/nr/gameboard/player_stats.cljs index 10d45aa2f8..7e54b69530 100644 --- a/src/cljs/nr/gameboard/player_stats.cljs +++ b/src/cljs/nr/gameboard/player_stats.cljs @@ -5,7 +5,7 @@ [nr.avatar :refer [avatar]] [nr.gameboard.actions :refer [send-command]] [nr.gameboard.state :refer [game-state not-spectator?]] - [nr.translations :refer [tr tr-span tr-element tr-pronouns]] + [jinteki.i18n :refer [tr tr-span tr-element tr-pronouns]] [reagent.core :as r])) (defn stat-controls diff --git a/src/cljs/nr/gameboard/replay.cljs b/src/cljs/nr/gameboard/replay.cljs index 7cf0ac713a..b62c04af9c 100644 --- a/src/cljs/nr/gameboard/replay.cljs +++ b/src/cljs/nr/gameboard/replay.cljs @@ -9,7 +9,7 @@ [nr.gameboard.state :refer [game-state last-state replay-side]] [nr.local-storage :as ls] [nr.new-game :refer [create-game]] - [nr.translations :refer [tr tr-span tr-element]] + [jinteki.i18n :refer [tr tr-span tr-element]] [nr.utils :refer [tr-non-game-toast render-message]] [nr.ws :as ws] [reagent.core :as r])) diff --git a/src/cljs/nr/gameboard/right_pane.cljs b/src/cljs/nr/gameboard/right_pane.cljs index a6c7557bb6..fc43a211d8 100644 --- a/src/cljs/nr/gameboard/right_pane.cljs +++ b/src/cljs/nr/gameboard/right_pane.cljs @@ -9,7 +9,7 @@ [nr.gameboard.replay :refer [notes-pane notes-shared-pane]] [nr.gameboard.state :refer [game-state]] [nr.gameboard.settings :refer [settings-pane]] - [nr.translations :refer [tr-span]] + [jinteki.i18n :refer [tr-span]] [reagent.core :as r])) (defonce loaded-tabs (r/atom {})) diff --git a/src/cljs/nr/gameboard/settings.cljs b/src/cljs/nr/gameboard/settings.cljs index d02acfa245..b6b3be0ae7 100644 --- a/src/cljs/nr/gameboard/settings.cljs +++ b/src/cljs/nr/gameboard/settings.cljs @@ -3,7 +3,7 @@ [nr.account :refer [post-options]] [nr.appstate :refer [app-state]] [nr.gameboard.pane-size :refer [reset-pane-size!]] - [nr.translations :refer [tr-element tr-span]])) + [jinteki.i18n :refer [tr-element tr-span]])) (defn settings-pane [] (fn [] diff --git a/src/cljs/nr/landing.cljs b/src/cljs/nr/landing.cljs index 869890567b..0317cf22c2 100644 --- a/src/cljs/nr/landing.cljs +++ b/src/cljs/nr/landing.cljs @@ -1,6 +1,6 @@ (ns nr.landing (:require - [nr.translations :refer [tr-element tr-element-with-embedded-content tr-span]])) + [jinteki.i18n :refer [tr-element tr-element-with-embedded-content tr-span]])) (def landing-content [:div.landing.panel.content-page.blue-shade diff --git a/src/cljs/nr/lobby.cljs b/src/cljs/nr/lobby.cljs index fe57f60cc6..89182f8523 100644 --- a/src/cljs/nr/lobby.cljs +++ b/src/cljs/nr/lobby.cljs @@ -17,7 +17,7 @@ [nr.pending-game :refer [pending-game]] [nr.replay-game :refer [start-replay-div]] [nr.sounds :refer [play-sound resume-sound]] - [nr.translations :refer [tr tr-span tr-element tr-format]] + [jinteki.i18n :refer [tr tr-span tr-element tr-format]] [nr.utils :refer [cond-button non-game-toast tr-non-game-toast slug->format]] [nr.ws :as ws] [reagent.core :as r] diff --git a/src/cljs/nr/lobby_chat.cljs b/src/cljs/nr/lobby_chat.cljs index fdc06000d5..6b42493553 100644 --- a/src/cljs/nr/lobby_chat.cljs +++ b/src/cljs/nr/lobby_chat.cljs @@ -1,7 +1,7 @@ (ns nr.lobby-chat (:require [nr.avatar :refer [avatar]] - [nr.translations :refer [tr tr-element]] + [jinteki.i18n :refer [tr tr-element]] [nr.utils :refer [scroll-to-bottom!]] [nr.ws :as ws] [reagent.core :as r] diff --git a/src/cljs/nr/navbar.cljs b/src/cljs/nr/navbar.cljs index 620f5b5699..1312d43dbf 100644 --- a/src/cljs/nr/navbar.cljs +++ b/src/cljs/nr/navbar.cljs @@ -4,7 +4,7 @@ [goog.history.EventType :as EventType] [nr.appstate :refer [app-state]] [nr.routes :as routes] - [nr.translations :refer [tr tr-span]]) + [jinteki.i18n :refer [tr tr-span]]) (:import goog.history.Html5History)) diff --git a/src/cljs/nr/new_game.cljs b/src/cljs/nr/new_game.cljs index 1753d3f039..c2c0235b66 100644 --- a/src/cljs/nr/new_game.cljs +++ b/src/cljs/nr/new_game.cljs @@ -4,7 +4,7 @@ [jinteki.preconstructed :refer [all-matchups matchup-by-key]] [nr.appstate :refer [app-state]] [nr.auth :refer [authenticated] :as auth] - [nr.translations :refer [tr tr-format tr-side tr-element tr-span]] + [jinteki.i18n :refer [tr tr-format tr-side tr-element tr-span]] [nr.utils :refer [cond-button focus-on-mount precon-decklist-links slug->format]] [nr.ws :as ws] [reagent.core :as r])) diff --git a/src/cljs/nr/password_game.cljs b/src/cljs/nr/password_game.cljs index 5809f37830..81f50c7fec 100644 --- a/src/cljs/nr/password_game.cljs +++ b/src/cljs/nr/password_game.cljs @@ -1,7 +1,7 @@ (ns nr.password-game (:require [nr.auth :refer [authenticated]] - [nr.translations :refer [tr tr-span tr-element tr-room-type]] + [jinteki.i18n :refer [tr tr-span tr-element tr-room-type]] [nr.utils :refer [focus-on-mount]] [nr.ws :as ws] [reagent.core :as r] diff --git a/src/cljs/nr/pending_game.cljs b/src/cljs/nr/pending_game.cljs index c20ad3d08c..6b1488ae0b 100644 --- a/src/cljs/nr/pending_game.cljs +++ b/src/cljs/nr/pending_game.cljs @@ -9,7 +9,7 @@ [nr.deckbuilder :refer [deck-name default-deck?]] [nr.lobby-chat :refer [lobby-chat]] [nr.player-view :refer [player-view]] - [nr.translations :refer [tr tr-element tr-element-with-embedded-content tr-span tr-side]] + [jinteki.i18n :refer [tr tr-element tr-element-with-embedded-content tr-span tr-side]] [nr.utils :refer [cond-button format-date-time mdy-formatter precon-decklist-links tr-non-game-toast non-game-toast]] [nr.ws :as ws] diff --git a/src/cljs/nr/player_view.cljs b/src/cljs/nr/player_view.cljs index 2391ae49ab..ef6ffbc923 100644 --- a/src/cljs/nr/player_view.cljs +++ b/src/cljs/nr/player_view.cljs @@ -1,7 +1,7 @@ (ns nr.player-view (:require [reagent.core :as r] [nr.avatar :refer [avatar]] - [nr.translations :refer [tr tr-span tr-side]] + [jinteki.i18n :refer [tr tr-span tr-side]] [nr.utils :refer [faction-icon notnum->zero num->percent]])) (defn user-status-span diff --git a/src/cljs/nr/replay_game.cljs b/src/cljs/nr/replay_game.cljs index 3fbc76cf63..72e0545dae 100644 --- a/src/cljs/nr/replay_game.cljs +++ b/src/cljs/nr/replay_game.cljs @@ -2,7 +2,7 @@ (:require [nr.auth :refer [authenticated]] [nr.ws :as ws] - [nr.translations :refer [tr-span]] + [jinteki.i18n :refer [tr-span]] [reagent.core :as r])) (defn on-load [onload-ev] diff --git a/src/cljs/nr/stats.cljs b/src/cljs/nr/stats.cljs index 15bfafe9d2..b1c0d01cd1 100644 --- a/src/cljs/nr/stats.cljs +++ b/src/cljs/nr/stats.cljs @@ -9,7 +9,7 @@ [nr.auth :refer [authenticated] :as auth] [nr.avatar :refer [avatar]] [nr.end-of-game-stats :refer [build-game-stats]] - [nr.translations :refer [tr tr-format tr-room-type tr-side]] + [jinteki.i18n :refer [tr tr-format tr-room-type tr-side]] [nr.utils :refer [day-word-with-time-formatter faction-icon format-date-time notnum->zero safe-divide player-highlight-option-class render-message render-player-highlight set-scroll-top store-scroll-top]] diff --git a/src/cljs/nr/status_bar.cljs b/src/cljs/nr/status_bar.cljs index 6c4f7bf9f0..ea86cc9ff1 100644 --- a/src/cljs/nr/status_bar.cljs +++ b/src/cljs/nr/status_bar.cljs @@ -5,7 +5,7 @@ [nr.gameboard.replay :refer [set-replay-side]] [nr.lobby :refer [filter-games leave-game]] [nr.player-view :refer [player-view]] - [nr.translations :refer [tr tr-span]] + [jinteki.i18n :refer [tr tr-span]] [nr.ws :as ws] [nr.zoom :as zoom] [reagent.core :as r])) diff --git a/src/cljs/nr/translations.cljs b/src/cljs/nr/translations.cljs deleted file mode 100644 index 770304a2f4..0000000000 --- a/src/cljs/nr/translations.cljs +++ /dev/null @@ -1,99 +0,0 @@ -(ns nr.translations - (:require - [clojure.string :as str] - [jinteki.i18n :as i18n] - [nr.appstate :refer [app-state]] - [reagent.core :as r])) - -(def language-cursor - (r/cursor app-state [:options :language])) - -(defn tr-with-info - ([resource] (tr-with-info resource nil)) - ([resource params] - (i18n/format language-cursor resource params))) - -(defn tr - ([resource] (tr resource nil)) - ([resource params] - (:translation (tr-with-info resource params)))) - -(defn- i18n-keys - "put params into tr-element as data if needed" - [params] - (into {} (map (fn [[k v]] [(keyword (str "data-i18n-param-" (name k))) (str v)])) params)) - -(defn- embed-content - [translation content] - ;; note - it's very awkward to do translations where there is an embedded element on the inside - ;; this will allow us to do that by passing in a content map, like: - ;; {:link [:a {:href ....}]} - (if (or (not content) (empty? content)) - translation - (do - (loop [elements [translation] - [pattern & patterns] (keys content)] - (let [insert (get content pattern) - reg (re-pattern (str "\\[" (name pattern) "]")) - new-elements (mapcat #(if-not (string? %) - [%] (interpose (get content pattern) (str/split % reg))) - elements)] - (if-not patterns - (into [:span] new-elements) - (recur new-elements patterns))))))) - -(defn tr-element-with-embedded-content - ([element resource content] (tr-element-with-embedded-content element resource content nil)) - ([element resource content params] - ;; note - sometimes a nil value will be passed into a tr, or the key is computed on the frontend - ;; and the 'else' value is nil (ie no run phase), so we need to make sure there is no black - ;; screen in a case like this. - (if (seq resource) - (let [{translation :translation - success? :target-language} (tr-with-info resource params)] - [element - (merge {:data-i18n-key (first resource) :data-i18n-success success?} (i18n-keys params)) - (or (embed-content translation content) "-")]) - [element {:data-i18n-failure true} "[no resource]"]))) - -(defn tr-element - ([element resource] (tr-element element resource nil)) - ([element resource params] (tr-element-with-embedded-content element resource nil params))) - -(defn tr-span - ([resource] (tr-element :span resource nil)) - ([resource params] (tr-element :span resource params))) - -(defn clean-input - [s] - (assert (seq s) "Given empty string") - (-> (or s "") - (str/replace " " "-") - (str/replace "&" "-") - (str/replace "'" "-") - (str/replace "." "") - (str/lower-case))) - -(defn tr-fix-server-name - [s] - (let [cleaned (clean-input s)] - (if-let [[_ num] (re-matches #"server-(\d+)" cleaned)] - {:msg "server-num" :num num} - {:msg cleaned}))) - -(defn tr-type [s] (tr [:card-type_name s] {:type (clean-input s)})) -(defn tr-side [s] (tr [:side_name s] {:side (clean-input s)})) -(defn tr-faction [s] (tr [:faction_name s] {:faction (clean-input s)})) -(defn tr-format [s] (tr [:format_name s] {:format (clean-input s)})) -(defn tr-sort-order [s] (tr [:sort_order_name s] {:sort (clean-input s)})) -(defn tr-room-type [s] (tr [:lobby_type s] {:type (clean-input s)})) -(defn tr-pronouns [s] (tr [:pronouns s] {:pronoun (clean-input s)})) -(defn tr-set [s] - (let [s (if (#{"0" "1" "2" "3" "4" "5" "6" "7" "8" "9"} (first s)) - (str "a" s) - s)] - (tr [:set_name s] {:name (clean-input s)}))) -(defn tr-game-prompt [s] (tr [:game_prompt s] (tr-fix-server-name s))) - -(defn tr-data [k data] - (or (get-in data [:localized k]) (k data))) diff --git a/src/cljs/nr/utils.cljs b/src/cljs/nr/utils.cljs index bdf9db1f0f..6b5092a43b 100644 --- a/src/cljs/nr/utils.cljs +++ b/src/cljs/nr/utils.cljs @@ -5,11 +5,11 @@ [cljc.java-time.zoned-date-time :as zdt] [cljc.java-time.zone-id :as zone] [clojure.string :refer [join] :as s] + [jinteki.i18n :refer [build-msg tr tr-span tr-data]] [goog.object :as gobject] [goog.string :as gstring] [goog.string.format] [nr.appstate :refer [app-state]] - [nr.translations :refer [tr tr-span tr-data]] [flatland.ordered.map :refer [ordered-map]] [reagent.dom :as rd] [reagent.dom.server :as rdom-server])) @@ -310,8 +310,14 @@ (defn render-input "Sanitize inputs into fragments before processing them with render-fragment" [input patterns] - (if (not (or (string? input) (vector? input))) + (cond + (map? input) + (if-let [raw (:raw-text input)] + [:<> raw] + (render-fragment [:<> (build-msg input)] patterns)) + (not (or (string? input) (vector? input))) [:<>] + :else (let [fragment (if (string? input) [:<> input] input)] (render-fragment fragment patterns)))) diff --git a/src/cljs/nr/ws.cljs b/src/cljs/nr/ws.cljs index 6badf200b5..45aa635b5b 100644 --- a/src/cljs/nr/ws.cljs +++ b/src/cljs/nr/ws.cljs @@ -3,7 +3,7 @@ [jinteki.msgpack-ext] [nr.ajax :refer [?csrf-token]] [nr.appstate :refer [app-state current-gameid]] - [nr.translations :refer [tr]] + [jinteki.i18n :refer [tr]] [nr.utils :refer [non-game-toast]] [reagent.core :as r] [taoensso.sente :as sente :refer [start-client-chsk-router!]] diff --git a/src/cljs/nr/zoom.cljs b/src/cljs/nr/zoom.cljs index 642e5049dd..1f05a06f11 100644 --- a/src/cljs/nr/zoom.cljs +++ b/src/cljs/nr/zoom.cljs @@ -4,7 +4,7 @@ [jinteki.utils :refer [clamp]] [nr.appstate :refer [app-state]] [nr.local-storage :as ls] - [nr.translations :refer [tr]] + [jinteki.i18n :refer [tr]] [reagent.core :as r])) (def zoom-cursor (r/cursor app-state [:options :zoom])) diff --git a/test/clj/game/cards/events_test.clj b/test/clj/game/cards/events_test.clj index cb106a5f2f..444c992138 100644 --- a/test/clj/game/cards/events_test.clj +++ b/test/clj/game/cards/events_test.clj @@ -954,11 +954,11 @@ (is (last-log-contains? state "reveal Hedge Fund, Hedge Fund, and Hedge Fund from HQ")) (is (changed? [(count (:deck (get-corp))) 2 (count (:hand (get-corp))) -2] - (click-prompt state :runner "Hedge Fund") - (click-prompt state :runner "Top of R&D") - (click-prompt state :runner "Hedge Fund") - (click-prompt state :runner "Bottom of R&D")) - "2 cards added to R&D"))) + (click-prompt state :runner "Hedge Fund") + (click-prompt state :runner "Top of R&D") + (click-prompt state :runner "Hedge Fund") + (click-prompt state :runner "Bottom of R&D")) + "2 cards added to R&D"))) (deftest by-any-means-full-test ;; Full test @@ -1671,7 +1671,7 @@ (take-credits state :corp) (play-from-hand state :runner "Concerto") (let [concerto (-> (get-runner) :play-area first)] - (is (last-log-contains? state "reveal Sure Gamble")) + (is (last-log-contains? state #"reveal.*Sure Gamble")) (is (find-card "Sure Gamble" (:hand (get-runner)))) (is (= 5 (get-counters (refresh concerto) :credit)))) (click-prompt state :runner "HQ") @@ -2990,8 +2990,8 @@ :runner {:hand ["Executive Wiretaps"]}}) (take-credits state :corp) (play-from-hand state :runner "Executive Wiretaps") - (is (last-log-contains? state (str "Runner uses Executive Wiretaps to reveal " - "Cayambe Grid, Hedge Fund, Hostile Takeover, Ice Wall, and PAD Campaign from HQ."))))) + (is (last-log-contains? state (str "Runner uses Executive Wiretaps to reveal 5 cards" + " (Cayambe Grid, Hedge Fund, Hostile Takeover, Ice Wall, PAD Campaign) from HQ."))))) (deftest exploit ;; Exploit @@ -5040,11 +5040,10 @@ (click-prompt state :runner "Virtual") (click-prompt state :runner "Yes") (click-prompt state :runner "Cookbook") - (is (changed? [(:credit (get-runner)) 2] + (is (changed? [(:credit (get-runner)) 1] (click-card state :runner "Cookbook") - (click-card state :runner "Always Be Running") (click-prompt state :runner "Done")) - "Runner gained 2 credits") + "Runner gained 1 credits") (is (no-prompt? state :runner)) ;; Choosing connection (play-from-hand state :runner "Meeting of Minds") @@ -5083,101 +5082,101 @@ "Corp took 1 BP without getting a prompt"))) (deftest mobius-second-run-triggered - ;; Second run triggered - (do-game - (new-game {:runner {:deck [(qty "Möbius" 3)]}}) - (starting-hand state :corp ["Hedge Fund"]) - (take-credits state :corp) - (play-from-hand state :runner "Möbius") - (run-continue-until state :success) - (click-prompt state :runner "No action") - (click-prompt state :runner "Yes") - (is (= [:rd] (get-in @state [:run :server])) "Second run on R&D triggered") - (run-continue state) - (run-continue state) - (click-prompt state :runner "No action") - (is (not (:run @state)) "Run is over") - (is (no-prompt? state :runner) "No prompt to run a third time"))) + ;; Second run triggered + (do-game + (new-game {:runner {:deck [(qty "Möbius" 3)]}}) + (starting-hand state :corp ["Hedge Fund"]) + (take-credits state :corp) + (play-from-hand state :runner "Möbius") + (run-continue-until state :success) + (click-prompt state :runner "No action") + (click-prompt state :runner "Yes") + (is (= [:rd] (get-in @state [:run :server])) "Second run on R&D triggered") + (run-continue state) + (run-continue state) + (click-prompt state :runner "No action") + (is (not (:run @state)) "Run is over") + (is (no-prompt? state :runner) "No prompt to run a third time"))) (deftest mobius-gain-4-credits-after-succesful-second-run - ;; Gain 4 credits after succesful second run - (do-game - (new-game {:runner {:deck [(qty "Möbius" 3)]}}) - (starting-hand state :corp ["Hedge Fund"]) - (take-credits state :corp) - (is (= 5 (:credit (get-runner)))) - (play-from-hand state :runner "Möbius") - (run-continue-until state :success) - (is (= 5 (:credit (get-runner)))) - (click-prompt state :runner "No action") - (click-prompt state :runner "Yes") - (run-continue state) - (run-continue state) - (click-prompt state :runner "No action") - (is (= 9 (:credit (get-runner)))))) + ;; Gain 4 credits after succesful second run + (do-game + (new-game {:runner {:deck [(qty "Möbius" 3)]}}) + (starting-hand state :corp ["Hedge Fund"]) + (take-credits state :corp) + (is (= 5 (:credit (get-runner)))) + (play-from-hand state :runner "Möbius") + (run-continue-until state :success) + (is (= 5 (:credit (get-runner)))) + (click-prompt state :runner "No action") + (click-prompt state :runner "Yes") + (run-continue state) + (run-continue state) + (click-prompt state :runner "No action") + (is (= 9 (:credit (get-runner)))))) (deftest mobius-no-second-run-if-first-is-unsuccesful - ;; No second run if first is unsuccesful - (do-game - (new-game {:runner {:deck [(qty "Möbius" 3)]}}) - (starting-hand state :corp ["Hedge Fund"]) - (take-credits state :corp) - (play-from-hand state :runner "Möbius") - (run-continue state) - (run-jack-out state) - (is (no-prompt? state :runner) "No option to run again on unsuccessful run"))) + ;; No second run if first is unsuccesful + (do-game + (new-game {:runner {:deck [(qty "Möbius" 3)]}}) + (starting-hand state :corp ["Hedge Fund"]) + (take-credits state :corp) + (play-from-hand state :runner "Möbius") + (run-continue state) + (run-jack-out state) + (is (no-prompt? state :runner) "No option to run again on unsuccessful run"))) (deftest mobius-normal-rnd-run-does-not-gain-cred - ;; Normal rnd run does not gain cred - (do-game - (new-game {:runner {:deck [(qty "Möbius" 3)]}}) - (starting-hand state :corp ["Hedge Fund"]) - (take-credits state :corp) - (play-from-hand state :runner "Möbius") - (run-continue-until state :success) - (click-prompt state :runner "No action") - (click-prompt state :runner "Yes") - (run-continue state) - (run-continue state) - (click-prompt state :runner "No action") - (is (changed? [(:credit (get-runner)) 0] - (run-empty-server state :rd) - (click-prompt state :runner "No action")) - "Normal run on R&D didn't give any credits"))) + ;; Normal rnd run does not gain cred + (do-game + (new-game {:runner {:deck [(qty "Möbius" 3)]}}) + (starting-hand state :corp ["Hedge Fund"]) + (take-credits state :corp) + (play-from-hand state :runner "Möbius") + (run-continue-until state :success) + (click-prompt state :runner "No action") + (click-prompt state :runner "Yes") + (run-continue state) + (run-continue state) + (click-prompt state :runner "No action") + (is (changed? [(:credit (get-runner)) 0] + (run-empty-server state :rd) + (click-prompt state :runner "No action")) + "Normal run on R&D didn't give any credits"))) (deftest mobius-recurred-use - ;; Recurred use - (do-game - (new-game {:runner {:deck ["Möbius" "Déjà Vu"]}}) - (starting-hand state :corp ["Hedge Fund"]) - (take-credits state :corp) - (is (= 5 (:credit (get-runner)))) - (play-from-hand state :runner "Möbius") - (run-continue state) - (run-continue state) - (is (= 5 (:credit (get-runner)))) - (click-prompt state :runner "No action") - (click-prompt state :runner "Yes") - (is (= [:rd] (get-in @state [:run :server])) "Second run on R&D triggered") - (run-continue state) - (run-continue state) - (click-prompt state :runner "No action") - (is (= 9 (:credit (get-runner)))) - (play-from-hand state :runner "Déjà Vu") - (click-prompt state :runner (find-card "Möbius" (:discard (get-runner)))) - (is (no-prompt? state :runner) "Recurring a non-virus card stops Déjà Vu prompting further") - (is (= 1 (count (:hand (get-runner))))) - (play-from-hand state :runner "Möbius") - (run-continue state) - (run-continue state) - (is (= 7 (:credit (get-runner)))) - (click-prompt state :runner "No action") - (click-prompt state :runner "Yes") - (is (= [:rd] (get-in @state [:run :server])) "Second run on R&D triggered") - (run-continue state) - (run-continue state) - (click-prompt state :runner "No action") - (is (= 11 (:credit (get-runner)))))) + ;; Recurred use + (do-game + (new-game {:runner {:deck ["Möbius" "Déjà Vu"]}}) + (starting-hand state :corp ["Hedge Fund"]) + (take-credits state :corp) + (is (= 5 (:credit (get-runner)))) + (play-from-hand state :runner "Möbius") + (run-continue state) + (run-continue state) + (is (= 5 (:credit (get-runner)))) + (click-prompt state :runner "No action") + (click-prompt state :runner "Yes") + (is (= [:rd] (get-in @state [:run :server])) "Second run on R&D triggered") + (run-continue state) + (run-continue state) + (click-prompt state :runner "No action") + (is (= 9 (:credit (get-runner)))) + (play-from-hand state :runner "Déjà Vu") + (click-prompt state :runner (find-card "Möbius" (:discard (get-runner)))) + (is (no-prompt? state :runner) "Recurring a non-virus card stops Déjà Vu prompting further") + (is (= 1 (count (:hand (get-runner))))) + (play-from-hand state :runner "Möbius") + (run-continue state) + (run-continue state) + (is (= 7 (:credit (get-runner)))) + (click-prompt state :runner "No action") + (click-prompt state :runner "Yes") + (is (= [:rd] (get-in @state [:run :server])) "Second run on R&D triggered") + (run-continue state) + (run-continue state) + (click-prompt state :runner "No action") + (is (= 11 (:credit (get-runner)))))) (deftest modded ;; Modded - Install a program or piece of hardware at a 3 credit discount @@ -5200,18 +5199,18 @@ (deftest moshing (do-game - (new-game {:runner {:deck [(qty "Sure Gamble" 5)] - :hand ["Moshing" (qty "Lamprey" 3)]}}) - (take-credits state :corp) - (is (= 0 (count (:discard (get-runner)))) "Runner has no cards in heap") - (is (= 4 (count (:hand (get-runner)))) "Runner starts with 4 cards") - (is (not (find-card "Sure Gamble" (:hand (get-runner)))) "Runner does not have Sure Gamble in grip") - (play-from-hand state :runner "Moshing") - (is (= 0 (count (:discard (get-runner)))) "Moshing is still in play") - (dotimes [card 3] (click-card state :runner (nth (:hand (get-runner)) card))) - (is (= 4 (count (:discard (get-runner)))) "Runner trashed 3 cards and discarded Moshing") - (is (= 3 (count (:hand (get-runner)))) "Runner draws 3 cards") - (is (find-card "Sure Gamble" (:hand (get-runner))) "Runner drew Sure Gamble"))) + (new-game {:runner {:deck [(qty "Sure Gamble" 5)] + :hand ["Moshing" (qty "Lamprey" 3)]}}) + (take-credits state :corp) + (is (= 0 (count (:discard (get-runner)))) "Runner has no cards in heap") + (is (= 4 (count (:hand (get-runner)))) "Runner starts with 4 cards") + (is (not (find-card "Sure Gamble" (:hand (get-runner)))) "Runner does not have Sure Gamble in grip") + (play-from-hand state :runner "Moshing") + (is (= 0 (count (:discard (get-runner)))) "Moshing is still in play") + (dotimes [card 3] (click-card state :runner (nth (:hand (get-runner)) card))) + (is (= 4 (count (:discard (get-runner)))) "Runner trashed 3 cards and discarded Moshing") + (is (= 3 (count (:hand (get-runner)))) "Runner draws 3 cards") + (is (find-card "Sure Gamble" (:hand (get-runner))) "Runner drew Sure Gamble"))) (deftest mutual-favor ;; Mutual Favor @@ -5353,7 +5352,7 @@ (let [grip (-> (get-runner) :hand count)] (click-prompt state :runner "Draw 4 cards") (is (= (+ 4 grip) (-> (get-runner) :hand count)) "Runner should draw 4 cards"))))] - (doall (map office-supplies-test (range 5))))) + (run! office-supplies-test (range 5)))) (deftest on-the-lam-vs-tags ;; vs tags @@ -5550,8 +5549,9 @@ :runner {:hand ["Paper Tripping"]}}) (take-credits state :corp) (gain-tags state :runner 100) - (play-from-hand state :runner "Paper Tripping") - (is (zero? (count-tags state)) "Runner should lose all tags"))) + (is (changed? [(count-tags state) -100] + (play-from-hand state :runner "Paper Tripping")) + "Runner should lose all tags"))) (deftest peace-in-our-time-no-runs-allowed ;; no runs allowed @@ -5675,17 +5675,12 @@ :runner {:hand [(qty "Power Nap" 3)]}}) (take-credits state :corp) (core/gain state :runner :click 2) - (let [credits (:credit (get-runner))] - (play-from-hand state :runner "Power Nap") - (is (= (+ credits 2) (:credit (get-runner))) "Runner should gain 2")) - (let [credits (:credit (get-runner))] - (play-from-hand state :runner "Power Nap") - (is (= (+ credits 3) (:credit (get-runner))) - "Runner should gain 3 for 1 double in heap")) - (let [credits (:credit (get-runner))] - (play-from-hand state :runner "Power Nap") - (is (= (+ credits 4) (:credit (get-runner))) - "Runner should gain 4 for 2 doubles in heap")))) + (is (changed? [(:credit (get-runner)) 2] + (play-from-hand state :runner "Power Nap"))) + (is (changed? [(:credit (get-runner)) 3] + (play-from-hand state :runner "Power Nap"))) + (is (changed? [(:credit (get-runner)) 4] + (play-from-hand state :runner "Power Nap"))))) (deftest power-to-the-people ;; Power to the People - Gain 7c the first time you access an agenda @@ -5995,12 +5990,12 @@ (is (core/can-access? state :runner other-pad)) "Not prevented from accessing other cards") (take-credits state :runner) (take-credits state :corp) - (let [pad (get-content state :remote1 0) - runner-creds (:credit (get-runner))] - (run-empty-server state "Server 1") - (is (core/can-access? state :runner (refresh pad)) "Can access PAD Campgain next turn") - (click-prompt state :runner "Pay 4 [Credits] to trash") - (is (= (- runner-creds 4) (:credit (get-runner))) "Paid 4 credits to trash PAD Campaign")))) + (is (changed? [(:credit (get-runner)) -4] + (run-empty-server state "Server 1") + (is (core/can-access? state :runner (get-content state :remote1 0)) + "Can access PAD Campgain next turn") + (click-prompt state :runner "Pay 4 [Credits] to trash")) + "Paid 4 credits to trash PAD Campaign"))) (deftest quest-completed ;; Quest Completed @@ -6036,7 +6031,7 @@ (is (zero? (count (:hand (get-runner)))) "hand wiped by komainu") (run-continue state) (is (changed? [(:credit (get-runner)) +3 - (count (:hand (get-runner))) +5] + (count (:hand (get-runner))) +5] (run-continue state)) "gained 3 credits from raindrop")))) @@ -6441,7 +6436,8 @@ (click-card state :runner (get-ice state :hq 1)) (is (no-prompt? state :runner)) (is (= "Vanilla" (:title (get-ice state :rd 0))) "Vanilla swapped to R&D") - (is (= "Paper Wall" (:title (get-ice state :hq 1))) "Paper Wall swapped to HQ outer position"))) + (is (= "Paper Wall" (:title (get-ice state :hq 1))) "Paper Wall swapped to HQ outer position") + (is (last-log-contains? state #"uses Reshape to swap the positions of ICE protecting R&D at position 0 and ICE protecting HQ at position 1")))) (deftest retrieval-run-happy-path ;; Happy Path @@ -6550,7 +6546,8 @@ (is (= "Easy Mark" (-> (get-runner) :hand first :title))) (is (no-prompt? state :corp) "Corp should have no more prompts") (is (no-prompt? state :runner) "Runner should have no more prompts") - (is (nil? (get-run)) "Run is ended"))) + (is (nil? (get-run)) "Run is ended") + (is (second-last-log-contains? state #"uses Rip Deal to add Easy Mark from the heap to the grip")))) (deftest rip-deal-with-gauntlet-2942 ;; with Gauntlet #2942 @@ -6862,6 +6859,7 @@ (let [credits (:credit (get-runner))] (click-card state :runner "Corroder") (click-card state :runner "Mass-Driver") + (is (second-last-log-contains? state #"uses Scavenge to trash an installed Corroder and install Mass-Driver")) (is (= "Mass-Driver" (:title (get-program state 0))) "Mass-Driver is now installed") (is (= (+ credits 2 -8) (:credit (get-runner))) "Scavenge should give discount")))) @@ -6893,6 +6891,7 @@ (play-from-hand state :runner "Scrounge") (click-card state :runner "Ika") (click-card state :runner "Mayfly") + (is (last-log-contains? state #"uses Scrounge to add Mayfly in the heap to the bottom of the stack")) (is-deck? state :runner ["Rezeki" "Mayfly"]))) (deftest scrubbed @@ -6987,11 +6986,12 @@ (run-continue state :encounter-ice) (card-subroutine state :corp (get-ice state :remote1 0) 0) (click-prompt state :corp opt) - (if (= opt "The run does not end") - (do (is (no-prompt? state :runner) "no prompt") + (if (= opt "The run does not end") (do (is (no-prompt? state :runner) "no prompt") (is (no-prompt? state :corp) "no prompt") + (is (last-log-contains? state #"uses Shred to prevent the run from ending")) (is (:run @state) "still run")) - (is (not (:run @state)) "not run"))))) + (do (is (last-log-contains? state #"reveals and trashes 3 random cards .* from HQ to satisfy Shred")) + (is (not (:run @state)) "not run")))))) (deftest singularity ;; Singularity - Run a remote; if successful, trash all contents at no cost @@ -7074,7 +7074,11 @@ (take-credits state :corp) (play-from-hand state :runner "Spear Phishing") (click-prompt state :runner "HQ") - (is (:run @state) "A run has been initiated"))) + (run-continue state :approach-ice) + (run-continue state :movement) + (is (last-n-log-contains? state 3 #"encounters Ice Wall protecting HQ at position 0")) + (is (last-n-log-contains? state 2 #"uses Spear Phishing to bypass Ice Wall protecting HQ at position 0")) + (is (last-n-log-contains? state 1 #"bypasses Ice Wall")))) (deftest spec-work ;; Spec Work @@ -7239,7 +7243,6 @@ (damage state :corp :meat 2) (is (waiting? state :corp) "Corp is waiting for the runner to pick a steelskin"))) - (deftest stimhack ;; Stimhack - Gain 9 temporary credits and take 1 brain damage after the run (do-game @@ -7305,6 +7308,7 @@ (is (= 2 (get-counters imp :virus)) "Imp has 2 counters after install") (play-from-hand state :runner "Surge") (click-card state :runner imp) + (is (last-log-contains? state "uses Surge to place 2 virus counters on Imp")) (is (= 4 (get-counters (refresh imp) :virus)) "Imp has 4 counters after surge")))) (deftest surge-don-t-fire-surge-if-target-is-not-a-virus @@ -7368,6 +7372,7 @@ (click-prompt state :corp "Discard 2 cards from HQ") (click-card state :corp (first (:hand (get-corp)))) (click-card state :corp (second (:hand (get-corp)))) + (is (last-log-contains? state "uses SYN Attack to force the Corp to discard 2 cards from HQ")) (is (= (+ hand -2) (count (:hand (get-corp)))) "Corp should discard 2 cards")))) (deftest system-outage @@ -7583,7 +7588,6 @@ (is (= 1 (-> (prompt-map :runner) :choices count)) "Runner has 1 choice") (is (= ["Stack"] (prompt-buttons :runner)) "Runner's only choice is Stack"))) - (deftest the-maker-s-eye (do-game (new-game {:corp {:deck [(qty "Quandary" 5)] @@ -7983,6 +7987,7 @@ (play-run-event state "Vamp" :hq) (click-prompt state :runner "Vamp") (click-prompt state :runner "8") + (is (second-last-log-contains? state "uses Vamp to force the Corp to lose 8 [Credit]")) (is (= 1 (count-tags state)) "Took 1 tag") (is (= 5 (:credit (get-runner))) "Paid 8 credits") (is (zero? (:credit (get-corp))) "Corp lost all 8 credits"))) diff --git a/test/clj/game/cards/hardware_test.clj b/test/clj/game/cards/hardware_test.clj index 6d10aa9000..ed9260dce4 100644 --- a/test/clj/game/cards/hardware_test.clj +++ b/test/clj/game/cards/hardware_test.clj @@ -1428,7 +1428,9 @@ (deftest buffer-drive-vs-annicam ;; The player may move one card trashed from the Grip by the Runner to the bottom of the Stack (do-game - (new-game {:runner {:hand ["Buffer Drive" "Aniccam" "Strike Fund"] + (new-game {:corp {:deck [(qty "Hedge Fund" 10)] + :hand ["Hedge Fund"]} + :runner {:hand ["Buffer Drive" "Aniccam" "Strike Fund"] :credits 19}}) (take-credits state :corp) (play-from-hand state :runner "Buffer Drive") @@ -2228,7 +2230,7 @@ (let [flip (get-hardware state 0)] (run-on state "HQ") (card-ability state :runner (get-hardware state 0) 0) - (is (= "Test Runner jacks out." (-> @state :log last :public :text))) + (is (last-log-contains? state "Test Runner jacks out.")) (is (nil? (refresh flip)) "Flip Switch has been trashed") (is (find-card "Flip Switch" (:discard (get-runner))))))) @@ -2573,13 +2575,13 @@ (click-card state :runner cache) (is (= 1 (count (:discard (get-runner)))) "Prevented 1 net damage") (is (= 2 (count (:hand (get-runner))))) - (is (last-log-contains? state "Runner trashes 1 installed card \\(Cache\\) to use Heartbeat to prevent 1 net 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) (click-prompt state :runner "Heartbeat") (click-card state :runner hbdown) (click-prompt state :runner "Pass priority") (is (= 4 (count (:discard (get-runner)))) "Prevented 1 of 3 net damage; used facedown card") - (is (true? (last-n-log-contains? state 1 "Runner trashes 1 installed card \\(a facedown card\\) to use Heartbeat to prevent 1 net 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 diff --git a/test/clj/game/cards/ice_test.clj b/test/clj/game/cards/ice_test.clj index 6c19bbb069..675193525d 100644 --- a/test/clj/game/cards/ice_test.clj +++ b/test/clj/game/cards/ice_test.clj @@ -1084,7 +1084,7 @@ (auto-pump-and-break state cor)) "Gained 2c from the runner breaking") (is (last-n-log-contains? state 2 "Corp uses Bailiff to gain 1 [Credits]")) - (is (true? (last-n-log-contains? state 3 "Corp uses Bailiff to gain 1 [Credits]"))))))) + (is (last-n-log-contains? state 3 "Corp uses Bailiff to gain 1 [Credits]")))))) (deftest ballista ;; Ballista @@ -3105,8 +3105,7 @@ (take-credits state :corp) (let [hand (:hand (get-corp)) gate (get-ice state :hq 0) - log-str (str "Corp uses Gatekeeper to reveal.+" - " from HQ, and shuffle them into R&D")] + log-str #"Corp uses Gatekeeper to reveal.+ from HQ, and shuffle them into R&D"] (run-on state "HQ") (rez state :corp gate) (run-continue state) @@ -3127,8 +3126,7 @@ (take-credits state :corp) (let [discard (:discard (get-corp)) gate (get-ice state :hq 0) - log-str (str "Corp uses Gatekeeper to reveal.+" - " from Archives, and shuffle them into R&D")] + log-str #"Corp uses Gatekeeper to reveal.+ from Archives, and shuffle them into R&D"] (run-on state "HQ") (rez state :corp gate) (run-continue state) @@ -3152,8 +3150,7 @@ (let [hand (:hand (get-corp)) discard (:discard (get-corp)) gate (get-ice state :hq 0) - log-str (str "Corp uses Gatekeeper to reveal.+ from HQ " - "and .+ from Archives, and shuffle them into R&D")] + log-str #"Corp uses Gatekeeper to reveal.+ from HQ and .+ from Archives, and shuffle them into R&D"] (run-on state "HQ") (rez state :corp gate) (run-continue state) @@ -3272,7 +3269,7 @@ "Paid 2c + 2c for breaking") (is (last-n-log-contains? state 2 "Corp uses Gold Farmer to force the runner to lose 1 [Credits] for breaking printed subs") "Correct messages") - (is (true? (last-n-log-contains? state 3 "Corp uses Gold Farmer to force the runner to lose 1 [Credits] for breaking printed subs")) "Also correct messages")))) + (is (last-n-log-contains? state 3 "Corp uses Gold Farmer to force the runner to lose 1 [Credits] for breaking printed subs") "Also correct messages")))) (deftest gold-farmer-interaction-with-paperclip ;; Interaction with Paperclip @@ -7557,7 +7554,7 @@ (let [sm (get-ice state :hq 0)] (rez state :corp sm)) (run-continue state) - (is (last-log-contains? state "Corp uses Slot Machine to put the top card of the stack to the bottom, then reveal Sure Gamble \\(Event\\), Sure Gamble \\(Event\\), and Sure Gamble \\(Event\\) from the top of the stack.") "3 top cards revelaed"))) + (is (last-log-contains? state "Corp uses Slot Machine to put the top card of the stack to the bottom, then reveal Sure Gamble (Event), Sure Gamble (Event), and Sure Gamble (Event) from the top of the stack.") "3 top cards revelaed"))) (deftest slot-machine-subroutines ;; Subroutines @@ -8407,6 +8404,16 @@ (click-prompts state :corp "Ice Wall" "Guard" "I want to start over" "Ice Wall" "Cancel" "OK") (is-hand? state :corp ["Ice Wall"]))) +(deftest tollbooth + (do-game + (new-game {:corp {:deck [(qty "Hedge Fund" 5)] + :hand ["Tollbooth"] + :credits 20}}) + (play-from-hand state :corp "Tollbooth" "HQ") + (rez state :corp (get-ice state :hq 0)) + (take-credits state :corp) + (run-on state "HQ") + (run-continue state :encounter-ice))) (deftest tour-guide-rez-before-other-assets ;; Rez before other assets diff --git a/test/clj/game/cards/programs_test.clj b/test/clj/game/cards/programs_test.clj index 268d024711..b8a416faa9 100644 --- a/test/clj/game/cards/programs_test.clj +++ b/test/clj/game/cards/programs_test.clj @@ -5925,14 +5925,14 @@ (run-on state "HQ") (run-continue state) (auto-pump-and-break state (refresh maven)) - (is (second-last-log-contains? state "Runner pays 4 [Credits] to use Maven to break all 2 subroutines on Border Control.") "Correct log with autopump ability") + (is (second-last-log-contains? state "Runner pays 4 [Credits] to use Maven to break all 2 subroutines on Border Control") "Correct log with autopump ability") (core/continue state :corp nil) (run-jack-out state) (run-on state "HQ") (run-continue state) (card-ability state :runner (refresh maven) 0) (click-prompt state :runner "End the run") - (is (last-log-contains? state "Runner pays 2 [Credits] to use Maven to break 1 subroutine on Border Control.") "Correct log with single sub break")))) + (is (last-log-contains? state "Runner pays 2 [Credits] to use Maven to break 1 subroutine on Border Control") "Correct log with single sub break")))) (deftest mayfly ;; Mayfly diff --git a/test/clj/game/cards/resources_test.clj b/test/clj/game/cards/resources_test.clj index 66b78af437..942d333800 100644 --- a/test/clj/game/cards/resources_test.clj +++ b/test/clj/game/cards/resources_test.clj @@ -270,7 +270,7 @@ (card-ability state :runner (get-resource state 0) 0) (click-prompt state :runner "Chatterjee University") (click-card state :runner "Vanity Project") - (is (last-log-contains? state "forfeits 1 agenda .* to use Artist Colony to install Chatterjee University")) + (is (last-log-contains? state #"forfeits 1 agenda .* to use Artist Colony to install Chatterjee University")) (is (= "Chatterjee University" (:title (get-resource state 1)))) (is (empty? (:scored (get-runner)))))) @@ -1525,7 +1525,7 @@ (take-credits state :corp) (play-from-hand state :runner "Dadiana Chacon") (play-from-hand state :runner "Corroder") - (is (true? (last-n-log-contains? state 3 "Runner spends [Click] and pays 0 [Credits] to install Dadiana Chacon."))) + (is (last-n-log-contains? state 3 "Runner spends [Click] and pays 0 [Credits] to install Dadiana Chacon.")) (is (last-n-log-contains? state 2 "Runner uses Dadiana Chacon to suffer 3 meat damage.")) (is (second-last-log-contains? state "Runner trashes Corroder, Corroder, and Corroder due to meat damage.")) (is (last-log-contains? state "Runner spends [Click] and pays 2 [Credits] to install Corroder.")))) @@ -4594,7 +4594,7 @@ (take-credits state :runner) (is (changed? [(count (:hand (get-runner))) -1] (click-prompt state :runner "Trash a random card from the grip") - (is (last-log-contains? state "Runner trashes 1 card \\(Sure Gamble\\) randomly from the grip to satisfy Mystic Maemi."))) + (is (last-log-contains? state "randomly trashes 1 card (Sure Gamble) from the grip to satisfy Mystic Maemi."))) "Trashed one card from grip") (take-credits state :corp) (play-from-hand state :runner "Sure Gamble") @@ -5400,7 +5400,7 @@ (is (changed? [(:credit (get-runner)) -4] (click-card state :runner mo)) "Pay 4 for MOpus install (1+5-2)") - (is (last-log-contains? state "Runner pays 1 [Credits], and then pays 3 [Credits], to use Paule's Café to install hosted Magnum Opus.") "Correct message for Mopus install") + (is (last-log-contains? state "Runner pays 1 [Credits], and then pays 3 [Credits], to use Paule's Café to install hosted Magnum Opus") "Correct message for Mopus install") (card-ability state :runner pau 1) (is (changed? [(:credit (get-runner)) -4] (click-card state :runner des)) diff --git a/test/clj/game/cards/upgrades_test.clj b/test/clj/game/cards/upgrades_test.clj index 3985f8b7f8..500df97f3e 100644 --- a/test/clj/game/cards/upgrades_test.clj +++ b/test/clj/game/cards/upgrades_test.clj @@ -637,16 +637,16 @@ (rez state :corp van1) (click-prompt state :corp "Yes") (is (changed? [(get-strength (refresh van1)) 3] - (click-card state :corp van2)) - "Vanilla protecting Server 1 got +3 strength") + (click-card state :corp van2)) + "Vanilla protecting Server 1 got +3 strength") + (is (last-log-contains? state #"derez Vanilla protecting HQ.*give \+3 strength to Vanilla protecting Server 1")) (is (not (rezzed? (refresh van2))) "Vanilla on HQ was derezzed") (run-continue state) (rez state :corp van2) (is (no-prompt? state :corp) "No prompt when rezzing ice protecting other servers") (run-continue state) (rez state :corp rime) - (is (no-prompt? state :corp) "Brasília Government Grid ability is once per turn") - ))) + (is (no-prompt? state :corp) "Brasília Government Grid ability is once per turn")))) (deftest buneaker-bay-grid ;; Breaker Bay Grid - Reduce rez cost of other cards in this server by 5 credits @@ -708,7 +708,9 @@ (deftest stinson-ignores-all-costs (do-game - (new-game {:corp {:hand ["Bryan Stinson"] :discard ["Ultraviolet Clearance"]}}) + (new-game {:corp {:deck [(qty "Hedge Fund" 10)] + :hand ["Bryan Stinson"] + :discard ["Ultraviolet Clearance"]}}) (play-from-hand state :corp "Bryan Stinson" "HQ") (rez state :corp (get-content state :hq 0)) (is (changed? [(:click (get-corp)) -1 @@ -2378,7 +2380,8 @@ (deftest la-costa-grid-the-corp-may-advance-hosted-cards-in-la-costa-grid-s-server ;; The Corp may advance hosted cards in La Costa Grid's server (do-game - (new-game {:corp {:hand ["La Costa Grid", "Full Immersion RecStudio", "Project Beale"]}}) + (new-game {:corp {:deck [(qty "Hedge Fund" 5)] + :hand ["La Costa Grid" "Full Immersion RecStudio" "Project Beale"]}}) (play-from-hand state :corp "La Costa Grid" "New remote") (play-from-hand state :corp "Full Immersion RecStudio" "Server 1") (let [[la-costa recstudio] (get-content state :remote1)] @@ -2391,6 +2394,7 @@ (take-credits state :runner) (end-phase-12 state :corp) (click-card state :corp beale) + (is (second-last-log-contains? state "place 1 advancement counter on hosted card in Server 1")) (is (= 1 (get-counters (refresh beale) :advancement)) "Clicking on a hosted card in the La Costa Grid server advances it"))))) (deftest la-costa-grid-properly-async-5049 diff --git a/test/clj/game/core/async_test.clj b/test/clj/game/core/async_test.clj index a8706e719d..3f95f5ed7a 100644 --- a/test/clj/game/core/async_test.clj +++ b/test/clj/game/core/async_test.clj @@ -105,7 +105,7 @@ (def safe-fns "functions which probably contain an eid, but do not complete it" - #{"can-pay?" "cost-value" "recurring-fn"}) + #{"can-pay?" "cost-value" "x-cost-value" "recurring-fn"}) (defn- contains-eid? [chunk depth] diff --git a/test/clj/game/core/costs_test.clj b/test/clj/game/core/costs_test.clj index 717164a475..e7561cc7f1 100644 --- a/test/clj/game/core/costs_test.clj +++ b/test/clj/game/core/costs_test.clj @@ -49,40 +49,56 @@ (testing "Testing several cost messages" (do-game (new-game {:runner {:hand ["Diesel" "Daily Casts" "Clot" "Career Fair" "Daily Casts" "Sure Gamble" "Misdirection"] :deck [(qty "Ika" 15)]} - :corp {:hand [(qty "Ice Wall" 2) "Turtlebacks" "Beanstalk Royalties" "Hedge Fund" "Project Beale" "Ben Musashi"]}}) + :corp {:hand [(qty "Ice Wall" 2) + "Turtlebacks" "Beanstalk Royalties" "Hedge Fund" "Project Beale" "Ben Musashi"]}}) (core/gain state :corp :click 10) (play-from-hand state :corp "Ice Wall" "HQ") - (is (last-log-contains? state "Corp spends [Click] and pays 0 [Credits] to install ice protecting HQ.") "Install ice, zero cost") + (is (last-log-contains? state "Corp spends [Click] and pays 0 [Credits] to install ice protecting HQ.") + "Install ice, zero cost") (play-from-hand state :corp "Ice Wall" "HQ") - (is (last-log-contains? state "Corp spends [Click] and pays 1 [Credits] to install ice protecting HQ.") "Install ice, one cost") + (is (last-log-contains? state "Corp spends [Click] and pays 1 [Credits] to install ice protecting HQ.") + "Install ice, one cost") (play-from-hand state :corp "Turtlebacks" "New remote") - (is (last-log-contains? state "Corp spends [Click] to install a card in the root of Server 1.") "Install asset, zero cost") + (is (last-log-contains? state "Corp spends [Click] to install a card in the root of Server 1") + "Install asset, zero cost") (play-from-hand state :corp "Ben Musashi" "Server 1") - (is (last-log-contains? state "Corp spends [Click] to install a card in the root of Server 1.") "Install upgrade, zero cost") + (is (last-log-contains? state "Corp spends [Click] to install a card in the root of Server 1.") + "Install upgrade, zero cost") (play-from-hand state :corp "Project Beale" "New remote") - (is (last-log-contains? state "Corp spends [Click] to install a card in the root of Server 2.") "Install agenda, zero cost") + (is (last-log-contains? state "Corp spends [Click] to install a card in the root of Server 2") + "Install agenda, zero cost") (play-from-hand state :corp "Beanstalk Royalties") - (is (second-last-log-contains? state "Corp spends [Click] and pays 0 [Credits] to play Beanstalk Royalties.") "Play operation, zero cost") + (is (second-last-log-contains? state "Corp spends [Click] and pays 0 [Credits] to play Beanstalk Royalties.") + "Play operation, zero cost") (play-from-hand state :corp "Hedge Fund") - (is (second-last-log-contains? state "Corp spends [Click] and pays 5 [Credits] to play Hedge Fund.") "Play operation, five cost") + (is (second-last-log-contains? state "Corp spends [Click] and pays 5 [Credits] to play Hedge Fund.") + "Play operation, five cost") (take-credits state :corp) (core/gain state :runner :click 10) (play-from-hand state :runner "Diesel") - (is (second-last-log-contains? state "Runner spends [Click] and pays 0 [Credits] to play Diesel.") "Play event, zero cost") + (is (second-last-log-contains? state "Runner spends [Click] and pays 0 [Credits] to play Diesel.") + "Play event, zero cost") (play-from-hand state :runner "Sure Gamble") - (is (second-last-log-contains? state "Runner spends [Click] and pays 5 [Credits] to play Sure Gamble.") "Play event, five cost") + (is (second-last-log-contains? state "Runner spends [Click] and pays 5 [Credits] to play Sure Gamble.") + "Play event, five cost") (play-from-hand state :runner "Clot") - (is (last-log-contains? state "Runner spends [Click] and pays 2 [Credits] to install Clot.") "Install program, two cost") + (is (last-log-contains? state "Runner spends [Click] and pays 2 [Credits] to install Clot.") + "Install program, two cost") (play-from-hand state :runner "Misdirection") - (is (last-log-contains? state "Runner spends [Click] and pays 0 [Credits] to install Misdirection.") "Install program, zero cost") + (is (last-log-contains? state "Runner spends [Click] and pays 0 [Credits] to install Misdirection.") + "Install program, zero cost") (play-from-hand state :runner "Career Fair") - (is (last-log-contains? state "Runner spends [Click] and pays 0 [Credits] to play Career Fair.") "Play Career Fair, zero cost") + (is (last-log-contains? state "Runner spends [Click] and pays 0 [Credits] to play Career Fair.") + "Play Career Fair, zero cost") (click-card state :runner (find-card "Daily Casts" (:hand (get-runner)))) - (is (last-log-contains? state "Runner pays 0 [Credits] to use Career Fair to install Daily Casts from the Grip") "Choose Daily cast, zero cost install") + (is (last-log-contains? state "Runner pays 0 [Credits] to use Career Fair to install Daily Casts from the Grip") + "Choose Daily cast, zero cost install") (play-from-hand state :runner "Daily Casts") - (is (last-log-contains? state "Runner spends [Click] and pays 3 [Credits] to install Daily Casts.") "Install resource, three cost") + (is (last-log-contains? state "Runner spends [Click] and pays 3 [Credits] to install Daily Casts.") + "Install resource, three cost") (run-on state :archives) - (is (last-n-log-contains? state 1 "Runner spends [Click] to make a run on Archives.") "Initiate run, zero cost"))) + (is (last-n-log-contains? state 1 "Runner spends [Click] to make a run on Archives.") + "Initiate run, zero cost"))) (testing "Issue #4295: Auto-pumping Icebreaker with pay-credits prompt" (do-game (new-game {:runner {:hand ["Corroder" "Net Mercur" "Cloak"]} @@ -97,12 +113,15 @@ (let [cre (:credit (get-runner)) cor (get-program state 0) clo (get-program state 1)] - (is (= 2 (get-strength (refresh cor))) "Corroder starts at 2 strength") + (is (= 2 (get-strength (refresh cor))) + "Corroder starts at 2 strength") (auto-pump state (refresh cor)) (click-card state :runner clo) (click-prompt state :runner "Place 1 [Credits] on Net Mercur") - (is (= 5 (get-strength (refresh cor))) "Corroder is at 5 strength") - (is (= (- cre 2) (:credit (get-runner))) "Spent 2 (+1 from Cloak) to pump"))))) + (is (= 5 (get-strength (refresh cor))) + "Corroder is at 5 strength") + (is (= (- cre 2) (:credit (get-runner))) + "Spent 2 (+1 from Cloak) to pump"))))) (deftest pump-and-break (testing "Basic test" @@ -118,11 +137,15 @@ hive (get-ice state :hq 0)] (rez state :corp hive) (run-continue state) - (is (= 2 (get-strength (refresh cor))) "Corroder starts at 2 strength") + (is (= 2 (get-strength (refresh cor))) + "Corroder starts at 2 strength") (auto-pump-and-break state (refresh cor)) - (is (= 3 (get-strength (refresh cor))) "Corroder now at 3 strength") - (is (empty? (remove :broken (:subroutines (refresh hive)))) "Hive is now fully broken") - (is (second-last-log-contains? state "Runner pays 6 [Credits] to increase the strength of Corroder to 3 and break all 5 subroutines on Hive.") "Should write correct pump & break price to log")))) + (is (= 3 (get-strength (refresh cor))) + "Corroder now at 3 strength") + (is (empty? (remove :broken (:subroutines (refresh hive)))) + "Hive is now fully broken") + (is (second-last-log-contains? state "Runner pays 6 [Credits] to increase the strength of Corroder to 3 and break all 5 subroutines on Hive.") + "Should write correct pump & break price to log")))) (testing "Auto-pump first" (do-game (new-game {:runner {:hand ["Corroder"]} @@ -137,11 +160,15 @@ (rez state :corp hive) (run-continue state) (auto-pump state (refresh cor)) - (is (= 3 (get-strength (refresh cor))) "Corroder now at 3 strength") - (is (last-log-contains? state "Runner pays 1 [Credits] to increase the strength of Corroder to 3.") "Should write correct pump price to log") + (is (= 3 (get-strength (refresh cor))) + "Corroder now at 3 strength") + (is (last-log-contains? state "Runner pays 1 [Credits] to increase the strength of Corroder to 3.") + "Should write correct pump price to log") (auto-pump-and-break state (refresh cor)) - (is (empty? (remove :broken (:subroutines (refresh hive)))) "Hive is now fully broken") - (is (second-last-log-contains? state "Runner pays 5 [Credits] to use Corroder to break all 5 subroutines on Hive.") "Should write correct break price to log")))) + (is (empty? (remove :broken (:subroutines (refresh hive)))) + "Hive is now fully broken") + (is (second-last-log-contains? state "Runner pays 5 [Credits] to use Corroder to break all 5 subroutines on Hive.") + "Should write correct break price to log")))) (testing "Inability to pay for auto-pump" (do-game (new-game {:runner {:hand ["Corroder"]} @@ -156,7 +183,8 @@ (rez state :corp hive) (run-continue state) (auto-pump state (refresh cor)) - (is (= 2 (get-strength (refresh cor))) "Corroder still at 2 strength")))) + (is (= 2 (get-strength (refresh cor))) + "Corroder still at 2 strength")))) (testing "Auto-pump with stealth" (do-game (new-game {:runner {:hand ["Houdini" (qty "Mantle" 2)]} @@ -175,12 +203,16 @@ (rez state :corp engine) (run-continue state) (auto-pump state (refresh hou)) - (is (str/includes? (:msg (prompt-map :runner)) "2 stealth") "The prompt tells us how many stealth credits we need") + (is (str/includes? (:msg (prompt-map :runner)) + "2 stealth") + "The prompt tells us how many stealth credits we need") (click-card state :runner mantle1) (click-card state :runner mantle2) - (is (= 10 (get-strength (refresh hou))) "Houdini is at 10 strength") + (is (= 10 (get-strength (refresh hou))) + "Houdini is at 10 strength") (auto-pump-and-break state (refresh hou)) - (is (empty? (remove :broken (:subroutines (refresh engine)))) "Engine is now fully broken")))) + (is (empty? (remove :broken (:subroutines (refresh engine)))) + "Engine is now fully broken")))) (testing "Auto-pump and break some subs manually first" (do-game (new-game {:runner {:hand ["Corroder"]} @@ -195,30 +227,33 @@ (rez state :corp hive) (run-continue state) (auto-pump state (refresh cor)) - (is (= 3 (get-strength (refresh cor))) "Corroder is now at 3 strength") + (is (= 3 (get-strength (refresh cor))) + "Corroder is now at 3 strength") (card-ability state :runner (refresh cor) 0) (click-prompt state :runner "End the run") (click-prompt state :runner "Done") - (is (= 4 (count (remove :broken (:subroutines (refresh hive))))) "Only broken 1 sub") + (is (= 4 (count (remove :broken (:subroutines (refresh hive))))) + "Only broken 1 sub") (auto-pump-and-break state (refresh cor)) - (is (empty? (remove :broken (:subroutines (refresh hive)))) "Hive is now fully broken") - (is (second-last-log-contains? state "Runner pays 4 [Credits] to use Corroder to break the remaining 4 subroutines on Hive.") "Should write correct price to log"))))) + (is (empty? (remove :broken (:subroutines (refresh hive)))) + "Hive is now fully broken") + (is (second-last-log-contains? state "Runner pays 4 [Credits] to use Corroder to break the remaining 4 subroutines on Hive.") + "Should write correct price to log"))))) (deftest run-additional-costs (testing "If runner cannot pay additional cost, server not shown as an option for run events or click to run button" (do-game - (new-game {:corp {:deck ["Ruhr Valley"]} - :runner {:deck ["Dirty Laundry"]}}) - (play-from-hand state :corp "Ruhr Valley" "HQ") - (take-credits state :corp) - (let [ruhr (get-content state :hq 0)] - (rez state :corp ruhr) - (core/gain state :runner :click -3) - (is (= 1 (:click (get-runner)))) - (play-from-hand state :runner "Dirty Laundry") - (is (= 2 (-> (get-runner) :prompt first :choices count)) "Runner should only get choice of Archives or R&D") - (is (not (some #{"HQ"} (-> (get-runner) :prompt first :choices))) - "Runner should only get choice of Archives or R&D"))))) + (new-game {:corp {:deck ["Ruhr Valley"]} + :runner {:deck ["Dirty Laundry"]}}) + (play-from-hand state :corp "Ruhr Valley" "HQ") + (take-credits state :corp) + (let [ruhr (get-content state :hq 0)] + (rez state :corp ruhr) + (core/gain state :runner :click -3) + (is (= 1 (:click (get-runner)))) + (play-from-hand state :runner "Dirty Laundry") + (is (= #{"R&D" "Archives"} (->> (get-runner) :prompt first :choices (map :value) (set))) + "Runner should only get choice of Archives or R&D"))))) (deftest expend-costs-reveal-the-discarded-card (testing "Expend abilities reveal the discarded card" @@ -230,4 +265,5 @@ agri (first (:hand (get-corp)))] (expend state :corp agri) (click-card state :corp iw) - (is (= 5 (:credit (get-corp))) "Expend triggered Hyoubu"))))) + (is (= 5 (:credit (get-corp))) + "Expend triggered Hyoubu"))))) diff --git a/test/clj/game/core/engine_test.clj b/test/clj/game/core/engine_test.clj index d30d3b8aa8..f51c9adaab 100644 --- a/test/clj/game/core/engine_test.clj +++ b/test/clj/game/core/engine_test.clj @@ -64,51 +64,66 @@ (deftest merge-costs-paid (let [eid1 {:cost-paid {:click {:paid/type :click + :paid/side :corp :paid/value 3} :credit {:paid/type :credit + :paid/side :corp :paid/value 1} :forfeit {:paid/type :forfeit + :paid/side :corp :paid/targets [{:title "NAPD Contract"}] :paid/value 1}}} eid2 {:cost-paid {:click {:paid/type :click + :paid/side :corp :paid/value 1}}} eid3 {:cost-paid {:trash {:paid/type :trash + :paid/side :corp :paid/value 1 :paid/targets [{:title "C.I. Fund"}]}}}] (is (= {:click {:paid/type :click + :paid/side :corp :paid/value 3} :credit {:paid/type :credit + :paid/side :corp :paid/value 1} :forfeit {:paid/type :forfeit + :paid/side :corp :paid/targets [{:title "NAPD Contract"}] :paid/value 1}} (e/merge-costs-paid (:cost-paid eid1)))) (is (= {:click {:paid/type :click + :paid/side :corp :paid/targets nil :paid/x-value 0 :paid/value 4} :credit {:paid/type :credit + :paid/side :corp :paid/targets nil :paid/x-value 0 :paid/value 1} :forfeit {:paid/type :forfeit + :paid/side :corp :paid/targets [{:title "NAPD Contract"}] :paid/x-value 0 :paid/value 1}} (e/merge-costs-paid (:cost-paid eid1) (:cost-paid eid2)))) (is (= {:click {:paid/type :click + :paid/side :corp :paid/targets nil :paid/x-value 0 :paid/value 4} :credit {:paid/type :credit + :paid/side :corp :paid/targets nil :paid/x-value 0 :paid/value 1} :forfeit {:paid/type :forfeit + :paid/side :corp :paid/targets [{:title "NAPD Contract"}] :paid/x-value 0 :paid/value 1} :trash {:paid/type :trash + :paid/side :corp :paid/targets [{:title "C.I. Fund"}] :paid/x-value 0 :paid/value 1}} diff --git a/test/clj/game/core/flags_test.clj b/test/clj/game/core/flags_test.clj index 0cc6095e7e..9a81aca94a 100644 --- a/test/clj/game/core/flags_test.clj +++ b/test/clj/game/core/flags_test.clj @@ -3,12 +3,13 @@ [clojure.test :refer :all] [game.core :as core] [game.core.card :refer :all] + [game.core.card-defs :refer [defcard-impl]] [game.core.flags :as flags] [game.macros :refer [effect]] [game.test-framework :refer :all])) (deftest can-score?-test - (defmethod core/defcard-impl "Test Card" [_] {}) + (defmethod defcard-impl "Test Card" [_] {}) (before-each [state (new-game) test-card {:title "Test Card" :type "Agenda" :advancementcost 2} test-card-with-counters (assoc test-card :advance-counter 2)] @@ -20,8 +21,8 @@ (is (not (flags/can-score? state :corp test-card))) (is (flags/can-score? state :corp test-card-with-counters))) (testing "can-core-req passes in state" - (defmethod core/defcard-impl "Test Card" [_] + (defmethod defcard-impl "Test Card" [_] {:flags {:can-score (effect (= (:title card) "Different Card"))}}) (is (not (flags/can-score? state :corp test-card-with-counters))) (is (flags/can-score? state :corp (assoc test-card-with-counters :title "Different Card"))))) - (remove-method core/defcard-impl "Test Card")) + (remove-method defcard-impl "Test Card")) diff --git a/test/clj/game/core/rules_test.clj b/test/clj/game/core/rules_test.clj index fd79fc4dfe..53f94eac85 100644 --- a/test/clj/game/core/rules_test.clj +++ b/test/clj/game/core/rules_test.clj @@ -84,7 +84,7 @@ (is (= "Kati Jones" (:title (get-resource state 1)))) (is (not (find-card "Kati Jones" (:hosted (get-resource state 0))))) (is (= "Kati Jones" (:title (get-discarded state :runner)))) - (is (last-log-contains? state "Kati Jones hosted on .* is trashed.")))) + (is (last-log-contains? state #"Kati Jones hosted on .* is trashed.")))) (testing "Should trash the loaded kati on OCA" (do-game (new-game {:runner {:hand [(qty "Kati Jones" 2) "Off-Campus Apartment"] @@ -97,7 +97,7 @@ (is (= 0 (get-counters (first (:hosted (get-resource state 0))) :credit)) "Correct kati trash") (is (find-card "Kati Jones" (:hosted (get-resource state 0)))) (is (= "Kati Jones" (:title (get-discarded state :runner)))) - (is (last-log-contains? state "Kati Jones hosted on .* is trashed."))))) + (is (last-log-contains? state #"Kati Jones hosted on .* is trashed."))))) (deftest installing-second-hivemind-trashes-hosted-hivemind-test (do-game @@ -343,18 +343,16 @@ (click-card state :runner "Corroder") (play-from-hand state :runner "Leprechaun") (click-prompt state :runner (:title od)) - (let [od (refresh od) - le (first (:hosted od))] - (play-from-hand state :runner "Mimic") - (click-prompt state :runner "Leprechaun") - (let [all-installed (core/all-installed state :runner)] - (is (= 5 (count all-installed)) "Number of installed runner cards is correct") - (is (not-empty (filter #(= (:title %) "Leprechaun") all-installed)) "Leprechaun is in all-installed") - (is (not-empty (filter #(= (:title %) "Personal Workshop") all-installed)) "Personal Workshop is in all-installed") - (is (not-empty (filter #(= (:title %) "Mimic") all-installed)) "Mimic is in all-installed") - (is (not-empty (filter #(= (:title %) "Omni-drive") all-installed)) "Omni-drive is in all-installed") - (is (not-empty (filter #(= (:title %) "Knight") all-installed)) "Knight is in all-installed") - (is (empty? (filter #(= (:title %) "Corroder") all-installed)) "Corroder is not in all-installed"))))))) + (play-from-hand state :runner "Mimic") + (click-prompt state :runner "Leprechaun") + (let [all-installed (core/all-installed state :runner)] + (is (= 5 (count all-installed)) "Number of installed runner cards is correct") + (is (not-empty (filter #(= (:title %) "Leprechaun") all-installed)) "Leprechaun is in all-installed") + (is (not-empty (filter #(= (:title %) "Personal Workshop") all-installed)) "Personal Workshop is in all-installed") + (is (not-empty (filter #(= (:title %) "Mimic") all-installed)) "Mimic is in all-installed") + (is (not-empty (filter #(= (:title %) "Omni-drive") all-installed)) "Omni-drive is in all-installed") + (is (not-empty (filter #(= (:title %) "Knight") all-installed)) "Knight is in all-installed") + (is (empty? (filter #(= (:title %) "Corroder") all-installed)) "Corroder is not in all-installed")))))) (deftest log-accessed-names ;; Check that accessed card names are logged - except those on R&D, and no logs on archives @@ -442,7 +440,8 @@ (deftest purge-corp ;; Purge virus counters on Corp cards (do-game - (new-game {:corp {:deck ["Cyberdex Trial" "Ice Wall"]}}) + (new-game {:corp {:deck [(qty "Hedge Fund" 10)] + :hand ["Cyberdex Trial" "Ice Wall"]}}) (play-from-hand state :corp "Ice Wall" "HQ") (let [iw (get-ice state :hq 0)] (core/command-counter state :corp ["virus" 2]) diff --git a/test/clj/game/core/say_test.clj b/test/clj/game/core/say_test.clj index 55f41598b7..7358650bb0 100644 --- a/test/clj/game/core/say_test.clj +++ b/test/clj/game/core/say_test.clj @@ -4,8 +4,9 @@ [clojure.repl :as repl] [game.core :as core] [game.core.card :refer :all] - [game.core.commands :refer [parse-command]] + [game.core.commands] [game.core.mark :refer :all] + [game.core.say :as sut] [game.test-framework :refer :all] [jinteki.utils :refer [command-info]])) @@ -208,22 +209,22 @@ (is (= 1000 (get-link state)) "runner has 1000 link"))))) (testing "/memory" - (let [user {:username "Runner"}] - (do-game - (new-game) - (core/command-parser state :runner {:user user :text "/memory 3"}) - (is (= 3 (:used (:memory (get-runner)))) "runner has 3 memory") - (core/command-parser state :runner {:user user :text "/memory -5"}) - (is (= -5 (:used (:memory (get-runner)))) "runner has -5 memory") - (core/command-parser state :runner {:user user :text "/memory 99999999999999999999999999999999999999999999"}) - (is (= 1000 (:used (:memory (get-runner)))) "runner has 1000 memory")))) + (let [user {:username "Runner"}] + (do-game + (new-game) + (core/command-parser state :runner {:user user :text "/memory 3"}) + (is (= 3 (:used (:memory (get-runner)))) "runner has 3 memory") + (core/command-parser state :runner {:user user :text "/memory -5"}) + (is (= -5 (:used (:memory (get-runner)))) "runner has -5 memory") + (core/command-parser state :runner {:user user :text "/memory 99999999999999999999999999999999999999999999"}) + (is (= 1000 (:used (:memory (get-runner)))) "runner has 1000 memory")))) (testing "/mark" - (let [user {:username "Runner"}] - (do-game - (new-game) - (core/command-parser state :runner {:user user :text "/mark"}) - (is (some? (:mark @state)) "Mark identified")))) + (let [user {:username "Runner"}] + (do-game + (new-game) + (core/command-parser state :runner {:user user :text "/mark"}) + (is (some? (:mark @state)) "Mark identified")))) (testing "/roll" (let [user {:username "Corp"}] @@ -351,7 +352,7 @@ (let [r {:username "Runner"}] (do-game (new-game {:corp {:hand ["Hedge Fund"]}}) - (dotimes [n 3] + (dotimes [_ 3] (click-credit state :corp)) (end-turn state :corp) (start-turn state :runner) @@ -372,7 +373,7 @@ c {:username "Corp"}] (do-game (new-game {:corp {:hand ["Hedge Fund"]}}) - (dotimes [n 3] + (dotimes [_ 3] (click-credit state :corp)) (end-turn state :corp) (start-turn state :runner) @@ -400,7 +401,7 @@ (is (last-log-contains? state "[!]Runner uses a command: /unique") "Correct message") (click-card state :runner wnp1) (is (not (unique? (refresh wnp1))) "WNP is not unique anymore") - (is (last-log-contains? state "Runner uses /unique command to make Wireless Net Pavilion not unique\\.") "Correct message") + (is (last-log-contains? state "Runner uses /unique command to make Wireless Net Pavilion not unique.") "Correct message") (play-from-hand state :runner "Wireless Net Pavilion") (is (zero? (count (:hand (get-runner)))) "Both cards have been installed") (is (= 2 (count (get-resource state)))) diff --git a/test/clj/game/core/set_up_test.clj b/test/clj/game/core/set_up_test.clj index 57273f886d..5d8dc0d376 100644 --- a/test/clj/game/core/set_up_test.clj +++ b/test/clj/game/core/set_up_test.clj @@ -16,11 +16,11 @@ (let [corp-hand (:hand (get-corp))] (click-prompt state :corp "Keep") (is (= corp-hand (:hand (get-corp)))) - (is (last-log-contains? state "Corp keeps their hand"))) + (is (last-log-contains? state "Corp keeps [their] hand"))) (let [runner-hand (:hand (get-runner))] (click-prompt state :runner "Keep") (is (= runner-hand (:hand (get-runner)))) - (is (last-log-contains? state "Runner keeps their hand"))))) + (is (last-log-contains? state "Runner keeps [their] hand"))))) (testing "mulligan" (do-game (new-game setup) diff --git a/test/clj/game/test_framework.clj b/test/clj/game/test_framework.clj index fcb8dfd468..a9133c4c9c 100644 --- a/test/clj/game/test_framework.clj +++ b/test/clj/game/test_framework.clj @@ -19,8 +19,11 @@ [game.utils :as utils] [game.utils-test :refer [error-wrapper is']] [jinteki.cards :refer [all-cards]] + [jinteki.i18n :refer [build-msg load-dictionary!]] [jinteki.utils :as jutils] - [web.game :refer [handle-message-and-send-diffs! update-and-send-diffs!]])) + [web.game :refer [handle-message-and-send-diffs! update-and-send-diffs!]]) + (:import + (java.util.regex Pattern))) ;; Card information and definitions (defn load-cards [] @@ -30,6 +33,7 @@ merge)) (defn load-all-cards [] + (load-dictionary! "public/i18n") (when (empty? @all-cards) (->> (load-cards) (map (juxt :title identity)) @@ -73,15 +77,15 @@ (test-var #'game.core.scenarios-test/masterwork-overinstall-boomerang-complex-case-full-game))) #_(prof/profile (clojure.test/run-all-tests)) (prof/serve-ui 8080)) - ;;; action-wrapper to better mimic real-world usage + (defn do-action [command state side args] (update-and-send-diffs! main/handle-action {:state state} side command args)) - ;; generic test helpers + (defn is-zone-impl "Is the zone exactly equal to a given set of cards?" [state side zone expected] @@ -184,8 +188,10 @@ (let [parsed-number (Integer/parseInt choice)] (when-not (do-action "choice" state side {:choice parsed-number :eid (:eid (get-prompt state side))}) (is' (not true) (str "Parsed number " parsed-number " is incorrect somehow")))) - (catch Exception _ - (is' (number? (Integer/parseInt choice)) (expect-type "number string" choice)))) + (catch Exception ex + (if (number? (Integer/parseInt choice)) + (throw ex) + (is' (number? (Integer/parseInt choice)) (expect-type "number string" choice))))) (= :trace (:prompt-type prompt)) (try @@ -196,9 +202,11 @@ (is' (<= int-choice (:choices prompt)) (str (utils/side-str side) " expected to pay [ " int-choice " ] to trace but couldn't afford it.")))) - (catch Exception _ - (is' (number? (Integer/parseInt choice)) - (expect-type "number string" choice)))) + (catch Exception ex + (if (number? (Integer/parseInt choice)) + (throw ex) + (is' (number? (Integer/parseInt choice)) + (expect-type "number string" choice))))) ;; List of card titles for auto-completion (:card-title choices) @@ -675,10 +683,10 @@ (ensure-no-prompts state) (is' (some? card) (str title "is in hand")) (if-not (some? card) - (do (let [other-side (if (= side :runner) :corp :runner)] - (when (some? (find-card title (get-in @state [other-side :hand]))) - (println title " was instead found in the opposing hand - was the wrong side used?"))) - true) + (let [other-side (if (= side :runner) :corp :runner)] + (when (some? (find-card title (get-in @state [other-side :hand]))) + (println title " was instead found in the opposing hand - was the wrong side used?")) + true) (when (do-action "play" state side {:card card}) (let [choice-sets (split-on-keywords choices)] (doseq [cs choice-sets] @@ -1096,15 +1104,45 @@ [state base] (handle-message-and-send-diffs! {:state state} :corp {} (str "/trace " base))) +(defn get-msg-text + [m] + (if (string? m) m (or (:raw-text m) (build-msg m)))) + +(defn escape-log-string [s] + (if (string? s) (Pattern/quote s) s)) + +(defn side-log + [side log] + (into [] (keep #(or (side %) (:public %)) log))) + +(defn last-log-contains? + ([state content] (last-log-contains? state content :public)) + ([state content side] + (->> (->> @state :log (side-log side) last :text get-msg-text) + (re-find (re-pattern (escape-log-string content)))))) + +(defn second-last-log-contains? + ([state content] (second-last-log-contains? state content :public)) + ([state content side] + (->> (->> @state :log (side-log side) butlast last :text get-msg-text) + (re-find (re-pattern (escape-log-string content)))))) + +(defn last-n-log-contains? + ([state n content] + (last-n-log-contains? state n content :public)) + ([state n content side] + (->> (-> @state :log reverse (->> (side-log side)) (nth n) :text get-msg-text) + (re-find (re-pattern (escape-log-string content)))))) + (defn log-str [state] (->> (:log @state) (keep :public) - (map :text) + (map (comp get-msg-text :text)) (str/join " "))) -#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} (defn print-logs [state] - (prn (log-str state))) + (prn (log-str state)) + (newline)) (defmacro do-game [s & body] `(let [~'state ~s @@ -1142,7 +1180,9 @@ ~'print-prompts (fn [] (print (~'prompt-fmt :corp)) (println (~'prompt-fmt :runner)))] - ~@body)) + (let [ret# (do ~@body)] + (log-str ~'state) + ret#))) (defmacro before-each [let-bindings & testing-blocks] @@ -1150,37 +1190,6 @@ (let [bundles (for [block testing-blocks] `(let [~@let-bindings] ~block))] `(do ~@bundles))) -(defn escape-log-string [s] - (str/escape s {\[ "\\[" \] "\\]"})) - -(defn- side-log - [side log] - (into [] (keep #(or (side %) (:public %)) log))) - -(defn last-log-contains? - ([state content] (last-log-contains? state content :public)) - ([state content side] - (->> (->> @state :log (side-log side) last :text) - (re-find (re-pattern (escape-log-string content))) - some?))) - -(defn second-last-log-contains? - ([state content] (second-last-log-contains? state content :public)) - ([state content side] - (->> (->> @state :log (side-log side) butlast last :text) - (re-find (re-pattern (escape-log-string content))) - some?))) - -(defn last-n-log-contains? - ([state n content] - (last-n-log-contains? state n content :public)) - ([state n content side] - (let [log (->> @state :log (side-log side) (mapv :text)) - index (- (count log) 1 n) - log-entry (nth log index "") - res (re-find (re-pattern (escape-log-string content)) log-entry)] - (some? res)))) - (defn- make-zone [zone replacement] (if (and zone (int? zone)) diff --git a/test/clj/game/test_framework/asserts.clj b/test/clj/game/test_framework/asserts.clj index c0cd02a0e4..32be79fd20 100644 --- a/test/clj/game/test_framework/asserts.clj +++ b/test/clj/game/test_framework/asserts.clj @@ -2,48 +2,62 @@ (:require [clojure.test :as t] [kaocha.hierarchy :as k.hierarchy] - [kaocha.output :as k.output] [kaocha.report :as k.report])) (defmethod t/assert-expr 'last-log-contains? [msg form] - `(let [state# ~(nth form 1) - content# ~(nth form 2) - log# (-> @state# :log last :text) - found# ~form] - (t/do-report - {:type (if found# :pass :fail) - :actual log# - :expected content# - :message ~msg}) - found#)) + (let [[_last-log-contains? state content side] form] + `(let [state# ~state + content# ~content + side# ~(or side :public) + log# (->> (:log @state#) + (game.test-framework/side-log side#) + last :text + game.test-framework/get-msg-text) + found# ~form] + (t/do-report + {:type (if found# :pass :fail) + :actual log# + :expected content# + :message ~msg}) + found#))) (defmethod t/assert-expr 'second-last-log-contains? [msg form] - `(let [state# ~(nth form 1) - content# ~(nth form 2) - log# (-> @state# :log butlast last :text) - found# ~form] - (t/do-report - {:type (if found# :pass :fail) - :actual log# - :expected content# - :message ~msg}) - found#)) + (let [[_second-last-log-contains? state content side] form] + `(let [state# ~state + content# ~content + side# ~(or side :public) + log# (->> (:log @state#) + (game.test-framework/side-log side#) + butlast last :text + game.test-framework/get-msg-text) + found# ~form] + (t/do-report + {:type (if found# :pass :fail) + :actual log# + :expected content# + :message ~msg}) + found#))) (defmethod t/assert-expr 'last-n-log-contains? [msg form] - `(let [state# ~(nth form 1) - n# ~(nth form 2) - content# ~(nth form 3) - log# (:text (nth (-> @state# :log reverse) n#)) - found# ~form] - (t/do-report - {:type (if found# :pass :fail) - :actual log# - :expected content# - :message ~msg}) - found#)) + (let [[_last-n-log-contains? state n content side] form] + `(let [state# ~state + n# ~n + content# ~content + side# ~(or side :public) + log# (-> @state# :log reverse + (->> (game.test-framework/side-log side#)) + (nth n#) :text + game.test-framework/get-msg-text) + found# ~form] + (t/do-report + {:type (if found# :pass :fail) + :actual log# + :expected content# + :message ~msg}) + found#))) (defmethod t/assert-expr 'prompt-is-type? [msg form]