More flexible input for adjusting the health/spirit stats

I wanted to be able to enter a +/- value, but also set it to an
absolute value with the = prefix.

While I was at it, I fixed up the UI that I've previously changed.
This commit is contained in:
Howard Abrams 2022-02-27 22:05:35 -08:00
parent 6cc5d8eda7
commit e052c6a1ec
5 changed files with 240 additions and 65 deletions

View file

@ -48,7 +48,7 @@ What I do, is add the following "code" somewhere in my Ironsworn-specific org fi
# End:
#+END_SRC
Finally, define your character. While I describe the details later, hit ~M-x~ and type: =rpgdm-ironsworn-new-character= and answer the questions. This will create the necessary formatting and you are ready to play with either your key-binding (or ~F6~) to bring up the Hydra of commands:
Finally, define your character. While I describe the details later, first, load an org-mode file and hit ~M-x~ to type: =rpgdm-ironsworn-new-character= and answer the questions. This will create the necessary formatting and you are ready to play with either your key-binding (or ~F6~) to bring up the Hydra of commands:
#+ATTR_HTML: :width 1100px
[[file:images/ui-screenshot.png]]
@ -56,16 +56,21 @@ Finally, define your character. While I describe the details later, hit ~M-x~ an
While the interface may change (and I may not update that screenshot too often), let me explain what you're looking at:
- The ~0~, ~1~ and ~6~ roll dice and display the result. Not very interesting.
- ~d~ rolls the /Ironsworn/ dice, two d10s and a d6 allowing you to add all the modifiers you want. Better.
- ~e~, ~h~, ~i~, ~s~, and ~w~ roll against your character's /edge/, /heart/, /iron/, /shadow/ and /wits/ respectively.
- ~D~ rolls the /Ironsworn/ dice, two d10s and a d6 allowing you to add all the modifiers you want. Better.
- ~e~, ~r~, ~i~, ~h~, and ~w~ roll against your character's /edge/, /heart/, /iron/, /shadow/ and /wits/ respectively.
- ~l~, ~s~, and ~p~ rolls against your current /health/, /spirit/ and /supply/ respectively.
- ~L~, ~S~, and ~P~ allows you to adjust your /health/, /spirit/ and /supply/ respectively.
- ~M~ let you change your momentum (rolls that benefit from momentum will show it)
- ~p~ allows you to create a new progress track, or mark progress against it. It has its own submenu:
#+ATTR_HTML: :width 1100px
[[file:images/ui-progress.png]]
- ~m~ lets you choose a move, display the details, and lets you roll against it.
#+ATTR_HTML: :width 1100px
[[file:images/list-of-moves.png]]
- ~H~, ~S~, ~G~, and ~M~ let you change those stats about your character
- ~z~ let's you ask a yes/no question, choosing how likely it is. Good for both solo play as well as turning it back on your players, "How likely do you think moor is haunted?"
- ~d~ lets you choose a Delve-specific actions, chosen after youve performed the move.
#+ATTR_ORG: :width 1627px
[[file:images/ui-delve.png]]
- ~z~ let's you ask a yes/no question, choosing how likely it is. Good for both solo play as well as turning it back on your players, "How likely do you think the moor is haunted?"
- ~c~ displays a list of random tables to roll against:
#+ATTR_HTML: :width 1100px
[[file:images/list-of-tables.png]]
@ -606,37 +611,127 @@ Just to prove it to ourselves, all of the following expressions return the same
#+END_SRC
*** Adjusting Character Stats
We need to modify /some/ of the stored values, like =health= and =supply=:
We need to modify /some/ of the stored values, like =health= and =supply,= but the value I want to enter could be:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-ironsworn-adjust-stat (stat adj &optional default)
"Increase or decrease the current character's STAT by ADJ.
If the STAT isn't found, returns DEFAULT."
(let* ((curr (rpgdm-ironsworn-character-stat stat))
(new (+ curr adj)))
(rpgdm-ironsworn-store-character-state stat new)))
- A modifier to /increase/ the value incrementally, like +1
- A modifier to /decrease/ the value incrementally, like -2
- A set value (like =5= when Sojourning).
- A reset to some default value, like going back to =2= for momentum.
(defun rpgdm-ironsworn-adjust-health (health-adj)
"Increase or decrease the current character's health by HEALTH-ADJ."
(interactive "nHealth Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'health health-adj 5))
This function will be used for =interactive= to return a tuple of both the /operation/ as well as the number:
(defun rpgdm-ironsworn-adjust-spirit (spirit-adj)
"Increase or decrease the current character's spirit by SPIRIT-ADJ."
(interactive "nSpirit Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'spirit spirit-adj 5))
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--read-stat (label)
"A `read-string', but for the changeable value associated with LABEL.
A `+1' means increasing the stat by 1, while a `-1' decreased that stat.
A `=3' sets the stat to the number (in this case, `3').
(defun rpgdm-ironsworn-adjust-supply (supply-adj)
"Increase or decrease the current character's supply by SUPPLY-ADJ."
(interactive "nSupply Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'supply supply-adj 5))
Hitting return (entering a blank value) increments the 'momentum stat,
but decrements any other stats by `1'. Any other value means to take
the default for that stat."
(let ((value (read-string (format "Adjustment to %s (+/-/= for absolute value): " label)))
(rxnum (rx (group (optional (or "+" "-" "="))) (* space) (group (+ digit)) (* space))))
(defun rpgdm-ironsworn-adjust-momentum (momentum-adj)
"Increase or decrease the current character's momentum by MOMENTUM-ADJ."
(interactive "nMomentum Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'momentum momentum-adj 2))
(if (string-match rxnum value)
(let ((sign (match-string 1 value))
(numb (string-to-number (match-string 2 value))))
(cond
((equal sign "-") `(:decrease ,numb))
((equal sign "+") `(:increase ,numb))
((equal sign "=") `(:absolute ,numb))
(t (if (eq label `momentum) `(:increase ,numb) `(:decrease ,numb)))))
(if (string-blank-p value)
(if (eq label 'momentum) '(:increase 1) '(:decrease 1))
'(:reset 0)))))
#+END_SRC
Best if we wrote some unit tests to both explain and verify this function. This test uses the [[help:cl-letf][letf]], which allows us to override the [[help:read-string][read-string]] function for my tests. Why yes, this is clever way of doing what other languages would need a /mock/ object.
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn--read-stat-test ()
;; Numbers with a minus sign always should indicate a decrease to the current value:
(cl-letf (((symbol-function 'read-string) (lambda (s) "-2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:decrease 2))))
;; Numbers with a minus sign always should indicate a increase to the current value:
(cl-letf (((symbol-function 'read-string) (lambda (s) "+2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:increase 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:increase 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:increase 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:increase 2))))
;; Numbers with a minus sign always should indicate a new setting:
(cl-letf (((symbol-function 'read-string) (lambda (s) "=2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:absolute 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:absolute 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:absolute 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:absolute 2))))
;; Just a number should change based on the type so stat, most go down, momentum goes up:
(cl-letf (((symbol-function 'read-string) (lambda (s) "2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:increase 2))))
;; No numeric value, most stats go down by one, but momentum goes up by one:
(cl-letf (((symbol-function 'read-string) (lambda (s) "")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:decrease 1)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:decrease 1)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:decrease 1)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:increase 1))))
;; Anything else should return a :reset, as it will take the default value:
(cl-letf (((symbol-function 'read-string) (lambda (s) "go back")))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:reset 0)))))
#+END_SRC
The =rpgdm-ironsworn-adjust-stat= function takes one of the four stats, like =health= or =momentum=, as well as its =default= or /starting/ value, collects the /current value/ (the =curr= variable), and then creates a new value based on the /operator/ determined by the input from =rpgdm-ironsworn--read-stat=. It sets the new stat by calling =rpgdm-ironsworn-store-character-state= defined below.
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-ironsworn-adjust-stat (stat &optional default)
"Increase or decrease the current character's STAT by ADJ.
If the STAT isn't found, returns DEFAULT."
(let* ((tuple (rpgdm-ironsworn--read-stat stat))
(curr (rpgdm-ironsworn-character-stat stat))
(oper (first tuple))
(numb (second tuple))
(new (cl-case oper
(:increase (+ curr numb))
(:decrease (- curr numb))
(:absolute numb)
(t default))))
(message "Combining curr %d with %d with %s operator" curr numb oper)
(rpgdm-ironsworn-store-character-state stat new)))
#+END_SRC
A few sugar functions for adding to our interface for each of the four stats:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-adjust-health ()
"Increase or decrease the current character's health interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'health 5))
(defun rpgdm-ironsworn-adjust-spirit ()
"Increase or decrease the current character's spirit interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'spirit 5))
(defun rpgdm-ironsworn-adjust-supply ()
"Increase or decrease the current character's supply interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'supply 5))
(defun rpgdm-ironsworn-adjust-momentum ()
"Increase or decrease the current character's momentum interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'momentum 2))
#+END_SRC
** Roll against Character Stats
Which allows us to create character stat-specific rolling functions:
@ -1193,7 +1288,7 @@ Lets put this function too in our =rpgdm-tables= hash table, so I can easily
To begin delving into a site, you choose a /theme/ and a /domain/, and then . Leta have a function that allows us to choose both (and store) so that we can refer to them again.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-discover-a-site (theme domain)
(defun rpgdm-ironsworn-discover-a-site (theme domain &optional name level)
"Store a Delve Site information in the org file.
The THEME and DOMAIN need to match org files in the `tables'
directory, and the choices themselves come from these files.
@ -1207,12 +1302,16 @@ To begin delving into a site, you choose a /theme/ and a /domain/, and then . Le
(completing-read "What is the site theme? " rpgdm-ironsworn-site-themes)
(completing-read "What is the domain? " rpgdm-ironsworn-site-domains)))
(rpgdm-ironsworn-progress-create
(read-string "Site Name: "
(rpgdm-ironsworn-oracle-site-name domain))
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels))
(next-line 2)
(when (called-interactively-p)
(setq name (read-string "Site Name: " (rpgdm-ironsworn-oracle-site-name domain)))
(setq level (completing-read-value "Progress Level: " rpgdm-ironsworn-progress-levels)))
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name)
(rpgdm-ironsworn-progress-create name level)
(ignore-errors
(next-line 2))
(rpgdm-ironsworn-store-character-state 'site-theme (downcase theme))
(rpgdm-ironsworn-store-character-state 'site-domain (downcase domain)))
#+END_SRC
@ -1515,7 +1614,7 @@ But we roll some of these more than others, especially since "moves" does most o
(defhydra hydra-rpgdm (:color blue :hint nil)
"
^Dice^ 0=d100 1=d10 6=d6 ^Roll/Adjust^ ^Oracles/Tables^ ^Moving/Editing^ ^Messages^
----------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------
_D_: Roll Dice _p_: Progress _l_/_L_: Health _z_/_Z_: Yes/No Oracle _o_: Links ⌘-h: Show Stats
_e_: Roll Edge _h_: Roll Shadow _t_/_T_: Spirit _c_/_C_: Show Oracle _J_/_K_: Page up/dn ⌘-l: Last Results
_r_: Roll Heart _w_: Roll Wits _s_/_S_: Supply _O_: Load Oracles _N_/_W_: Narrow/Widen ⌘-k: ↑ Previous

BIN
images/ui-delve.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View file

@ -27,6 +27,46 @@
(should (= 4 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6) 4))))
(should (= 3 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6))))))
(ert-deftest rpgdm-ironsworn--read-stat-test ()
;; Numbers with a minus sign always should indicate a decrease to the current value:
(cl-letf (((symbol-function 'read-string) (lambda (s) "-2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:decrease 2))))
;; Numbers with a minus sign always should indicate a increase to the current value:
(cl-letf (((symbol-function 'read-string) (lambda (s) "+2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:increase 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:increase 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:increase 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:increase 2))))
;; Numbers with a minus sign always should indicate a new setting:
(cl-letf (((symbol-function 'read-string) (lambda (s) "=2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:absolute 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:absolute 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:absolute 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:absolute 2))))
;; Just a number should change based on the type so stat, most go down, momentum goes up:
(cl-letf (((symbol-function 'read-string) (lambda (s) "2")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:decrease 2)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:increase 2))))
;; No numeric value, most stats go down by one, but momentum goes up by one:
(cl-letf (((symbol-function 'read-string) (lambda (s) "")))
(should (equal (rpgdm-ironsworn--read-stat 'health) '(:decrease 1)))
(should (equal (rpgdm-ironsworn--read-stat 'spirit) '(:decrease 1)))
(should (equal (rpgdm-ironsworn--read-stat 'supply) '(:decrease 1)))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:increase 1))))
;; Anything else should return a :reset, as it will take the default value:
(cl-letf (((symbol-function 'read-string) (lambda (s) "go back")))
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:reset 0)))))
(ert-deftest rpgdm-ironsworn--move-tuple-test ()
(let ((file "moves/fate/ask-the-oracle.org")
(full "~/other/over/here/moves/fate/ask-the-oracle.org"))

View file

@ -313,32 +313,64 @@ the `rpgdm-ironsworn-current-character-state' function."
(setq character (rpgdm-ironsworn-current-character-state)))
(gethash stat character 1))
(defun rpgdm-ironsworn-adjust-stat (stat adj &optional default)
(defun rpgdm-ironsworn--read-stat (label)
"A `read-string', but for the changeable value associated with LABEL.
A `+1' means increasing the stat by 1, while a `-1' decreased that stat.
A `=3' sets the stat to the number (in this case, `3').
Hitting return (entering a blank value) increments the 'momentum stat,
but decrements any other stats by `1'. Any other value means to take
the default for that stat."
(let ((value (read-string (format "Adjustment to %s (+/-/= for absolute value): " label)))
(rxnum (rx (group (optional (or "+" "-" "="))) (* space) (group (+ digit)) (* space))))
(if (string-match rxnum value)
(let ((sign (match-string 1 value))
(numb (string-to-number (match-string 2 value))))
(cond
((equal sign "-") `(:decrease ,numb))
((equal sign "+") `(:increase ,numb))
((equal sign "=") `(:absolute ,numb))
(t (if (eq label `momentum) `(:increase ,numb) `(:decrease ,numb)))))
(if (string-blank-p value)
(if (eq label 'momentum) '(:increase 1) '(:decrease 1))
'(:reset 0)))))
(defun rpgdm-ironsworn-adjust-stat (stat &optional default)
"Increase or decrease the current character's STAT by ADJ.
If the STAT isn't found, returns DEFAULT."
(let* ((curr (rpgdm-ironsworn-character-stat stat))
(new (+ curr adj)))
If the STAT isn't found, returns DEFAULT."
(let* ((tuple (rpgdm-ironsworn--read-stat stat))
(curr (rpgdm-ironsworn-character-stat stat))
(oper (first tuple))
(numb (second tuple))
(new (cl-case oper
(:increase (+ curr numb))
(:decrease (- curr numb))
(:absolute numb)
(t default))))
(message "Combining curr %d with %d with %s operator" curr numb oper)
(rpgdm-ironsworn-store-character-state stat new)))
(defun rpgdm-ironsworn-adjust-health (health-adj)
"Increase or decrease the current character's health by HEALTH-ADJ."
(interactive "nHealth Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'health health-adj 5))
(defun rpgdm-ironsworn-adjust-health ()
"Increase or decrease the current character's health interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'health 5))
(defun rpgdm-ironsworn-adjust-spirit (spirit-adj)
"Increase or decrease the current character's spirit by SPIRIT-ADJ."
(interactive "nSpirit Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'spirit spirit-adj 5))
(defun rpgdm-ironsworn-adjust-spirit ()
"Increase or decrease the current character's spirit interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'spirit 5))
(defun rpgdm-ironsworn-adjust-supply (supply-adj)
"Increase or decrease the current character's supply by SUPPLY-ADJ."
(interactive "nSupply Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'supply supply-adj 5))
(defun rpgdm-ironsworn-adjust-supply ()
"Increase or decrease the current character's supply interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'supply 5))
(defun rpgdm-ironsworn-adjust-momentum (momentum-adj)
"Increase or decrease the current character's momentum by MOMENTUM-ADJ."
(interactive "nMomentum Adjustment: ")
(rpgdm-ironsworn-adjust-stat 'momentum momentum-adj 2))
(defun rpgdm-ironsworn-adjust-momentum ()
"Increase or decrease the current character's momentum interactively."
(interactive)
(rpgdm-ironsworn-adjust-stat 'momentum 2))
(defun rpgdm-ironsworn-roll-stat (stat modifier)
"Roll an action based on a loaded character's STAT with a MODIFIER."
@ -701,7 +733,7 @@ The nature is a combination of theme and domain."
(puthash "site" 'rpgdm-ironsworn-oracle-site-nature rpgdm-tables)
(defun rpgdm-ironsworn-discover-a-site (theme domain)
(defun rpgdm-ironsworn-discover-a-site (theme domain &optional name level)
"Store a Delve Site information in the org file.
The THEME and DOMAIN need to match org files in the `tables'
directory, and the choices themselves come from these files.
@ -715,12 +747,16 @@ and progress level, and stores all this information in the org file."
(completing-read "What is the site theme? " rpgdm-ironsworn-site-themes)
(completing-read "What is the domain? " rpgdm-ironsworn-site-domains)))
(rpgdm-ironsworn-progress-create
(read-string "Site Name: "
(rpgdm-ironsworn-oracle-site-name domain))
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels))
(next-line 2)
(when (called-interactively-p)
(setq name (read-string "Site Name: " (rpgdm-ironsworn-oracle-site-name domain)))
(setq level (completing-read-value "Progress Level: " rpgdm-ironsworn-progress-levels)))
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name)
(rpgdm-ironsworn-progress-create name level)
(ignore-errors
(next-line 2))
(rpgdm-ironsworn-store-character-state 'site-theme (downcase theme))
(rpgdm-ironsworn-store-character-state 'site-domain (downcase domain)))
@ -937,7 +973,7 @@ You'll need to pick and choose what works and discard what doesn't."
(defhydra hydra-rpgdm (:color blue :hint nil)
"
^Dice^ 0=d100 1=d10 6=d6 ^Roll/Adjust^ ^Oracles/Tables^ ^Moving/Editing^ ^Messages^
----------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------
_D_: Roll Dice _p_: Progress _l_/_L_: Health _z_/_Z_: Yes/No Oracle _o_: Links -h: Show Stats
_e_: Roll Edge _h_: Roll Shadow _t_/_T_: Spirit _c_/_C_: Show Oracle _J_/_K_: Page up/dn -l: Last Results
_r_: Roll Heart _w_: Roll Wits _s_/_S_: Supply _O_: Load Oracles _N_/_W_: Narrow/Widen -k: Previous