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]].
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
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.
#+BEGIN_SRC emacs-lisp
(defvar rpgdm-ironsworn-project (file-name-directory load-file-name)
"The root directory to the rpgdm-ironsworn project.")
(defvar rpgdm-ironsworn-project (file-name-directory load-file-name)
"The root directory to the rpgdm-ironsworn project.")
#+END_SRC
** 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).
@ -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."
(cl-flet* ((convert (str) (s-replace "-" " " str))
(uppity (str) (s-titleize (convert str))))
(when (string-match (rx (one-or-more any)
"/"
(group (one-or-more (not "/"))) ; parent directory
"/"
(group (one-or-more (not "/"))) ; base filename
".org")
(when (string-match (rx (one-or-more any) "/"
;; parent directory
(group (one-or-more (not "/"))) "/"
;; base filename
(group (one-or-more (not "/"))) ".org")
filename)
(format "%s :: %s"
(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."
(unless number
(setq number 3))
(loop for x from 1 to number
(cl-loop for x from 1 to number
collect (seq-random-elt asset-filenames)))
#+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.
See `rpgdm-ironsworn-character-display'."
(let* ((value (gethash stat character))
(s-val (number-to-string value))
(color (cond
((< value 1) "red")
((< value 3) "orange")
((< value 4) "yellow")
(t "green"))))
(s-val (number-to-string value))
(color (cond
((< value 1) "red")
((< value 3) "orange")
((< value 4) "yellow")
(t "green"))))
(propertize s-val 'face `(:foreground ,color))))
(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
(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))))
#+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.
If CHARACTER doesn't refer to a character hash, then this calls
the `rpgdm-ironsworn-current-character-state' function."
(when (null character)
(unless character
(setq character (rpgdm-ironsworn-current-character-state)))
(gethash stat character 1))
(gethash stat character 0))
#+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):
@ -709,7 +710,7 @@ The =rpgdm-ironsworn-adjust-stat= function takes one of the four stats, like =
#+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."
If the STAT isn't found, returns DEFAULT."
(let* ((tuple (rpgdm-ironsworn--read-stat stat))
(curr (rpgdm-ironsworn-character-stat stat))
(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))
#+END_SRC
** 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
(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.
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
- Move up to that header's parent
- 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:
#+BEGIN_SRC emacs-lisp
(defun org-heading-level ()
"Return heading level of the element at the point.
Return 0 if not at a heading, or above first headline."
(if-let ((level-str (org-element-property :level (org-element-at-point))))
level-str
0))
(defun org-heading-level ()
"Return heading level of the element at the point.
Return 0 if not at a heading, or above first headline."
(if-let ((level-str (org-element-property :level (org-element-at-point))))
level-str
0))
#+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:
@ -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)
(if (string-match (rx bos (one-or-more digit) eos) value)
(string-to-number value)
value))
(string-to-number value)
value))
(let ((props (org-element--get-node-properties)))
(loop for (k v) on props by (function cddr) do
;; If key is ironsworn property, but isn't in the table...
(when (rpgdm-ironsworn--property-p k)
(let ((key (key-convert k))
(val (value-convert v)))
(unless (gethash key results)
(puthash key val results)))))
(cl-loop for (k v) on props by (function cddr) do
;; If key is ironsworn property, but isn't in the table...
(when (rpgdm-ironsworn--property-p k)
(let ((key (key-convert k))
(val (value-convert v)))
(message "Found %s : %s" key val)
(unless (gethash key results)
(puthash key val results)))))
(unless (= (org-heading-level) 1)
(org-up-element)
(rpgdm-ironsworn--current-character-state results))))
(org-up-heading)
(rpgdm-ironsworn--current-character-state results))))
(defun rpgdm-ironsworn-current-character-state ()
"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
(let ((results (make-hash-table :test 'str-or-keys)))
(unless (org-at-heading-p)
(org-up-element))
(org-up-heading))
;; Put the lowest heading title in the results hashtable:
(puthash 'title (thread-first
(org-element-at-point)
(second)
(plist-get :raw-value))
results)
(rpgdm-ironsworn--current-character-state results)
results)))
(puthash 'title (thread-first
(org-element-at-point)
(second)
(plist-get :raw-value))
results)
(message "Hash: %s" results)
(rpgdm-ironsworn--current-character-state results)
results)))
#+END_SRC
*** Reading Progress Tracks from Org Files
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.
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
(defun rpgdm-ironsworn--mark-progress-track (str)
"Given a progress track's name, STR, update its progress mark."
(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)
(-let* (((label level progress) (rpgdm-ironsworn--progress-values v)))
(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:
(unless (= (org-heading-level) 1)
(org-up-element)
(org-up-heading)
(rpgdm-ironsworn--mark-progress-track str)))
#+END_SRC

View file

@ -67,6 +67,48 @@
(cl-letf (((symbol-function 'read-string) (lambda (s) "go back")))
(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 ()
(let ((file "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."
(cl-flet* ((convert (str) (s-replace "-" " " str))
(uppity (str) (s-titleize (convert str))))
(when (string-match (rx (one-or-more any)
"/"
(group (one-or-more (not "/"))) ; parent directory
"/"
(group (one-or-more (not "/"))) ; base filename
".org")
(when (string-match (rx (one-or-more any) "/"
;; parent directory
(group (one-or-more (not "/"))) "/"
;; base filename
(group (one-or-more (not "/"))) ".org")
filename)
(format "%s :: %s"
(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."
(unless number
(setq number 3))
(loop for x from 1 to number
(cl-loop for x from 1 to number
collect (seq-random-elt asset-filenames)))
(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.
See `rpgdm-ironsworn-character-display'."
(let* ((value (gethash stat character))
(s-val (number-to-string value))
(color (cond
((< value 1) "red")
((< value 3) "orange")
((< value 4) "yellow")
(t "green"))))
(s-val (number-to-string value))
(color (cond
((< value 1) "red")
((< value 3) "orange")
((< value 4) "yellow")
(t "green"))))
(propertize s-val 'face `(:foreground ,color))))
(defun rpgdm-ironsworn-character-display ()
@ -278,18 +277,19 @@ See `rpgdm-ironsworn-character-display'."
(interactive)
(let ((character (rpgdm-ironsworn-current-character-state)))
(rpgdm-message "Edge: %d Heart: %d Iron: %d Shadow: %d Wits: %d
Health: %s Spirit: %s Supply: %s Momentum: %d"
(rpgdm-ironsworn-character-stat 'edge character)
(rpgdm-ironsworn-character-stat 'heart character)
(rpgdm-ironsworn-character-stat 'iron character)
(rpgdm-ironsworn-character-stat 'shadow character)
(rpgdm-ironsworn-character-stat 'wits character)
Health: %s Spirit: %s Supply: %s Momentum: %d %s"
(rpgdm-ironsworn-character-stat 'edge character)
(rpgdm-ironsworn-character-stat 'heart character)
(rpgdm-ironsworn-character-stat 'iron character)
(rpgdm-ironsworn-character-stat 'shadow character)
(rpgdm-ironsworn-character-stat 'wits character)
(rpgdm-ironsworn--display-stat 'health character)
(rpgdm-ironsworn--display-stat 'spirit character)
(rpgdm-ironsworn--display-stat 'supply character)
(rpgdm-ironsworn--display-stat 'health character)
(rpgdm-ironsworn--display-stat 'spirit 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)
"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
(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))))
@ -309,9 +310,9 @@ Health: %s Spirit: %s Supply: %s Momentum: %d"
"Return integer value associated with a character's STAT.
If CHARACTER doesn't refer to a character hash, then this calls
the `rpgdm-ironsworn-current-character-state' function."
(when (null character)
(unless character
(setq character (rpgdm-ironsworn-current-character-state)))
(gethash stat character 1))
(gethash stat character 0))
(defun rpgdm-ironsworn--read-stat (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)
"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))
(curr (rpgdm-ironsworn-character-stat stat))
(oper (first tuple))
@ -424,6 +425,85 @@ the default for that stat."
(rpgdm-ironsworn-progress-roll
(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)
"Return a list of a string representation of FILE, and FILE.
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")
("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)
"
^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
_i_: Roll Iron _m_: Make Move _M_: Momentum _d_: Delve Actions _y_/_Y_: Yank/Move -j: Next "
_D_: Roll Dice _h_: Roll Shadow _l_/_L_: Health _z_/_Z_: Yes/No Oracle _o_: Links -h: Show Stats
_e_: Roll Edge _w_: Roll Wits _t_/_T_: Spirit _c_/_C_: Show Oracle _J_/_K_: Page up/dn -l: Last Results
_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 "
("D" rpgdm-ironsworn-roll)
("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)
("p" hydra-rpgdm-progress/body)
("a" hydra-rpgdm-assets/body)
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
("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)))
(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)
"Given a symbol PROP, return non-nil if it is an ironsworn keyword.
Specifically, does it begin with `:IRONSWORN-'"
@ -1054,6 +1153,12 @@ Return 0 if not at a heading, or above first headline."
level-str
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)
"Recursive helper to insert current header properties in RESULTS.
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)
(if (string-match (rx bos (one-or-more digit) eos) value)
(string-to-number value)
(string-to-number value)
value))
(let ((props (org-element--get-node-properties)))
(loop for (k v) on props by (function cddr) do
;; If key is ironsworn property, but isn't in the table...
(when (rpgdm-ironsworn--property-p k)
(let ((key (key-convert k))
(val (value-convert v)))
(unless (gethash key results)
(puthash key val results)))))
(cl-loop for (k v) on props by (function cddr) do
;; If key is ironsworn property, but isn't in the table...
(when (rpgdm-ironsworn--property-p k)
(let ((key (key-convert k))
(val (value-convert v)))
(message "Found %s : %s" key val)
(unless (gethash key results)
(puthash key val results)))))
(unless (= (org-heading-level) 1)
(org-up-element)
(org-up-heading)
(rpgdm-ironsworn--current-character-state results))))
(defun rpgdm-ironsworn-current-character-state ()
@ -1088,14 +1194,15 @@ lower levels of the tree headings take precedence."
(save-excursion
(let ((results (make-hash-table :test 'str-or-keys)))
(unless (org-at-heading-p)
(org-up-element))
(org-up-heading))
;; Put the lowest heading title in the results hashtable:
(puthash 'title (thread-first
(org-element-at-point)
(second)
(plist-get :raw-value))
results)
(org-element-at-point)
(second)
(plist-get :raw-value))
results)
(message "Hash: %s" results)
(rpgdm-ironsworn--current-character-state results)
results)))
@ -1128,7 +1235,7 @@ and the current progress of the track."
(defun rpgdm-ironsworn--mark-progress-track (str)
"Given a progress track's name, STR, update its progress mark."
(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)
(-let* (((label level progress) (rpgdm-ironsworn--progress-values v)))
(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:
(unless (= (org-heading-level) 1)
(org-up-element)
(org-up-heading)
(rpgdm-ironsworn--mark-progress-track str)))
(defun rpgdm-ironsworn-mark-progress-track (label)