Skip to content

Commit

Permalink
Fancier special actions (#104)
Browse files Browse the repository at this point in the history
* Support promoting Special Actions to being shown under Combat

This also adds support for using the spell-card widget to render
spell slot-consuming abilities, like Divine Smite. This lets us
take advantage of the existing support for choosing and using
appropriate spell slot levels, and rendering a `:dice` function—nice!

* Render number of remaining uses on cast button + improve style
  • Loading branch information
dhleong committed Mar 25, 2019
1 parent cb2f697 commit 6833033
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 62 deletions.
14 changes: 14 additions & 0 deletions docs/dnd5e/DnD Attrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,20 @@ Sneak Attack. Sneak Attack does not consume any time on its own, but is
used in conjunction with an action.
See [`:action`](#action) above for format and usage.

Instead of the normal `true` value, you can also provide special flags as
a map, for example `{:combat true}`, or, for a single flag, use the shortcut
of suppling the flag directly, eg:

```clojure
[:!provide-attr
[:special-action :paladin/divine-smite]
:combat]
```

Supported flags:

- `:combat` The action will be displayed on the "Combat" section of the "Actions" page. This is useful for things like Divine Smite or Sneak Attack that add to the damage dealt by a normal attack.

## `:spells`

Modify the attributes of a spell. Very commonly used by Warlock Eldritch Invocations, but might also be used by racial traits.
Expand Down
6 changes: 5 additions & 1 deletion resources/sources/dnd5e/classes/paladin.edn
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,13 @@ This feature has no effect on undead and constructs."
:desc "Starting at 2nd level, when you hit a creature with a melee weapon attack, you can expend one spell slot to deal radiant damage to the target, in addition to the weapon’s damage. The extra damage is 2d8 for a 1st-level spell slot, plus 1d8 for each spell level higher than 1st, to a maximum of 5d8. The damage increases by 1d8 if the target is an undead or a fiend."
:spell-level 1 ; min spell slot level
:consumes :*spell-slot
:damage :radiant
:dice (fn [spell-level]
(str (min 5 (+ 1 spell-level))
"d8"))
:! [[:!provide-attr
[:special-action :paladin/divine-smite]
true]]} ]}
:combat]]} ]}

3 {:+features
[{:id :paladin/divine-health
Expand Down
2 changes: 1 addition & 1 deletion resources/sources/dnd5e/classes/rogue.edn
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ The amount of the extra damage increases as you gain levels in this class, as sh
20 {:>>desc "\nAt 20th level, your sneak attack damage is **10d6**"}}
:! [[:!provide-attr
[:special-action :rogue/sneak-attack]
true]
:combat]
[:!provide-attr
[:combat-info :rogue/sneak-attack]
{:name "Sneak Attack Damage"
Expand Down
16 changes: 13 additions & 3 deletions src/cljs/wish/sheets/dnd5e.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,17 @@
[:h4 "Other Attacks"]
(for [a attacks]
^{:key (:id a)}
[attack-block a])])])
[attack-block a])])

(when-let [actions (seq (<sub [::subs/special-combat-actions]))]
[:div.special
[:h4 "Special Attack Actions"]
(for [a actions]
^{:key (:id a)}
[:div.clickable
{:on-click (click>evt [:toggle-overlay [#'overlays/info a]])}
(:name a)])])
])

(defn- action-block
[a]
Expand All @@ -404,7 +414,7 @@

(defn- actions-for-type [filter-type]
(let [spells (seq (<sub [::subs/prepared-spells-filtered filter-type]))
actions (seq (<sub [::subs/combat-actions filter-type]))]
actions (seq (<sub [::subs/actions-for-type filter-type]))]
(if (or spells actions)
[:<>
(when spells
Expand Down Expand Up @@ -610,7 +620,7 @@
level (or cast-level base-level)]
[expandable
[:div.spell
[cast-button s]
[cast-button {:nested? true} s]

[:div.spell-info
[:div.name (:name s)]
Expand Down
10 changes: 7 additions & 3 deletions src/cljs/wish/sheets/dnd5e/style.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@
:margin "0 8px 0 0"})
[:&.upcast {:position 'relative
:border (str "2px solid " color-accent2)}
[:&:hover {:background-color "#f0f0f0"
:color "#333"}]
[:&:hover>.upcast-level
{:background (color/lighten color-accent2 20)}]
[:.upcast-level {:position 'absolute
Expand All @@ -441,14 +443,16 @@
:bottom "-0.7em"
:font-size "0.5em"
:transform "translate(50%, 0)"}]]
[:.uses-remaining {:padding "0.1em"
:font-size "0.7em"
:font-style 'italic}]

[:&.button
[:&.disabled {:font-style 'italic
:color "rgba(1,1,1, 0.25) !important"
:cursor 'default} ]
[:&:hover {:background-color "#f0f0f0"
:color "#333"}
[:&.disabled {:background-color "#ccc"}]]])
[:&.nested:hover {:background-color "#f0f0f0"
:color "#333"}]])

(defstyled spell-card
{:max-width "300px"}
Expand Down
39 changes: 28 additions & 11 deletions src/cljs/wish/sheets/dnd5e/subs.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,8 +1081,14 @@

; ======= combat ==========================================

(def ^:private compile-dice-fn (memoize ->callable))

(defn- maybe-compile-dice [entity]
(when entity
(update entity :dice compile-dice-fn)))

(reg-sub
::combat-actions
::actions-for-type
:<- [:sheet-source]
:<- [:classes]
:<- [:races]
Expand All @@ -1092,19 +1098,30 @@
flatten
(mapcat
(fn [c]
(let [ids (keys (get-in c [:attrs filter-type]))]
(map
(fn [id]
(or (when (= id (:id c))
; attuned equipment, probably
c)
(get-in c [:features id])
(src/find-feature data-source id)))
ids))))
(map (fn [[id flags]]
(with-meta
(maybe-compile-dice
(or (when (= id (:id c))
; attuned equipment, probably
c)
(get-in c [:features id])
(src/find-feature data-source id)))
(cond
(map? flags) flags
(keyword? flags) {flags true}
:else nil)))
(get-in c [:attrs filter-type]))))
(keep identity)
(sort-by :name))))

(def ^:private compile-dice-fn (memoize ->callable))
(reg-sub
::special-combat-actions
:<- [::actions-for-type :special-action]
(fn [actions _]
(filter
#(:combat (meta %))
actions)))

(def ^:private compile-save-fn (memoize ->callable))
(reg-sub
::other-attacks
Expand Down
98 changes: 55 additions & 43 deletions src/cljs/wish/sheets/dnd5e/widgets.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,29 @@

(defn stringify-components
[{components :comp}]
(letfn [(vs-parts [k]
(case k
:v "V"
:s "S"
:vs "V, S"))
(m-part [p]
(str "M (" p ")"))]
(cond
(keyword? components)
(vs-parts components)

; are any material only?
(string? components)
(m-part components)

(= 1 (count components))
(m-part (first components))

:else
(str (vs-parts (first components))
", "
(m-part (second components))))))
(when components
(letfn [(vs-parts [k]
(case k
:v "V"
:s "S"
:vs "V, S"))
(m-part [p]
(str "M (" p ")"))]
(cond
(keyword? components)
(vs-parts components)

; are any material only?
(string? components)
(m-part components)

(= 1 (count components))
(m-part (first components))

:else
(str (vs-parts (first components))
", "
(m-part (second components)))))))

(defn- stringify-dam-type
[t]
Expand Down Expand Up @@ -111,21 +112,24 @@
"Renders a button to cast the given spell at its current level.
Renders a box with 'At Will' if the spell is a cantrip"
([s] (cast-button nil s))
([{:keys [base-level upcastable?]
([{:keys [base-level upcastable? nested?]
:or {upcastable? true}} s]
(let [cantrip? (= 0 (:spell-level s))
at-will? (or cantrip?
(:at-will? s))
base-level (or base-level
(:spell-level s))

use-id (:consumes s)
use-slot? (= (:consumes s) :*spell-slot)
use-id (when-not use-slot?
(:consumes s))

; if it's not at-will (or consumes a limited-use)
; try to figure out what slot we can use
; (the sub handles the check)
{slot-level :level
slot-kind :kind
slot-remain :unused
slot-total :total} (<sub [::subs/usable-slot-for s])

castable-level (if cantrip?
Expand All @@ -135,15 +139,23 @@
slot-level))
slot-level))

uses-left (when use-id
(:uses-left (<sub [::subs/limited-use use-id])))

has-uses? (or cantrip?
(if use-id
(when-let [{:keys [uses-left]} (<sub [::subs/limited-use use-id])]
(when uses-left
(> uses-left 0))

; normal spell; if there's a castable-level for it,
; we're good to go
(not (nil? castable-level))))

uses-remaining (when-not cantrip?
(if use-id
uses-left
slot-remain))

upcast? (when has-uses?
(> castable-level base-level))]

Expand All @@ -154,6 +166,8 @@

[:div.cast.button
{:class [styles/cast-spell
(when nested?
"nested")
(when-not has-uses?
"disabled")
(when upcast?
Expand All @@ -171,10 +185,14 @@
slot-kind slot-level slot-total])))))}

; div content:
(if use-id
(if (or use-id use-slot?)
"Use"
"Cast")

(when uses-remaining
[:div.uses-remaining
uses-remaining " left"])

(when upcast?
[:span.upcast-level
" @" (get data/level-suffixed castable-level)])
Expand Down Expand Up @@ -208,6 +226,12 @@
[:span.tag "R"])
]))

(defn- opt-row [s key-fn title]
(when-let [v (key-fn s)]
[:tr
[:td.header title]
[:td v]]))

(defn- the-spell-card
[{:keys [update-level! base-level]}
{:keys [spell-level] :as s}]
Expand All @@ -224,29 +248,17 @@
[:div styles/spell-card
[:table.info
[:tbody
[:tr
[:th.header "Casting Time"]
[:td (:time s)]]
[:tr
[:th.header "Range"]
[:td (:range s)]]
(opt-row s :time "Casting Time")
(opt-row s :range "Range")

(when-let [aoe (:aoe s)]
[:tr
[:th.header "Area of Effect"]
[:td [spell-aoe aoe]]])

[:tr
[:th.header "Components"]
[:td (stringify-components s)]]
[:tr
[:th.header "Duration"]
[:td (:duration s)]]

(when-let [tags (spell-tags s)]
[:tr
[:th.header "Properties"]
[:td tags]])
(opt-row s stringify-components "Components")
(opt-row s :duration "Duration")
(opt-row s spell-tags "Properties")

(when-let [dice (:dice s)]
(let [{:keys [damage]} s
Expand Down

0 comments on commit 6833033

Please sign in to comment.