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:
Howard Abrams 2022-03-23 20:44:36 -07:00
parent 474cdd0f9f
commit 7a10f7ab1f
3 changed files with 258 additions and 96 deletions

View file

@ -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=:
@ -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
@ -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):
@ -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.
@ -1762,6 +1763,16 @@ Since we need to know if we are at the top-level, we could have a function, =org
0)) 0))
#+END_SRC #+END_SRC
Since [[help:org-up-element][org-up-element]]s behavior has changed, and [[help:outline-up-heading][outline-up-heading]] doesnt 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
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:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -1780,16 +1791,17 @@ Enough chit-chat, let's write this function. While we are at it, let's convert t
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)))
(message "Found %s : %s" key val)
(unless (gethash key results) (unless (gethash key results)
(puthash key val 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 ()
@ -1799,7 +1811,7 @@ 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
@ -1807,6 +1819,7 @@ Enough chit-chat, let's write this function. While we are at it, let's convert t
(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)))
#+END_SRC #+END_SRC
@ -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

View file

@ -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"))

View file

@ -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)
@ -278,7 +277,7 @@ 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)
@ -289,7 +288,8 @@ Health: %s Spirit: %s Supply: %s Momentum: %d"
(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.
@ -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,13 +1050,21 @@ 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
@ -1069,16 +1174,17 @@ precendence over similar settings in higher headers."
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)))
(message "Found %s : %s" key val)
(unless (gethash key results) (unless (gethash key results)
(puthash key val 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,7 +1194,7 @@ 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
@ -1096,6 +1202,7 @@ lower levels of the tree headings take precedence."
(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)