Fix bug associated with org-up-element
Wrote my own version called `org-up-heading' which works as I expect. I also fixed a few other warnings.
This commit is contained in:
parent
474cdd0f9f
commit
7a10f7ab1f
3 changed files with 258 additions and 96 deletions
111
README.org
111
README.org
|
@ -16,6 +16,7 @@ While my original goal for creating my [[https://gitlab.com/howardabrams/emacs-r
|
||||||
However, the struggles for getting friends to play online proved as challenging as getting them around the table, so I started looking for [[https://www.dicebreaker.com/categories/roleplaying-game/how-to/how-to-play-tabletop-rpgs-by-yourself][solo rpg options]] and discovered [[https://www.ironswornrpg.com/][Ironsworn RPG]]. The bulk of the game is its /moves/ and its /oracles/ (random tables for /everything/). I easily copied sections of the [[https://docs.google.com/document/d/11ypqt6GfLuBhGDJuBGWKlHa-Ru48Tf3G_6zbrYKmXgY/edit#heading=h.xl9vk0d7wwn3][SRD]] into org files, [[file:tables][tables]].
|
However, the struggles for getting friends to play online proved as challenging as getting them around the table, so I started looking for [[https://www.dicebreaker.com/categories/roleplaying-game/how-to/how-to-play-tabletop-rpgs-by-yourself][solo rpg options]] and discovered [[https://www.ironswornrpg.com/][Ironsworn RPG]]. The bulk of the game is its /moves/ and its /oracles/ (random tables for /everything/). I easily copied sections of the [[https://docs.google.com/document/d/11ypqt6GfLuBhGDJuBGWKlHa-Ru48Tf3G_6zbrYKmXgY/edit#heading=h.xl9vk0d7wwn3][SRD]] into org files, [[file:tables][tables]].
|
||||||
|
|
||||||
After listening to the author, [[https://twitter.com/ShawnTomkin][Shawn Tomkin]], play the game in his podcast, [[https://ironsworn.podbean.com/][Ask the Oracle]], he used [[https://roll20.net][Roll20]]. The [[https://wiki.roll20.net/Ironsworn][character sheet]] was brilliant, as each move was /described/ along with being able to roll on them. While I love physically rolling dice, perhaps mimicking the approach in org files in Emacs for [[https://www.ironswornrpg.com/products-ironsworn][solo play]] may be ideal.
|
After listening to the author, [[https://twitter.com/ShawnTomkin][Shawn Tomkin]], play the game in his podcast, [[https://ironsworn.podbean.com/][Ask the Oracle]], he used [[https://roll20.net][Roll20]]. The [[https://wiki.roll20.net/Ironsworn][character sheet]] was brilliant, as each move was /described/ along with being able to roll on them. While I love physically rolling dice, perhaps mimicking the approach in org files in Emacs for [[https://www.ironswornrpg.com/products-ironsworn][solo play]] may be ideal.
|
||||||
|
|
||||||
* Getting Started
|
* Getting Started
|
||||||
Neither this, nor the [[https://gitlab.com/howardabrams/emacs-rpgdm][rpgdm project]] are currently in MELPA, so if you wish to follow along at home, you'll need to clone both repos, and add them to your =load-path= variable with =add-to-list=:
|
Neither this, nor the [[https://gitlab.com/howardabrams/emacs-rpgdm][rpgdm project]] are currently in MELPA, so if you wish to follow along at home, you'll need to clone both repos, and add them to your =load-path= variable with =add-to-list=:
|
||||||
|
|
||||||
|
@ -179,8 +180,8 @@ To begin, we'll need the [[https://gitlab.com/howardabrams/emacs-rpgdm][rpgdm pr
|
||||||
We also need the name of the directory for this project, so that we can load tables and other documentation.
|
We also need the name of the directory for this project, so that we can load tables and other documentation.
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defvar rpgdm-ironsworn-project (file-name-directory load-file-name)
|
(defvar rpgdm-ironsworn-project (file-name-directory load-file-name)
|
||||||
"The root directory to the rpgdm-ironsworn project.")
|
"The root directory to the rpgdm-ironsworn project.")
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
** Dice Roller
|
** Dice Roller
|
||||||
In *Ironsworn*, all dice rolls follow a pattern where you set the challenge level for a check by rolling /challenge dice/ (two d10s) and compare that against rolling an /action die/ (a single d6 ... adding all modifiers to that six-sided die).
|
In *Ironsworn*, all dice rolls follow a pattern where you set the challenge level for a check by rolling /challenge dice/ (two d10s) and compare that against rolling an /action die/ (a single d6 ... adding all modifiers to that six-sided die).
|
||||||
|
@ -330,12 +331,11 @@ We store the assets in a collection of org files in the [[file:assets/][assets]]
|
||||||
"Given a FILENAME of an Ironsworn asset, return an Asset label."
|
"Given a FILENAME of an Ironsworn asset, return an Asset label."
|
||||||
(cl-flet* ((convert (str) (s-replace "-" " " str))
|
(cl-flet* ((convert (str) (s-replace "-" " " str))
|
||||||
(uppity (str) (s-titleize (convert str))))
|
(uppity (str) (s-titleize (convert str))))
|
||||||
(when (string-match (rx (one-or-more any)
|
(when (string-match (rx (one-or-more any) "/"
|
||||||
"/"
|
;; parent directory
|
||||||
(group (one-or-more (not "/"))) ; parent directory
|
(group (one-or-more (not "/"))) "/"
|
||||||
"/"
|
;; base filename
|
||||||
(group (one-or-more (not "/"))) ; base filename
|
(group (one-or-more (not "/"))) ".org")
|
||||||
".org")
|
|
||||||
filename)
|
filename)
|
||||||
(format "%s :: %s"
|
(format "%s :: %s"
|
||||||
(uppity (match-string 1 filename))
|
(uppity (match-string 1 filename))
|
||||||
|
@ -434,7 +434,7 @@ The rules say to start off with three assets, so let's have a function that can
|
||||||
If NUMBER is nil, then return 3."
|
If NUMBER is nil, then return 3."
|
||||||
(unless number
|
(unless number
|
||||||
(setq number 3))
|
(setq number 3))
|
||||||
(loop for x from 1 to number
|
(cl-loop for x from 1 to number
|
||||||
collect (seq-random-elt asset-filenames)))
|
collect (seq-random-elt asset-filenames)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
@ -557,12 +557,12 @@ Sure, you could open up the appropriate drawer to see a character's stats, but w
|
||||||
"Colorized the STAT from a CHARACTER hash containing it.
|
"Colorized the STAT from a CHARACTER hash containing it.
|
||||||
See `rpgdm-ironsworn-character-display'."
|
See `rpgdm-ironsworn-character-display'."
|
||||||
(let* ((value (gethash stat character))
|
(let* ((value (gethash stat character))
|
||||||
(s-val (number-to-string value))
|
(s-val (number-to-string value))
|
||||||
(color (cond
|
(color (cond
|
||||||
((< value 1) "red")
|
((< value 1) "red")
|
||||||
((< value 3) "orange")
|
((< value 3) "orange")
|
||||||
((< value 4) "yellow")
|
((< value 4) "yellow")
|
||||||
(t "green"))))
|
(t "green"))))
|
||||||
(propertize s-val 'face `(:foreground ,color))))
|
(propertize s-val 'face `(:foreground ,color))))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn-character-display ()
|
(defun rpgdm-ironsworn-character-display ()
|
||||||
|
@ -597,7 +597,8 @@ We need an /internal representation/ of a character using a hash table of the at
|
||||||
|
|
||||||
(define-hash-table-test 'str-or-keys
|
(define-hash-table-test 'str-or-keys
|
||||||
(lambda (a b)
|
(lambda (a b)
|
||||||
(string-equal (rpgdm-ironsworn-to-string a) (rpgdm-ironsworn-to-string b)))
|
(string-equal (rpgdm-ironsworn-to-string a)
|
||||||
|
(rpgdm-ironsworn-to-string b)))
|
||||||
|
|
||||||
(lambda (s) (sxhash-equal (rpgdm-ironsworn-to-string s))))
|
(lambda (s) (sxhash-equal (rpgdm-ironsworn-to-string s))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
@ -610,9 +611,9 @@ And a help function to retrieve the stats of the character is just a wrapper aro
|
||||||
"Return integer value associated with a character's STAT.
|
"Return integer value associated with a character's STAT.
|
||||||
If CHARACTER doesn't refer to a character hash, then this calls
|
If CHARACTER doesn't refer to a character hash, then this calls
|
||||||
the `rpgdm-ironsworn-current-character-state' function."
|
the `rpgdm-ironsworn-current-character-state' function."
|
||||||
(when (null character)
|
(unless character
|
||||||
(setq character (rpgdm-ironsworn-current-character-state)))
|
(setq character (rpgdm-ironsworn-current-character-state)))
|
||||||
(gethash stat character 1))
|
(gethash stat character 0))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
Just to prove it to ourselves, all of the following expressions return the same number (however, only run this in an org file that has a character properly formatted in it):
|
Just to prove it to ourselves, all of the following expressions return the same number (however, only run this in an org file that has a character properly formatted in it):
|
||||||
|
@ -709,7 +710,7 @@ The =rpgdm-ironsworn-adjust-stat= function takes one of the four stats, like =
|
||||||
#+BEGIN_SRC emacs-lisp :results silent
|
#+BEGIN_SRC emacs-lisp :results silent
|
||||||
(defun rpgdm-ironsworn-adjust-stat (stat &optional default)
|
(defun rpgdm-ironsworn-adjust-stat (stat &optional default)
|
||||||
"Increase or decrease the current character's STAT by ADJ.
|
"Increase or decrease the current character's STAT by ADJ.
|
||||||
If the STAT isn't found, returns DEFAULT."
|
If the STAT isn't found, returns DEFAULT."
|
||||||
(let* ((tuple (rpgdm-ironsworn--read-stat stat))
|
(let* ((tuple (rpgdm-ironsworn--read-stat stat))
|
||||||
(curr (rpgdm-ironsworn-character-stat stat))
|
(curr (rpgdm-ironsworn-character-stat stat))
|
||||||
(oper (first tuple))
|
(oper (first tuple))
|
||||||
|
@ -747,7 +748,7 @@ A few sugar functions for adding to our interface for each of the four stats:
|
||||||
(rpgdm-ironsworn-adjust-stat 'momentum 2))
|
(rpgdm-ironsworn-adjust-stat 'momentum 2))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
** Roll against Character Stats
|
** Roll against Character Stats
|
||||||
Which allows us to create character stat-specific rolling functions:
|
The previous functions allows us to create character stat-specific rolling functions:
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun rpgdm-ironsworn-roll-stat (stat modifier)
|
(defun rpgdm-ironsworn-roll-stat (stat modifier)
|
||||||
|
@ -1745,7 +1746,7 @@ Why do we need it? Well, when we will want this when we /re-set/ the property.
|
||||||
As I've mentioned before, the code needs to walk "up" an Org Tree looking for properties. The crux is using the /internal/ [[help:org-element--get-node-properties][org-element--get-node-properties]] function, which returns a [[info:elisp#Property Lists][property list]] /iff/ the point is on a header.
|
As I've mentioned before, the code needs to walk "up" an Org Tree looking for properties. The crux is using the /internal/ [[help:org-element--get-node-properties][org-element--get-node-properties]] function, which returns a [[info:elisp#Property Lists][property list]] /iff/ the point is on a header.
|
||||||
|
|
||||||
So the general idea is:
|
So the general idea is:
|
||||||
- Move to the previous header, e.g. [[help:org-up-element][org-up-element]]
|
- Move to the previous header
|
||||||
- Collect the properties
|
- Collect the properties
|
||||||
- Move up to that header's parent
|
- Move up to that header's parent
|
||||||
- Collect its properties, etc.
|
- Collect its properties, etc.
|
||||||
|
@ -1754,12 +1755,22 @@ So the general idea is:
|
||||||
Since we need to know if we are at the top-level, we could have a function, =org-heading-level= that returns =1= if we are at the top-level, and =0= if we aren't at any level:
|
Since we need to know if we are at the top-level, we could have a function, =org-heading-level= that returns =1= if we are at the top-level, and =0= if we aren't at any level:
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun org-heading-level ()
|
(defun org-heading-level ()
|
||||||
"Return heading level of the element at the point.
|
"Return heading level of the element at the point.
|
||||||
Return 0 if not at a heading, or above first headline."
|
Return 0 if not at a heading, or above first headline."
|
||||||
(if-let ((level-str (org-element-property :level (org-element-at-point))))
|
(if-let ((level-str (org-element-property :level (org-element-at-point))))
|
||||||
level-str
|
level-str
|
||||||
0))
|
0))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
Since [[help:org-up-element][org-up-element]]’s behavior has changed, and [[help:outline-up-heading][outline-up-heading]] doesn’t go to the next heading if it is already on a heading, we need to make a little helper:
|
||||||
|
|
||||||
|
#+BEGIN_SRC emacs-lisp
|
||||||
|
(defun org-up-heading ()
|
||||||
|
"Move the point to next parent heading, unless already at the top-level."
|
||||||
|
(if (= 0 (org-heading-level))
|
||||||
|
(outline-up-heading 0)
|
||||||
|
(outline-up-heading 1)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
Enough chit-chat, let's write this function. While we are at it, let's convert the property symbols into short symbols, e.g. =:IRONSWORN-SHADOW= should just be =shadow=, and number values should be numeric:
|
Enough chit-chat, let's write this function. While we are at it, let's convert the property symbols into short symbols, e.g. =:IRONSWORN-SHADOW= should just be =shadow=, and number values should be numeric:
|
||||||
|
@ -1776,21 +1787,22 @@ Enough chit-chat, let's write this function. While we are at it, let's convert t
|
||||||
|
|
||||||
(defun value-convert (value)
|
(defun value-convert (value)
|
||||||
(if (string-match (rx bos (one-or-more digit) eos) value)
|
(if (string-match (rx bos (one-or-more digit) eos) value)
|
||||||
(string-to-number value)
|
(string-to-number value)
|
||||||
value))
|
value))
|
||||||
|
|
||||||
(let ((props (org-element--get-node-properties)))
|
(let ((props (org-element--get-node-properties)))
|
||||||
(loop for (k v) on props by (function cddr) do
|
(cl-loop for (k v) on props by (function cddr) do
|
||||||
;; If key is ironsworn property, but isn't in the table...
|
;; If key is ironsworn property, but isn't in the table...
|
||||||
(when (rpgdm-ironsworn--property-p k)
|
(when (rpgdm-ironsworn--property-p k)
|
||||||
(let ((key (key-convert k))
|
(let ((key (key-convert k))
|
||||||
(val (value-convert v)))
|
(val (value-convert v)))
|
||||||
(unless (gethash key results)
|
(message "Found %s : %s" key val)
|
||||||
(puthash key val results)))))
|
(unless (gethash key results)
|
||||||
|
(puthash key val results)))))
|
||||||
|
|
||||||
(unless (= (org-heading-level) 1)
|
(unless (= (org-heading-level) 1)
|
||||||
(org-up-element)
|
(org-up-heading)
|
||||||
(rpgdm-ironsworn--current-character-state results))))
|
(rpgdm-ironsworn--current-character-state results))))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn-current-character-state ()
|
(defun rpgdm-ironsworn-current-character-state ()
|
||||||
"Return all set properties based on cursor position in org doc.
|
"Return all set properties based on cursor position in org doc.
|
||||||
|
@ -1799,16 +1811,17 @@ Enough chit-chat, let's write this function. While we are at it, let's convert t
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(let ((results (make-hash-table :test 'str-or-keys)))
|
(let ((results (make-hash-table :test 'str-or-keys)))
|
||||||
(unless (org-at-heading-p)
|
(unless (org-at-heading-p)
|
||||||
(org-up-element))
|
(org-up-heading))
|
||||||
|
|
||||||
;; Put the lowest heading title in the results hashtable:
|
;; Put the lowest heading title in the results hashtable:
|
||||||
(puthash 'title (thread-first
|
(puthash 'title (thread-first
|
||||||
(org-element-at-point)
|
(org-element-at-point)
|
||||||
(second)
|
(second)
|
||||||
(plist-get :raw-value))
|
(plist-get :raw-value))
|
||||||
results)
|
results)
|
||||||
(rpgdm-ironsworn--current-character-state results)
|
(message "Hash: %s" results)
|
||||||
results)))
|
(rpgdm-ironsworn--current-character-state results)
|
||||||
|
results)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
*** Reading Progress Tracks from Org Files
|
*** Reading Progress Tracks from Org Files
|
||||||
A progress track looks like this:
|
A progress track looks like this:
|
||||||
|
@ -1860,13 +1873,13 @@ The function works fine for [[help:completing-read][completing-read]], as it acc
|
||||||
|
|
||||||
Unlike the =rpgdm-ironsworn-store-character-state= function, we want up update the /location/ in the org file where a progress track is defined and stored.
|
Unlike the =rpgdm-ironsworn-store-character-state= function, we want up update the /location/ in the org file where a progress track is defined and stored.
|
||||||
|
|
||||||
I like the recursive notion of walking higher in the org file using [[help:org-up-element][org-up-element]], however, having a function use both Common Lisp's [[help:cl-loop][loop]] as well as the Clojure-inspired [[help:-let*][-let*]] really shows the fusion that Emacs has become:
|
I like the recursive notion of walking higher in the org file using [[help:outline-up-heading][org-up-heading]], however, having a function use both Common Lisp's [[help:cl-loop][loop]] as well as the Clojure-inspired [[help:-let*][-let*]] really shows the fusion that Emacs has become:
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun rpgdm-ironsworn--mark-progress-track (str)
|
(defun rpgdm-ironsworn--mark-progress-track (str)
|
||||||
"Given a progress track's name, STR, update its progress mark."
|
"Given a progress track's name, STR, update its progress mark."
|
||||||
(let ((props (org-element--get-node-properties)))
|
(let ((props (org-element--get-node-properties)))
|
||||||
(loop for (k v) on props by (function cddr) do
|
(cl-loop for (k v) on props by (function cddr) do
|
||||||
(when (rpgdm-ironsworn--progress-p k)
|
(when (rpgdm-ironsworn--progress-p k)
|
||||||
(-let* (((label level progress) (rpgdm-ironsworn--progress-values v)))
|
(-let* (((label level progress) (rpgdm-ironsworn--progress-values v)))
|
||||||
(when (equal str label)
|
(when (equal str label)
|
||||||
|
@ -1877,7 +1890,7 @@ I like the recursive notion of walking higher in the org file using [[help:org-u
|
||||||
|
|
||||||
;; Did not find a match at this level, let's pop up one and try again:
|
;; Did not find a match at this level, let's pop up one and try again:
|
||||||
(unless (= (org-heading-level) 1)
|
(unless (= (org-heading-level) 1)
|
||||||
(org-up-element)
|
(org-up-heading)
|
||||||
(rpgdm-ironsworn--mark-progress-track str)))
|
(rpgdm-ironsworn--mark-progress-track str)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,48 @@
|
||||||
(cl-letf (((symbol-function 'read-string) (lambda (s) "go back")))
|
(cl-letf (((symbol-function 'read-string) (lambda (s) "go back")))
|
||||||
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:reset 0)))))
|
(should (equal (rpgdm-ironsworn--read-stat 'momentum) '(:reset 0)))))
|
||||||
|
|
||||||
|
(ert-deftest rpgdm-ironsworn--asset-stat-key-test ()
|
||||||
|
(should (symbolp (rpgdm-ironsworn--asset-stat-key "Foo bar")))
|
||||||
|
(should (string= 'asset-foo-bar (rpgdm-ironsworn--asset-stat-key "Foo bar")))
|
||||||
|
(should (string= 'asset-foos-bar (rpgdm-ironsworn--asset-stat-key "Foo's bar"))))
|
||||||
|
|
||||||
|
(ert-deftest rpgdm-ironsworn--asset-stat-name-test ()
|
||||||
|
(should (string= "Foo Bar" (rpgdm-ironsworn--asset-stat-name 'asset-foo-bar))))
|
||||||
|
|
||||||
|
(ert-deftest rpgdm-ironsworn--asset-stat-alist-test ()
|
||||||
|
(let* ((stats #s(hash-table size 65 test str-or-keys rehash-size 1.5 rehash-threshold 0.8125 data
|
||||||
|
(title "Travels of Kannan"
|
||||||
|
edge 2 heart 1 iron 1 shadow 2 wits 3
|
||||||
|
health 5 spirit 5 supply 5 momentum 2
|
||||||
|
asset-mammoth-health 5
|
||||||
|
asset-invoke-level 3)))
|
||||||
|
(assets (rpgdm-ironsworn--asset-stat-alist stats)))
|
||||||
|
(should (= (length assets) 2))
|
||||||
|
(should (eq (alist-get "Mammoth Health" assets 0 nil 'equal) 'asset-mammoth-health))
|
||||||
|
(should (eq (alist-get "Invoke Level" assets 0 nil 'equal) 'asset-invoke-level))))
|
||||||
|
|
||||||
|
(ert-deftest rpgdm-ironsworn--asset-stat-alist-test ()
|
||||||
|
;; Using Lisp to `mock' the function to get an stat value:
|
||||||
|
(cl-letf (((symbol-function 'rpgdm-ironsworn-character-stat) (lambda (s) 3)))
|
||||||
|
|
||||||
|
(let* ((stats #s(hash-table size 65 test str-or-keys rehash-size 1.5
|
||||||
|
rehash-threshold 0.8125 data
|
||||||
|
(title "Travels of Kannan"
|
||||||
|
edge 2 heart 1 iron 1 shadow 2 wits 3
|
||||||
|
health 5 spirit 5 supply 5 momentum 2
|
||||||
|
asset-mammoth-health 3
|
||||||
|
asset-invoke-level 3))))
|
||||||
|
(should (string= "Mammoth Health: 3 Invoke Level: 3"
|
||||||
|
(rpgdm-ironsworn--asset-stat-show-all stats))))
|
||||||
|
|
||||||
|
;; Return an empty string if there are not asset-related stats:
|
||||||
|
(let* ((stats #s(hash-table size 65 test str-or-keys rehash-size 1.5
|
||||||
|
rehash-threshold 0.8125 data
|
||||||
|
(title "Travels of Kannan"
|
||||||
|
edge 2 heart 1 iron 1 shadow 2 wits 3
|
||||||
|
health 5 spirit 5 supply 5 momentum 2))))
|
||||||
|
(should (string= "" (rpgdm-ironsworn--asset-stat-show-all stats))))))
|
||||||
|
|
||||||
(ert-deftest rpgdm-ironsworn--move-tuple-test ()
|
(ert-deftest rpgdm-ironsworn--move-tuple-test ()
|
||||||
(let ((file "moves/fate/ask-the-oracle.org")
|
(let ((file "moves/fate/ask-the-oracle.org")
|
||||||
(full "~/other/over/here/moves/fate/ask-the-oracle.org"))
|
(full "~/other/over/here/moves/fate/ask-the-oracle.org"))
|
||||||
|
|
|
@ -111,12 +111,11 @@ a random name is generated for the purposes of the template."
|
||||||
"Given a FILENAME of an Ironsworn asset, return an Asset label."
|
"Given a FILENAME of an Ironsworn asset, return an Asset label."
|
||||||
(cl-flet* ((convert (str) (s-replace "-" " " str))
|
(cl-flet* ((convert (str) (s-replace "-" " " str))
|
||||||
(uppity (str) (s-titleize (convert str))))
|
(uppity (str) (s-titleize (convert str))))
|
||||||
(when (string-match (rx (one-or-more any)
|
(when (string-match (rx (one-or-more any) "/"
|
||||||
"/"
|
;; parent directory
|
||||||
(group (one-or-more (not "/"))) ; parent directory
|
(group (one-or-more (not "/"))) "/"
|
||||||
"/"
|
;; base filename
|
||||||
(group (one-or-more (not "/"))) ; base filename
|
(group (one-or-more (not "/"))) ".org")
|
||||||
".org")
|
|
||||||
filename)
|
filename)
|
||||||
(format "%s :: %s"
|
(format "%s :: %s"
|
||||||
(uppity (match-string 1 filename))
|
(uppity (match-string 1 filename))
|
||||||
|
@ -177,7 +176,7 @@ That is, all are unique, only one companion, etc."
|
||||||
If NUMBER is nil, then return 3."
|
If NUMBER is nil, then return 3."
|
||||||
(unless number
|
(unless number
|
||||||
(setq number 3))
|
(setq number 3))
|
||||||
(loop for x from 1 to number
|
(cl-loop for x from 1 to number
|
||||||
collect (seq-random-elt asset-filenames)))
|
collect (seq-random-elt asset-filenames)))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn--random-character-assets (&optional number-of-assets)
|
(defun rpgdm-ironsworn--random-character-assets (&optional number-of-assets)
|
||||||
|
@ -265,12 +264,12 @@ which should be using the `org-mode' major mode."
|
||||||
"Colorized the STAT from a CHARACTER hash containing it.
|
"Colorized the STAT from a CHARACTER hash containing it.
|
||||||
See `rpgdm-ironsworn-character-display'."
|
See `rpgdm-ironsworn-character-display'."
|
||||||
(let* ((value (gethash stat character))
|
(let* ((value (gethash stat character))
|
||||||
(s-val (number-to-string value))
|
(s-val (number-to-string value))
|
||||||
(color (cond
|
(color (cond
|
||||||
((< value 1) "red")
|
((< value 1) "red")
|
||||||
((< value 3) "orange")
|
((< value 3) "orange")
|
||||||
((< value 4) "yellow")
|
((< value 4) "yellow")
|
||||||
(t "green"))))
|
(t "green"))))
|
||||||
(propertize s-val 'face `(:foreground ,color))))
|
(propertize s-val 'face `(:foreground ,color))))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn-character-display ()
|
(defun rpgdm-ironsworn-character-display ()
|
||||||
|
@ -278,18 +277,19 @@ See `rpgdm-ironsworn-character-display'."
|
||||||
(interactive)
|
(interactive)
|
||||||
(let ((character (rpgdm-ironsworn-current-character-state)))
|
(let ((character (rpgdm-ironsworn-current-character-state)))
|
||||||
(rpgdm-message "Edge: %d Heart: %d Iron: %d Shadow: %d Wits: %d
|
(rpgdm-message "Edge: %d Heart: %d Iron: %d Shadow: %d Wits: %d
|
||||||
Health: %s Spirit: %s Supply: %s Momentum: %d"
|
Health: %s Spirit: %s Supply: %s Momentum: %d %s"
|
||||||
(rpgdm-ironsworn-character-stat 'edge character)
|
(rpgdm-ironsworn-character-stat 'edge character)
|
||||||
(rpgdm-ironsworn-character-stat 'heart character)
|
(rpgdm-ironsworn-character-stat 'heart character)
|
||||||
(rpgdm-ironsworn-character-stat 'iron character)
|
(rpgdm-ironsworn-character-stat 'iron character)
|
||||||
(rpgdm-ironsworn-character-stat 'shadow character)
|
(rpgdm-ironsworn-character-stat 'shadow character)
|
||||||
(rpgdm-ironsworn-character-stat 'wits character)
|
(rpgdm-ironsworn-character-stat 'wits character)
|
||||||
|
|
||||||
(rpgdm-ironsworn--display-stat 'health character)
|
(rpgdm-ironsworn--display-stat 'health character)
|
||||||
(rpgdm-ironsworn--display-stat 'spirit character)
|
(rpgdm-ironsworn--display-stat 'spirit character)
|
||||||
(rpgdm-ironsworn--display-stat 'supply character)
|
(rpgdm-ironsworn--display-stat 'supply character)
|
||||||
|
|
||||||
(gethash 'momentum character 5))))
|
(gethash 'momentum character 5)
|
||||||
|
(rpgdm-ironsworn--asset-stat-show-all character))))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn-to-string (a)
|
(defun rpgdm-ironsworn-to-string (a)
|
||||||
"Return a lowercase string from either A, a string, keyword or symbol."
|
"Return a lowercase string from either A, a string, keyword or symbol."
|
||||||
|
@ -301,7 +301,8 @@ Health: %s Spirit: %s Supply: %s Momentum: %d"
|
||||||
|
|
||||||
(define-hash-table-test 'str-or-keys
|
(define-hash-table-test 'str-or-keys
|
||||||
(lambda (a b)
|
(lambda (a b)
|
||||||
(string-equal (rpgdm-ironsworn-to-string a) (rpgdm-ironsworn-to-string b)))
|
(string-equal (rpgdm-ironsworn-to-string a)
|
||||||
|
(rpgdm-ironsworn-to-string b)))
|
||||||
|
|
||||||
(lambda (s) (sxhash-equal (rpgdm-ironsworn-to-string s))))
|
(lambda (s) (sxhash-equal (rpgdm-ironsworn-to-string s))))
|
||||||
|
|
||||||
|
@ -309,9 +310,9 @@ Health: %s Spirit: %s Supply: %s Momentum: %d"
|
||||||
"Return integer value associated with a character's STAT.
|
"Return integer value associated with a character's STAT.
|
||||||
If CHARACTER doesn't refer to a character hash, then this calls
|
If CHARACTER doesn't refer to a character hash, then this calls
|
||||||
the `rpgdm-ironsworn-current-character-state' function."
|
the `rpgdm-ironsworn-current-character-state' function."
|
||||||
(when (null character)
|
(unless character
|
||||||
(setq character (rpgdm-ironsworn-current-character-state)))
|
(setq character (rpgdm-ironsworn-current-character-state)))
|
||||||
(gethash stat character 1))
|
(gethash stat character 0))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn--read-stat (label)
|
(defun rpgdm-ironsworn--read-stat (label)
|
||||||
"A `read-string', but for the changeable value associated with LABEL.
|
"A `read-string', but for the changeable value associated with LABEL.
|
||||||
|
@ -339,7 +340,7 @@ the default for that stat."
|
||||||
|
|
||||||
(defun rpgdm-ironsworn-adjust-stat (stat &optional default)
|
(defun rpgdm-ironsworn-adjust-stat (stat &optional default)
|
||||||
"Increase or decrease the current character's STAT by ADJ.
|
"Increase or decrease the current character's STAT by ADJ.
|
||||||
If the STAT isn't found, returns DEFAULT."
|
If the STAT isn't found, returns DEFAULT."
|
||||||
(let* ((tuple (rpgdm-ironsworn--read-stat stat))
|
(let* ((tuple (rpgdm-ironsworn--read-stat stat))
|
||||||
(curr (rpgdm-ironsworn-character-stat stat))
|
(curr (rpgdm-ironsworn-character-stat stat))
|
||||||
(oper (first tuple))
|
(oper (first tuple))
|
||||||
|
@ -424,6 +425,85 @@ the default for that stat."
|
||||||
(rpgdm-ironsworn-progress-roll
|
(rpgdm-ironsworn-progress-roll
|
||||||
(rpgdm-ironsworn-character-stat :supply)))
|
(rpgdm-ironsworn-character-stat :supply)))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn--asset-stat-key (name)
|
||||||
|
"Convert a string, NAME, into an `asset-str' symbol."
|
||||||
|
(thread-last name
|
||||||
|
(s-replace-regexp (rx space) "-")
|
||||||
|
(s-replace-regexp (rx (one-or-more (not (any "-" alphanumeric)))) "")
|
||||||
|
(downcase)
|
||||||
|
(format "asset-%s")
|
||||||
|
(make-symbol)))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn--asset-stat-name (stat)
|
||||||
|
"Convert an asset-related STAT symbol into a readable name."
|
||||||
|
(let ((no-asset (thread-first stat
|
||||||
|
(symbol-name)
|
||||||
|
(substring 6))))
|
||||||
|
(thread-last no-asset
|
||||||
|
(string-replace "-" " ")
|
||||||
|
(s-titleize))))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn-asset-stat-create (name value)
|
||||||
|
"Create a special stat associated with an asset.
|
||||||
|
Note that this is created as an org property value at the
|
||||||
|
top-most heading. The NAME is a short string for the name of the
|
||||||
|
asset, and VALUE is the initial value, probably an integer
|
||||||
|
value."
|
||||||
|
(interactive (list (read-string "Asset stat name: ")
|
||||||
|
(read-string "Initial asset stat value: ")))
|
||||||
|
(unless (s-blank? name)
|
||||||
|
(let ((stat (rpgdm-ironsworn--asset-stat-key name)))
|
||||||
|
(rpgdm-ironsworn-store-default-character-state stat value))))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn--asset-stat-alist (&optional stats)
|
||||||
|
"Return alist of all defined asset-related STATS."
|
||||||
|
(unless stats
|
||||||
|
(setq stats (rpgdm-ironsworn-current-character-state)))
|
||||||
|
(let* ((keys (thread-last stats
|
||||||
|
(hash-table-keys)
|
||||||
|
(--filter (string-prefix-p "asset-" (symbol-name it)))))
|
||||||
|
(names (-map 'rpgdm-ironsworn--asset-stat-name keys)))
|
||||||
|
(-zip names keys)))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn--asset-stat-choose ()
|
||||||
|
"Display all asset-related stats, allowing user to choose one.
|
||||||
|
Return the symbol, `asset-xyz."
|
||||||
|
(let* ((choices (rpgdm-ironsworn--asset-stat-alist))
|
||||||
|
(choice (completing-read "Choose Asset Stat: " choices)))
|
||||||
|
(alist-get choice choices 0 nil 'equal)))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn-asset-stat-adjust (stat)
|
||||||
|
"Adjust an asset-related STAT, after choosing that.
|
||||||
|
Note that the stat must have already been defined."
|
||||||
|
(interactive (list (rpgdm-ironsworn--asset-stat-choose)))
|
||||||
|
(rpgdm-ironsworn-adjust-stat stat))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn-asset-stat-show (stat)
|
||||||
|
"Display an asset STAT after selecting it from those created."
|
||||||
|
(interactive (list (rpgdm-ironsworn--asset-stat-choose)))
|
||||||
|
(message "%d" (rpgdm-ironsworn-character-stat stat)))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn--asset-stat-show-all (&optional stats)
|
||||||
|
"Return string showing all asset-related STATS and their values.
|
||||||
|
Note that if STATS is nil, the stats are acquired by calling
|
||||||
|
`rpgdm-ironsworn-current-character-state'."
|
||||||
|
(unless stats
|
||||||
|
(setq stats (rpgdm-ironsworn-current-character-state)))
|
||||||
|
|
||||||
|
(cl-flet ((convert (stat) (cons (car stat)
|
||||||
|
(rpgdm-ironsworn-character-stat (cdr stat)))))
|
||||||
|
(thread-last stats
|
||||||
|
(rpgdm-ironsworn--asset-stat-alist)
|
||||||
|
(-map #'convert)
|
||||||
|
(--map (format "%s: %s" (car it) (cdr it)))
|
||||||
|
(s-join " "))))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn-asset-stat-roll (stat modifier)
|
||||||
|
"Use an asset-related stat as a modifier for a standard roll."
|
||||||
|
(interactive (list (rpgdm-ironsworn--asset-stat-choose)
|
||||||
|
(read-string "Other modifier: ")))
|
||||||
|
(rpgdm-ironsworn-roll-stat stat modifier))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn--move-tuple (file)
|
(defun rpgdm-ironsworn--move-tuple (file)
|
||||||
"Return a list of a string representation of FILE, and FILE.
|
"Return a list of a string representation of FILE, and FILE.
|
||||||
The string representation is created by looking at the parent
|
The string representation is created by looking at the parent
|
||||||
|
@ -970,14 +1050,22 @@ You'll need to pick and choose what works and discard what doesn't."
|
||||||
("x" rpgdm-ironsworn-progress-delete "delete")
|
("x" rpgdm-ironsworn-progress-delete "delete")
|
||||||
("r" rpgdm-ironsworn-progress-roll "roll"))
|
("r" rpgdm-ironsworn-progress-roll "roll"))
|
||||||
|
|
||||||
|
(defhydra hydra-rpgdm-assets (:color blue)
|
||||||
|
"Progress Tracks"
|
||||||
|
("A" rpgdm-ironsworn-insert-character-asset "insert new asset")
|
||||||
|
("a" rpgdm-ironsworn-asset-stat-adjust "adjust asset stat")
|
||||||
|
("n" rpgdm-ironsworn-asset-stat-create "new asset stat")
|
||||||
|
("s" rpgdm-ironsworn-asset-stat-show "show")
|
||||||
|
("r" rpgdm-ironsworn-asset-stat-roll "roll asset as modifier"))
|
||||||
|
|
||||||
(defhydra hydra-rpgdm (:color blue :hint nil)
|
(defhydra hydra-rpgdm (:color blue :hint nil)
|
||||||
"
|
"
|
||||||
^Dice^ 0=d100 1=d10 6=d6 ^Roll/Adjust^ ^Oracles/Tables^ ^Moving/Editing^ ^Messages^
|
^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
|
_D_: Roll Dice _h_: Roll Shadow _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
|
_e_: Roll Edge _w_: Roll Wits _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
|
_r_: Roll Heart _p_/_a_: Progress/Assets _s_/_S_: Supply _O_: Load Oracles _N_/_W_: Narrow/Widen ⌘-k: ↑ Previous
|
||||||
_i_: Roll Iron _m_: Make Move _M_: Momentum _d_: Delve Actions _y_/_Y_: Yank/Move ⌘-j: ↓ Next "
|
_i_: Roll Iron _m_: Make Move _M_: Momentum _d_: Delve Actions _y_/_Y_: Yank/Move ⌘-j: ↓ Next "
|
||||||
("D" rpgdm-ironsworn-roll)
|
("D" rpgdm-ironsworn-roll)
|
||||||
("z" rpgdm-ironsworn-oracle) ("Z" rpgdm-yes-and-50/50)
|
("z" rpgdm-ironsworn-oracle) ("Z" rpgdm-yes-and-50/50)
|
||||||
|
|
||||||
|
@ -1000,6 +1088,7 @@ You'll need to pick and choose what works and discard what doesn't."
|
||||||
|
|
||||||
("d" hydra-rpgdm-delve/body)
|
("d" hydra-rpgdm-delve/body)
|
||||||
("p" hydra-rpgdm-progress/body)
|
("p" hydra-rpgdm-progress/body)
|
||||||
|
("a" hydra-rpgdm-assets/body)
|
||||||
|
|
||||||
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
|
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
|
||||||
("K" scroll-down :color pink) ("J" scroll-up :color pink)
|
("K" scroll-down :color pink) ("J" scroll-up :color pink)
|
||||||
|
@ -1026,6 +1115,16 @@ number, but doesn't have to be."
|
||||||
(setq value (number-to-string value)))
|
(setq value (number-to-string value)))
|
||||||
(org-set-property prop value)))
|
(org-set-property prop value)))
|
||||||
|
|
||||||
|
(defun rpgdm-ironsworn-store-default-character-state (stat value)
|
||||||
|
"Store the VALUE of a character's STAT in the top-level org tree property.
|
||||||
|
Note that STAT should be a symbol, like `supply' and VALUE should be a
|
||||||
|
number, but doesn't have to be."
|
||||||
|
(save-excursion
|
||||||
|
(org-up-heading)
|
||||||
|
(while (> (org-heading-level) 1)
|
||||||
|
(org-up-heading))
|
||||||
|
(rpgdm-ironsworn-store-character-state stat value)))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn--property-p (prop)
|
(defun rpgdm-ironsworn--property-p (prop)
|
||||||
"Given a symbol PROP, return non-nil if it is an ironsworn keyword.
|
"Given a symbol PROP, return non-nil if it is an ironsworn keyword.
|
||||||
Specifically, does it begin with `:IRONSWORN-'"
|
Specifically, does it begin with `:IRONSWORN-'"
|
||||||
|
@ -1054,6 +1153,12 @@ Return 0 if not at a heading, or above first headline."
|
||||||
level-str
|
level-str
|
||||||
0))
|
0))
|
||||||
|
|
||||||
|
(defun org-up-heading ()
|
||||||
|
"Move the point to next parent heading, unless already at the top-level."
|
||||||
|
(if (= 0 (org-heading-level))
|
||||||
|
(outline-up-heading 0)
|
||||||
|
(outline-up-heading 1)))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn--current-character-state (results)
|
(defun rpgdm-ironsworn--current-character-state (results)
|
||||||
"Recursive helper to insert current header properties in RESULTS.
|
"Recursive helper to insert current header properties in RESULTS.
|
||||||
Calls itself if it is not looking at the top level header in the
|
Calls itself if it is not looking at the top level header in the
|
||||||
|
@ -1065,20 +1170,21 @@ precendence over similar settings in higher headers."
|
||||||
|
|
||||||
(defun value-convert (value)
|
(defun value-convert (value)
|
||||||
(if (string-match (rx bos (one-or-more digit) eos) value)
|
(if (string-match (rx bos (one-or-more digit) eos) value)
|
||||||
(string-to-number value)
|
(string-to-number value)
|
||||||
value))
|
value))
|
||||||
|
|
||||||
(let ((props (org-element--get-node-properties)))
|
(let ((props (org-element--get-node-properties)))
|
||||||
(loop for (k v) on props by (function cddr) do
|
(cl-loop for (k v) on props by (function cddr) do
|
||||||
;; If key is ironsworn property, but isn't in the table...
|
;; If key is ironsworn property, but isn't in the table...
|
||||||
(when (rpgdm-ironsworn--property-p k)
|
(when (rpgdm-ironsworn--property-p k)
|
||||||
(let ((key (key-convert k))
|
(let ((key (key-convert k))
|
||||||
(val (value-convert v)))
|
(val (value-convert v)))
|
||||||
(unless (gethash key results)
|
(message "Found %s : %s" key val)
|
||||||
(puthash key val results)))))
|
(unless (gethash key results)
|
||||||
|
(puthash key val results)))))
|
||||||
|
|
||||||
(unless (= (org-heading-level) 1)
|
(unless (= (org-heading-level) 1)
|
||||||
(org-up-element)
|
(org-up-heading)
|
||||||
(rpgdm-ironsworn--current-character-state results))))
|
(rpgdm-ironsworn--current-character-state results))))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn-current-character-state ()
|
(defun rpgdm-ironsworn-current-character-state ()
|
||||||
|
@ -1088,14 +1194,15 @@ lower levels of the tree headings take precedence."
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(let ((results (make-hash-table :test 'str-or-keys)))
|
(let ((results (make-hash-table :test 'str-or-keys)))
|
||||||
(unless (org-at-heading-p)
|
(unless (org-at-heading-p)
|
||||||
(org-up-element))
|
(org-up-heading))
|
||||||
|
|
||||||
;; Put the lowest heading title in the results hashtable:
|
;; Put the lowest heading title in the results hashtable:
|
||||||
(puthash 'title (thread-first
|
(puthash 'title (thread-first
|
||||||
(org-element-at-point)
|
(org-element-at-point)
|
||||||
(second)
|
(second)
|
||||||
(plist-get :raw-value))
|
(plist-get :raw-value))
|
||||||
results)
|
results)
|
||||||
|
(message "Hash: %s" results)
|
||||||
(rpgdm-ironsworn--current-character-state results)
|
(rpgdm-ironsworn--current-character-state results)
|
||||||
results)))
|
results)))
|
||||||
|
|
||||||
|
@ -1128,7 +1235,7 @@ and the current progress of the track."
|
||||||
(defun rpgdm-ironsworn--mark-progress-track (str)
|
(defun rpgdm-ironsworn--mark-progress-track (str)
|
||||||
"Given a progress track's name, STR, update its progress mark."
|
"Given a progress track's name, STR, update its progress mark."
|
||||||
(let ((props (org-element--get-node-properties)))
|
(let ((props (org-element--get-node-properties)))
|
||||||
(loop for (k v) on props by (function cddr) do
|
(cl-loop for (k v) on props by (function cddr) do
|
||||||
(when (rpgdm-ironsworn--progress-p k)
|
(when (rpgdm-ironsworn--progress-p k)
|
||||||
(-let* (((label level progress) (rpgdm-ironsworn--progress-values v)))
|
(-let* (((label level progress) (rpgdm-ironsworn--progress-values v)))
|
||||||
(when (equal str label)
|
(when (equal str label)
|
||||||
|
@ -1139,7 +1246,7 @@ and the current progress of the track."
|
||||||
|
|
||||||
;; Did not find a match at this level, let's pop up one and try again:
|
;; Did not find a match at this level, let's pop up one and try again:
|
||||||
(unless (= (org-heading-level) 1)
|
(unless (= (org-heading-level) 1)
|
||||||
(org-up-element)
|
(org-up-heading)
|
||||||
(rpgdm-ironsworn--mark-progress-track str)))
|
(rpgdm-ironsworn--mark-progress-track str)))
|
||||||
|
|
||||||
(defun rpgdm-ironsworn-mark-progress-track (label)
|
(defun rpgdm-ironsworn-mark-progress-track (label)
|
||||||
|
|
Loading…
Reference in a new issue