Add a number of Delve-specific moves

Combining the themes and domains to help make less table-consulting.
This commit is contained in:
Howard Abrams 2022-02-22 21:31:55 -08:00
parent bea1a2a6b8
commit e53d9f05a9
7 changed files with 636 additions and 275 deletions

View file

@ -19,7 +19,7 @@ After listening to the author, [[https://twitter.com/ShawnTomkin][Shawn Tomkin]]
* 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=:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp :tangle no
(add-to-list 'load-path (expand-file-name "~/other/rpgdm")) (add-to-list 'load-path (expand-file-name "~/other/rpgdm"))
(add-to-list 'load-path (expand-file-name "~/other/rpgdm-ironsworn")) (add-to-list 'load-path (expand-file-name "~/other/rpgdm-ironsworn"))
#+END_SRC #+END_SRC
@ -136,6 +136,26 @@ Again, the UI will attempt to update all of these values, so you don't need to c
Details? Did someone say details? Let's talk about the code ... all the code that makes this work. Details? Did someone say details? Let's talk about the code ... all the code that makes this work.
* Code * Code
#+BEGIN_SRC emacs-lisp :exports none
;;; rpgdm-ironsworn -- Functions for integrating Ironsworn with Org
;;
;; Copyright (C) 2020 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams
;; Created: September 18, 2020
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; This file is conspicuously absent from commentary or even
;; comments. This is because this file is created from tangling
;; the README.org file in this directory.
;;
;;; Code:
#+END_SRC
To begin, we'll need the [[https://gitlab.com/howardabrams/emacs-rpgdm][rpgdm project]] cloned in the =load-path= variable so that we can load it simply by calling: To begin, we'll need the [[https://gitlab.com/howardabrams/emacs-rpgdm][rpgdm project]] cloned in the =load-path= variable so that we can load it simply by calling:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -146,7 +166,7 @@ We also need the name of the directory for this project, so that we can load tab
#+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). You always three possible values: 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). You always three possible values:
@ -161,50 +181,57 @@ When we roll, I want one of those three results printed, but in different colors
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--results (action modifier one-challenge two-challenge (defun rpgdm-ironsworn--results (action modifier one-challenge two-challenge
&optional momentum) &optional momentum)
"Return formatted string for an Ironsworn dice roll results.
The ACTION is the d6 which is added to the MODIFIER (which can
have character attribute values as well as any bonuses. The sum
is compared to the two d10, ONE-CHALLENGE and TWO-CHALLENGE.
The optional MOMENTUM can be specified to add a message that the
use could burn that in order to improve the roll."
(unless momentum (unless momentum
(setq momentum 0)) (setq momentum 0))
(cl-flet ((strong-p (value dice1 dice2) (and (> value dice1) (> value dice2))) (cl-flet ((strong-p (value dice1 dice2) (and (> value dice1) (> value dice2)))
(weak-p (value dice1 dice2) (or (> value dice1) (> value dice2))) (weak-p (value dice1 dice2) (or (> value dice1) (> value dice2)))
(miss-p (value dice1 dice2) (and (<= value dice1) (<= value dice2))) (miss-p (value dice1 dice2) (and (<= value dice1) (<= value dice2)))
(faded (str) (propertize str 'face '(:foreground "#888"))) (faded (str) (propertize str 'face '(:foreground "#888")))
(noted (str) (propertize str 'face '(:foreground "light blue"))) (noted (str) (propertize str 'face '(:foreground "light blue")))
(strong (str) (propertize str 'face '(:foreground "green"))) (strong (str) (propertize str 'face '(:foreground "green")))
(weak (str) (propertize str 'face '(:foreground "yellow"))) (weak (str) (propertize str 'face '(:foreground "yellow")))
(interest (str) (propertize str 'face '(:foreground "orange"))) (interest (str) (propertize str 'face '(:foreground "orange")))
(miss (str) (propertize str 'face '(:foreground "red")))) (miss (str) (propertize str 'face '(:foreground "red"))))
(let* ((action-results (+ action modifier)) (let* ((action-results (+ action modifier))
(str-results (cond (str-results (cond
((strong-p action-results one-challenge two-challenge) ((strong-p action-results one-challenge two-challenge)
(strong "Strong hit")) (strong "Strong hit"))
((weak-p action-results one-challenge two-challenge) ((weak-p action-results one-challenge two-challenge)
(weak "Weak hit")) (weak "Weak hit"))
(t (miss "Miss")))) (t (miss "Miss"))))
(burn-msg (if (> momentum action-results) (burn-msg (if (> momentum action-results)
(cond (cond
((and (strong-p momentum one-challenge two-challenge) ((and (strong-p momentum one-challenge two-challenge)
(not (strong-p action-results one-challenge two-challenge))) (not (strong-p action-results one-challenge two-challenge)))
(concat " -- Burn momentum for a " (strong "Strong hit"))) (concat " -- Burn momentum for a " (strong "Strong hit")))
((and (weak-p momentum one-challenge two-challenge) ((and (weak-p momentum one-challenge two-challenge)
(miss-p action-results one-challenge two-challenge)) (miss-p action-results one-challenge two-challenge))
(concat " -- Burn momentum for a " (weak "Weak hit"))) (concat " -- Burn momentum for a " (weak "Weak hit")))
(t "")) (t ""))
"")) ""))
(matched-msg (if (= one-challenge two-challenge) (matched-msg (if (= one-challenge two-challenge)
(concat " ← " (interest "Create a Twist")) (concat " ← " (interest "Create a Twist"))
""))) "")))
(format "%s %s %d %s%d %s %d%s %s %d %s %d%s%s" (format "%s %s %d %s%d %s %d%s %s %d %s %d%s%s"
str-results (faded "::") str-results (faded "::")
(+ action modifier) (faded "(") (+ action modifier) (faded "(")
action (faded "+") action (faded "+")
modifier (faded ")") modifier (faded ")")
(noted "→") (noted "→")
one-challenge (faded "/") one-challenge (faded "/")
two-challenge two-challenge
matched-msg burn-msg)))) matched-msg burn-msg))))
#+END_SRC #+END_SRC
So the following messages, given various /rolls/ should cover those possibilities with text properties: So the following messages, given various /rolls/ should cover those possibilities with text properties:
@ -230,15 +257,18 @@ The basic interface will query for a modifer, roll all three dice, and then disp
#+BEGIN_SRC emacs-lisp :results silent #+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-ironsworn-roll (modifier &optional momentum) (defun rpgdm-ironsworn-roll (modifier &optional momentum)
"Display a Hit/Miss message based on comparing a d6 action "Display a Hit/Miss message based on an Ironsworn roll.
roll (added to MODIFIER) vs. two d10 challenge dice." Done by rolling and comparing a d6 action roll (summed with
MODIFIER) vs two d10 challenge dice. If given, the MOMENTUM may
trigger a message to the user that they can burn that for better
results."
(interactive "nModifier: ") (interactive "nModifier: ")
(let ((one-challenge (rpgdm--roll-die 10)) (let ((one-challenge (rpgdm--roll-die 10))
(two-challenge (rpgdm--roll-die 10)) (two-challenge (rpgdm--roll-die 10))
(action-roll (rpgdm--roll-die 6))) (action-roll (rpgdm--roll-die 6)))
(rpgdm-message (rpgdm-ironsworn--results action-roll modifier (rpgdm-message (rpgdm-ironsworn--results action-roll modifier
one-challenge two-challenge one-challenge two-challenge
momentum)))) momentum))))
#+END_SRC #+END_SRC
** Character Information ** Character Information
@ -250,15 +280,17 @@ We assume you have created an org-file, and the /template/ will just append some
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--new-character-template (name) (defun rpgdm-ironsworn--new-character-template (name)
"Insert a basic Ironsworn template at the end of the current buffer." "Insert basic Ironsworn template at the end of the current buffer.
A header is created with NAME, but if this is an empty string,
a random name is generated for the purposes of the template."
(when (s-blank? name) (when (s-blank? name)
(setq name (rpgdm-tables-choose "names-ironlander"))) (setq name (rpgdm-tables-choose "names-ironlander")))
(let ((frmt (seq-random-elt '("* The Adventures of %s" (let ((frmt (seq-random-elt '("* The Adventures of %s"
"* The Journeys of %s" "* The Journeys of %s"
"* %s, an Epic Saga" "* %s, an Epic Saga"
"* The Epic of %s" "* The Epic of %s"
"* Travels of %s")))) "* Travels of %s"))))
(goto-char (point-max)) (goto-char (point-max))
(insert "# Local Variables: (insert "# Local Variables:
# eval: (progn (require 'rpgdm-ironsworn) (rpgdm-mode)) # eval: (progn (require 'rpgdm-ironsworn) (rpgdm-mode))
@ -299,7 +331,7 @@ Let's fill in those asset values /lazily/. The first time we call this function,
(defun rpgdm-ironsworn-character-assets () (defun rpgdm-ironsworn-character-assets ()
"Return an association list of all available assets. "Return an association list of all available assets.
The `car' is a label for the asset, and the `cdr' is the filename The `car' is a label for the asset, and the `cdr' is the filename
that contains the text. The first time we call this, we read from that contains the text. The first time we call this, we read from
the `assets' directory, otherwise, we return a cached version." the `assets' directory, otherwise, we return a cached version."
(unless rpgdm-ironsworn-character-assets (unless rpgdm-ironsworn-character-assets
(let ((asset-files (thread-first rpgdm-ironsworn-project (let ((asset-files (thread-first rpgdm-ironsworn-project
@ -328,7 +360,7 @@ Let the user choose an asset and insert it into the file at the current point. W
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-insert-character-asset (asset) (defun rpgdm-ironsworn-insert-character-asset (asset)
"Choose and insert the contents of an asset in the current buffer." "Choose and insert the contents of an ASSET in the current buffer."
(interactive (list (rpgdm-ironsworn--pick-character-asset))) (interactive (list (rpgdm-ironsworn--pick-character-asset)))
(let ((file (if (consp asset) (cdr asset) asset))) (let ((file (if (consp asset) (cdr asset) asset)))
(insert-file-contents file nil) (insert-file-contents file nil)
@ -343,7 +375,7 @@ When you start a character, you choose three assets, but what if we choose them
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--good-character-assets (asset-files) (defun rpgdm-ironsworn--good-character-assets (asset-files)
"Return ASSET-FILES if all given are _good enough_. "Return ASSET-FILES if all given are _good enough_.
That is, all are unique, only one companion, etc." That is, all are unique, only one companion, etc."
(cl-flet ((companion-p (entry) (cl-flet ((companion-p (entry)
(when (consp entry) (when (consp entry)
(setq entry (cdr entry))) (setq entry (cdr entry)))
@ -412,6 +444,8 @@ Now we have a function that inserts the contents of three randomly chosen assets
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-random-character-assets (&optional number-of-assets) (defun rpgdm-ironsworn-random-character-assets (&optional number-of-assets)
"Return the file names of NUMBER-OF-ASSETS from the `assets' directory.
The chosen assets are _good_ in that they won't have duplicates, etc."
(interactive "nHow many random assets should we insert? ") (interactive "nHow many random assets should we insert? ")
(dolist (file (rpgdm-ironsworn--random-character-assets number-of-assets)) (dolist (file (rpgdm-ironsworn--random-character-assets number-of-assets))
(rpgdm-ironsworn-insert-character-asset file))) (rpgdm-ironsworn-insert-character-asset file)))
@ -434,12 +468,13 @@ This function will query the user for all of the stats and other properties that
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--new-character-stats () (defun rpgdm-ironsworn--new-character-stats ()
"Query the user for a new character's stats, and add them as "Insert character stats after querying user for them.
properties using the `rpgdm-ironsworn-store-character-state' and Note: The stats are added as properties using the
the `rpgdm-ironsworn-progress-create' functions." `rpgdm-ironsworn-store-character-state' and the
`rpgdm-ironsworn-progress-create' functions."
(dolist (stat '(edge heart iron shadow wits)) (dolist (stat '(edge heart iron shadow wits))
(rpgdm-ironsworn-store-character-state stat (rpgdm-ironsworn-store-character-state stat
(read-string (format "What '%s' stat: " stat)))) (read-string (format "What '%s' stat: " stat))))
(dolist (stat '(health spirit supply)) (dolist (stat '(health spirit supply))
(rpgdm-ironsworn-store-character-state stat 5)) (rpgdm-ironsworn-store-character-state stat 5))
@ -458,13 +493,15 @@ Perhaps the clearest approach is to do both, create two process functions, and t
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--new-character-stats-first (&optional name) (defun rpgdm-ironsworn--new-character-stats-first (&optional name)
"Insert a new character template, query for the stats, then insert assets." "Insert a new character template for character, NAME.
The character stats are first queried, and then assets inserted."
(rpgdm-ironsworn--new-character-template name) (rpgdm-ironsworn--new-character-template name)
(rpgdm-ironsworn--new-character-stats) (rpgdm-ironsworn--new-character-stats)
(rpgdm-ironsworn--new-character-assets)) (rpgdm-ironsworn--new-character-assets))
(defun rpgdm-ironsworn--new-character-assets-first (&optional name) (defun rpgdm-ironsworn--new-character-assets-first (&optional name)
"Insert a new character template, insert assets then query for the stats." "Insert a new character template for character, NAME.
The assets are inserted first, and then character stats are queried."
(rpgdm-ironsworn--new-character-template name) (rpgdm-ironsworn--new-character-template name)
;; Saving and restoring point, means the properties should be in the ;; Saving and restoring point, means the properties should be in the
;; correct, top-level position. ;; correct, top-level position.
@ -474,13 +511,15 @@ Perhaps the clearest approach is to do both, create two process functions, and t
(defun rpgdm-ironsworn-new-character (name order) (defun rpgdm-ironsworn-new-character (name order)
"Interactively query the user for a new character's attribute. "Interactively query the user for a new character's attribute.
This function _appends_ this information to the current buffer, The NAME is the character's name, and ORDER determines how the
which should be using the `org-mode' major mode." template will generate and query the user for the rest of the data.
This function _appends_ this information to the current buffer,
which should be using the `org-mode' major mode."
(interactive (list (interactive (list
(read-string "What is the new character's name? ") (read-string "What is the new character's name? ")
(completing-read "What order should we build this? " '("Statistics first" "Assets first")))) (completing-read "What order should we build this? " '("Statistics first" "Assets first"))))
(if (equal order "Assets first") (if (equal order "Assets first")
(rpgdm-ironsworn--new-character-assets-first) (rpgdm-ironsworn--new-character-assets-first)
(rpgdm-ironsworn--new-character-stats-first)) (rpgdm-ironsworn--new-character-stats-first))
(message "Alright, the template is complete. Edit away!" name)) (message "Alright, the template is complete. Edit away!" name))
#+END_SRC #+END_SRC
@ -490,13 +529,15 @@ Sure, you could open up the appropriate drawer to see a character's stats, but
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--display-stat (stat character) (defun rpgdm-ironsworn--display-stat (stat character)
"Colorized the STAT from a CHARACTER hash containing it.
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 ()
@ -505,24 +546,24 @@ Sure, you could open up the appropriate drawer to see a character's stats, but
(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"
(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))))
#+END_SRC #+END_SRC
*** Retrieving Character Stats *** Retrieving Character Stats
We need an /internal representation/ of a character using a hash table of the attributes and other stats. One key feature is that I want to be able to look up a stat by either symbol or string, e.g. ='edge= or ="edge"= or even =:edge=. For this, I define a /comparator/, er, a Lisp test that uses a function to convert to a common format, a string: We need an /internal representation/ of a character using a hash table of the attributes and other stats. One key feature is that I want to be able to look up a stat by either symbol or string, e.g. ='edge= or ="edge"= or even =:edge=. For this, I define a /comparator/, er, a Lisp test that uses a function to convert to a common format, a string:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-to-string (a) (defun rpgdm-ironsworn-to-string (a)
"Return a lowercase string from either a string, keyword or symbol." "Return a lowercase string from either A, a string, keyword or symbol."
(downcase (downcase
(cond (cond
((keywordp a) (substring (symbol-name a) 1)) ((keywordp a) (substring (symbol-name a) 1))
@ -541,7 +582,9 @@ And a help function to retrieve the stats of the character is just a wrapper aro
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-character-stat (stat &optional character) (defun rpgdm-ironsworn-character-stat (stat &optional character)
"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
the `rpgdm-ironsworn-current-character-state' function."
(when (null character) (when (null character)
(setq character (rpgdm-ironsworn-current-character-state))) (setq character (rpgdm-ironsworn-current-character-state)))
(gethash stat character 1)) (gethash stat character 1))
@ -561,9 +604,10 @@ We need to modify /some/ of the stored values, like =health= and =supply=:
#+BEGIN_SRC emacs-lisp :results silent #+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-ironsworn-adjust-stat (stat adj &optional default) (defun rpgdm-ironsworn-adjust-stat (stat adj &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."
(let* ((curr (rpgdm-ironsworn-character-stat stat)) (let* ((curr (rpgdm-ironsworn-character-stat stat))
(new (+ curr adj))) (new (+ curr adj)))
(rpgdm-ironsworn-store-character-state stat new))) (rpgdm-ironsworn-store-character-state stat new)))
(defun rpgdm-ironsworn-adjust-health (health-adj) (defun rpgdm-ironsworn-adjust-health (health-adj)
@ -655,18 +699,21 @@ The [[file:moves][moves]] directory contains one org file for each move. These f
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--move-tuple (file) (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
directory and file name."
(let* ((regx (rx "moves/" (let* ((regx (rx "moves/"
(group (one-or-more (not "/"))) (group (one-or-more (not "/")))
"/" "/"
(group (one-or-more (not "."))) (group (one-or-more (not ".")))
".org" eol)) ".org" eol))
(mtch (string-match regx file)) (mtch (string-match regx file))
(type (thread-last file (type (thread-last file
(match-string 1) (match-string 1)
(s-titleize))) (s-titleize)))
(name (thread-last file (name (thread-last file
(match-string 2) (match-string 2)
(s-replace-regexp "-" " ")))) (s-replace-regexp "-" " "))))
(list (format "%s :: %s" type name) file))) (list (format "%s :: %s" type name) file)))
#+END_SRC #+END_SRC
@ -685,22 +732,24 @@ And let's verify the format:
Once I read the list of moves, I want to /cache/ it, using a poor-person's /memoize/ feature: Once I read the list of moves, I want to /cache/ it, using a poor-person's /memoize/ feature:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defvar rpgdm-ironsworn-moves () "A list of tuples of the move and the file containing its goodness.") (defvar rpgdm-ironsworn-moves ()
"A list of tuples of the move and the file containing its goodness.")
#+END_SRC #+END_SRC
Oh, one issue... how do I know where the data files for the moves are? Oh, one issue... how do I know where the data files for the moves are?
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-moves () (defun rpgdm-ironsworn-moves ()
"Return a list containing available moves, and the filename containing "Return a list containing available moves and its filename.
the moves instructions, and other properties. Note that this function is The file contains the move's instructions and other properties. Note
memoized, in that re-calling this function will return a cached copy." that this function is memoized, in that re-calling this function
will return a cached copy."
(unless rpgdm-ironsworn-moves (unless rpgdm-ironsworn-moves
(setq rpgdm-ironsworn-moves (setq rpgdm-ironsworn-moves
(mapcar 'rpgdm-ironsworn--move-tuple (mapcar 'rpgdm-ironsworn--move-tuple
(directory-files-recursively (directory-files-recursively
(f-join rpgdm-ironsworn-project "moves") (f-join rpgdm-ironsworn-project "moves")
".*\.org$")))) ".*\.org$"))))
rpgdm-ironsworn-moves) rpgdm-ironsworn-moves)
#+END_SRC #+END_SRC
@ -710,21 +759,33 @@ Choosing a move comes from using the =completing-read= along with a /list/ of al
(completing-read "Move: " (rpgdm-ironsworn-moves)) (completing-read "Move: " (rpgdm-ironsworn-moves))
#+END_SRC #+END_SRC
We'll wrap that in a function to let the user choose a nicely formatted move, but return the file containing the move. A frustrating lack-of-function is a [[help:completing-read][completing-read]] function that can take a plist, but return the /value/ instead of the key. Lets creating one.
#+BEGIN_SRC emacs-lisp
(defun completing-read-value (prompt values)
"Like `completing-read' but returns the value from VALUES instead of key.
Display PROMPT, and has a list of choices displayed for the user to select."
(thread-first prompt
(completing-read values)
(assoc values)
(second)))
#+END_SRC
We can use that function to let the user choose a nicely formatted move, but return the file containing the move.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-choose-move () (defun rpgdm-ironsworn-choose-move ()
(let* ((move (completing-read "Move: " (rpgdm-ironsworn-moves))) "A `completing-read' for moves, but returns the move filename."
(tuple (assoc move (rpgdm-ironsworn-moves)))) (completing-read-value "Move: " (rpgdm-ironsworn-moves)))
(cadr tuple)))
#+END_SRC #+END_SRC
Another feature I want, is that after completing a move, to put the results in a register, so that I can paste it into my notes file: Another feature I want, is that after completing a move, to put the results in a register, so that I can paste it into my notes file:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--store-move (title results) (defun rpgdm-ironsworn--store-move (title results)
"Store the results in a `m' register. It should also include "Store RESULTS in `m' register for later pasting.
the name of the move, based on the current file." The register also has TITLE, the name of the move, based on the
current file."
(set-register ?m (format "# %s ... %s " title results))) (set-register ?m (format "# %s ... %s " title results)))
#+END_SRC #+END_SRC
@ -797,11 +858,11 @@ While the 10 boxes are easy for pen-and-paper games, we really need the number a
(cl-flet* ((faded (str) (propertize str 'face '(:foreground "#888"))) (cl-flet* ((faded (str) (propertize str 'face '(:foreground "#888")))
(msg (a b) (format "%s %s %s" a (faded "--") (faded b)))) (msg (a b) (format "%s %s %s" a (faded "--") (faded b))))
(defvar (defvar
rpgdm-ironsworn-progress-levels `((,(msg "troublesome" "quick") . 12) rpgdm-ironsworn-progress-levels `((,(msg "troublesome" "quick") 12)
(,(msg "dangerous" "short") . 8) (,(msg "dangerous" "short") 8)
(,(msg "formidable" "long") . 4) (,(msg "formidable" "long") 4)
(,(msg "extreme" "very long") . 2) (,(msg "extreme" "very long") 2)
(,(msg "epic" "never-ending") . 1)) (,(msg "epic" "never-ending") 1))
"The five levels of progression for an Ironsworn progress track.")) "The five levels of progression for an Ironsworn progress track."))
#+END_SRC #+END_SRC
@ -848,13 +909,27 @@ Adding a progress to a character amounts to an arbitrary name, and the number of
Stored as a property in the org file. Keep in mind that the Stored as a property in the org file. Keep in mind that the
NAME should be a short title, not a description." NAME should be a short title, not a description."
(interactive (list (read-string "Progress Name: ") (interactive (list (read-string "Progress Name: ")
(completing-read "Progress Level: " (completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels))) rpgdm-ironsworn-progress-levels)))
(let* ((track-id (substring (secure-hash 'md5 name) 0 8))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level 0))
(title (org-get-heading))
(option '(("At the same level as a sibling?" same-level)
("As a subheading to this?" subheading)
("No new heading. Re-use this." no))))
(cl-case (completing-read-value "Create a new heading? " option)
('same-level (progn
(org-insert-heading-respect-content)
(insert name)))
('subheading (progn
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name))))
(let* ((level-value (rpgdm-ironsworn-progress-level level))
(track-id (substring (secure-hash 'md5 name) 0 8))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level-value 0)))
(org-set-property track-prop track-val))) (org-set-property track-prop track-val)))
#+END_SRC #+END_SRC
@ -938,6 +1013,80 @@ Let's make sure these function work as we expect:
(rpgdm-ironsworn-progress-mark track 2) (rpgdm-ironsworn-progress-mark track 2)
(should (= (rpgdm-ironsworn-progress-amount track) 1)))) (should (= (rpgdm-ironsworn-progress-amount track) 1))))
#+END_SRC #+END_SRC
*** Delve Site Progress
In the Ironsworn Delve expansion, you can venture in a /dangerous place/, and this is a slightly different progress. To begin, you choose a /theme/ and a /domain/, and then many oracles can refer to a combination of them. Leta have a function that allows us to choose both (and store) so that we can refer to them again.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-discover-a-site (theme domain)
"Store a Delve Site information in the org file."
(interactive
(list
(completing-read "What is the site theme? " rpgdm-ironsworn-site-themes)
(completing-read "What is the domain? " rpgdm-ironsworn-site-domains)))
(rpgdm-ironsworn-progress-create
(read-string "Site Name: "
(rpgdm-ironsworn-oracle-site-name domain))
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels))
(next-line 2)
(rpgdm-ironsworn-store-character-state 'site-theme (downcase theme))
(rpgdm-ironsworn-store-character-state 'site-domain (downcase domain)))
#+END_SRC
With these properties in place, we can now do a much better job with the [[file:moves/delve/delve-the-depths.org][Delve the Depths]] move.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-delve-the-depths-weak (stat)
"Return random results from weak hit table for Delve the Depths."
(interactive (list (completing-read "Stat Choice: "
'("wits" "shadow" "edge"))))
(let ((table-name (format "delve/weak-hit/%s" stat)))
(message "Rolling on %s" table-name)
(rpgdm-tables-choose table-name)))
(defun rpgdm-ironsworn-delve-the-depths-weak-edge ()
(interactive)
(rpgdm-ironsworn-delve-the-depths-weak "edge"))
(defun rpgdm-ironsworn-delve-the-depths-weak-shadow ()
(interactive)
(rpgdm-ironsworn-delve-the-depths-weak "shadow"))
(defun rpgdm-ironsworn-delve-the-depths-weak-wits ()
(interactive)
(rpgdm-ironsworn-delve-the-depths-weak "wits"))
#+END_SRC
With the theme and domain properties in place, we can now do a much better job with the [[file:moves/delve/reveal-a-danger.org][Reveal a Danger]] move.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--reveal-a-danger ()
"Return a random danger appropriate for Delve sites.
For instance, if the `danger' table states to consult the theme
or domain tables, this reads the `site-theme' and `site-domain'
properties in the current org file, and rolls on the appropriate
chart."
(let* ((theme (rpgdm-ironsworn-character-stat 'site-theme))
(domain (rpgdm-ironsworn-character-stat 'site-domain))
(danger (rpgdm-tables-choose "danger")))
(cond
((equal danger "Check the theme card.")
(rpgdm-tables-choose (format "danger/theme/%s" theme)))
((equal danger "Check the domain card.")
(rpgdm-tables-choose (format "danger/domain/%s" theme)))
(t danger))))
(defun rpgdm-ironsworn-reveal-a-danger ()
"Display a random danger appropriate for Delve sites.
For instance, if the `danger' table states to consult the theme
or domain tables, this reads the `site-theme' and `site-domain'
properties in the current org file, and rolls on the appropriate
chart."
(interactive)
(rpgdm-message "Revealed Danger: %s" (rpgdm-ironsworn--reveal-a-danger)))
#+END_SRC
** Oracles ** Oracles
Shawn Tompkin has created some useful oracles (random tables) to consult. I'm breaking my own [[https://gitlab.com/howardabrams/emacs-rpgdm][rpgdm project]] convention, and having this code automatically load those tables. Shawn Tompkin has created some useful oracles (random tables) to consult. I'm breaking my own [[https://gitlab.com/howardabrams/emacs-rpgdm][rpgdm project]] convention, and having this code automatically load those tables.
@ -945,6 +1094,19 @@ Shawn Tompkin has created some useful oracles (random tables) to consult. I'm br
(rpgdm-tables-load (f-join rpgdm-ironsworn-project "tables")) (rpgdm-tables-load (f-join rpgdm-ironsworn-project "tables"))
#+END_SRC #+END_SRC
Some tables contain /code/ we need, so lets gather those. The /trick/ is that the [[help:rpgdm-tables-load][rpgdm-tables-load]] function first stores only the /filename/ until we call [[help:rpgdm-tables-choose][rpgdm-tables-choose]], in which case, the contents of the table is now stored in the hash. So, to get the values, we first choose an item (and ignore it), and then just store the value from the hashtable.
#+BEGIN_SRC emacs-lisp
(defvar rpgdm-ironsworn-site-themes
(progn (rpgdm-tables-choose "site/theme")
(gethash "site/theme" rpgdm-tables))
"A list of the Delve site themes.")
(defvar rpgdm-ironsworn-site-domains
(progn (rpgdm-tables-choose "site/domain")
(gethash "site/domain" rpgdm-tables))
"A list of the Delve site domains.")
#+END_SRC
He designed many of the tables to work together, for instance, you should roll on both the [[file:tables/actions.org][actions]] and [[file:tables/themes.org][themes]] and combine the result to kick-start your ideas. He designed many of the tables to work together, for instance, you should roll on both the [[file:tables/actions.org][actions]] and [[file:tables/themes.org][themes]] and combine the result to kick-start your ideas.
Rolling on one table is simple, but here we have a collection of helper function to roll on multiple tables, and display the result altogether. Rolling on one table is simple, but here we have a collection of helper function to roll on multiple tables, and display the result altogether.
@ -1067,7 +1229,7 @@ Requires a =place-type= to help limit the values that can be in /place/ and then
(let ((description (rpgdm-tables-choose "site/name/description")) (let ((description (rpgdm-tables-choose "site/name/description"))
(detail (rpgdm-tables-choose "site/name/detail")) (detail (rpgdm-tables-choose "site/name/detail"))
(namesake (rpgdm-tables-choose "site/name/namesake")) (namesake (rpgdm-tables-choose "site/name/namesake"))
(place (rpgdm-tables-choose (format "site/name/place/%s" place-type))) (place (rpgdm-tables-choose (format "site/name/place/%s" (downcase place-type))))
(roll (rpgdm--roll-die 100))) (roll (rpgdm--roll-die 100)))
(rpgdm-message (rpgdm-message
(cond (cond
@ -1194,12 +1356,22 @@ Can a Hydra call a hydra? Let's more all the special oracle and progress functio
("s" rpgdm-ironsworn-oracle-site-nature "Site Nature") ("s" rpgdm-ironsworn-oracle-site-nature "Site Nature")
("t" rpgdm-ironsworn-oracle-threat-goal "Threat's Goal")) ("t" rpgdm-ironsworn-oracle-threat-goal "Threat's Goal"))
(defhydra hydra-rpgdm-delve (:color blue)
"Delve site actions"
("n" rpgdm-ironsworn-discover-a-site "discover a site")
("w" rpgdm-ironsworn-delve-the-depths-weak "weak hit table")
("d" rpgdm-ironsworn-reveal-a-danger "reveal a danger")
("m" rpgdm-ironsworn-progress-mark "mark progress")
("p" rpgdm-ironsworn-progress-amount "show progress")
("x" rpgdm-ironsworn-progress-delete "delete")
("r" rpgdm-ironsworn-progress-roll "escape the depths"))
(defhydra hydra-rpgdm-progress (:color blue) (defhydra hydra-rpgdm-progress (:color blue)
"Progress Tracks" "Progress Tracks"
("n" rpgdm-ironsworn-progress-create "new") ("n" rpgdm-ironsworn-progress-create "new")
("m" rpgdm-ironsworn-progress-mark "mark") ("m" rpgdm-ironsworn-progress-mark "mark")
("p" rpgdm-ironsworn-progress-amount "show") ("p" rpgdm-ironsworn-progress-amount "show")
("d" rpgdm-ironsworn-progress-delete "delete") ("x" rpgdm-ironsworn-progress-delete "delete")
("r" rpgdm-ironsworn-progress-roll "roll")) ("r" rpgdm-ironsworn-progress-roll "roll"))
#+END_SRC #+END_SRC
@ -1223,11 +1395,11 @@ But we roll some of these more than others, especially since "moves" does most o
" "
^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 _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 _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 _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 _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-progress-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)
("e" rpgdm-ironsworn-roll-edge) ("e" rpgdm-ironsworn-roll-edge)
@ -1246,6 +1418,8 @@ But we roll some of these more than others, especially since "moves" does most o
("M" rpgdm-ironsworn-adjust-momentum :color pink) ("M" rpgdm-ironsworn-adjust-momentum :color pink)
("O" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color pink) ("O" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color pink)
("d" hydra-rpgdm-delve/body)
("p" hydra-rpgdm-progress/body) ("p" hydra-rpgdm-progress/body)
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen) ("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
@ -1366,21 +1540,21 @@ 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 (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) (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-element)
(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.
@ -1388,11 +1562,17 @@ Enough chit-chat, let's write this function. While we are at it, let's convert t
lower levels of the tree headings take precedence." 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 (eq 'headline (org-element-type (org-element-at-point))) (unless (org-at-heading-p)
(org-up-element)) (org-up-element))
(rpgdm-ironsworn--current-character-state results) ;; Put the lowest heading title in the results hashtable:
results))) (puthash 'title (thread-first
(org-element-at-point)
(second)
(plist-get :raw-value))
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:

View file

@ -2,17 +2,17 @@
When you traverse an area within a perilous site, envision your surroundings ([[file:/Volumes/Personal/personal/ironsworn/moves/fate/ask-the-oracle.org][Ask the Oracle]] if unsure). Then, consider your approach. If you navigate this area... When you traverse an area within a perilous site, envision your surroundings ([[file:/Volumes/Personal/personal/ironsworn/moves/fate/ask-the-oracle.org][Ask the Oracle]] if unsure). Then, consider your approach. If you navigate this area...
- With haste: Roll +edge. - With haste: Roll ~+edge~.
- With stealth or trickery: Roll +shadow. - With stealth or trickery: Roll ~+shadow~.
- With observation, intuition, or expertise: Roll +wits. - With observation, intuition, or expertise: Roll ~+wits~.
On a *strong hit*, you delve deeper. Mark progress and [[file:find-an-opportunity.org][Find an Opportunity]]. On a *strong hit*, you delve deeper. Mark progress and [[file:find-an-opportunity.org][Find an Opportunity]].
On a *weak hit*, roll on the following table according to your stat. On a *weak hit*, [[elisp:rpgdm-ironsworn-delve-the-depths-weak][roll on the following table]] according to your stat.
On a *miss*, [[file:reveal-a-danger.org][Reveal a Danger]]. On a *miss*, [[file:reveal-a-danger.org][Reveal a Danger]].
| Edge | Shadow | Wits | Weak Hit Result | | [[elisp:rpgdm-ironsworn-delve-the-depths-weak-edge][Edge]] | [[elisp:rpgdm-ironsworn-delve-the-depths-weak-shadow][Shadow]] | [[elisp:rpgdm-ironsworn-delve-the-depths-weak-wits][Wits]] | Weak Hit Result |
|-------+--------+-------+---------------------------------------------------| |-------+--------+-------+---------------------------------------------------|
| 1-45 | 1-30 | 1-40 | Mark progress and Reveal a Danger. | | 1-45 | 1-30 | 1-40 | Mark progress and Reveal a Danger. |
| 46-65 | 31-65 | 41-55 | Mark progress. | | 46-65 | 31-65 | 41-55 | Mark progress. |

View file

@ -1,7 +1,7 @@
** Reveal a Danger ** Reveal a Danger
When you *encounter a risky situation within a site*, envision the danger When you *encounter a risky situation within a site*, envision the danger
or [[elisp:(rpgdm-tables-choose "dangers")][roll on the following table]]: or [[elisp:(rpgdm-tables-choose "dangers")][roll on the following table]], or better yet, call the function.
| Roll | Result | | Roll | Result |
|-------+-------------------------------------------------------------| |-------+-------------------------------------------------------------|

View file

@ -1,78 +1,105 @@
(add-to-list 'load-path (expand-file-name "~/other/rpgdm")) ;;; rpgdm-ironsworn -- Functions for integrating Ironsworn with Org
(add-to-list 'load-path (expand-file-name "~/other/rpgdm-ironsworn")) ;;
;; Copyright (C) 2020 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams
;; Created: September 18, 2020
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; This file is conspicuously absent from commentary or even
;; comments. This is because this file is created from tangling
;; the README.org file in this directory.
;;
;;; Code:
(require 'rpgdm) (require 'rpgdm)
(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.")
(defun rpgdm-ironsworn--results (action modifier one-challenge two-challenge (defun rpgdm-ironsworn--results (action modifier one-challenge two-challenge
&optional momentum) &optional momentum)
"Return formatted string for an Ironsworn dice roll results.
The ACTION is the d6 which is added to the MODIFIER (which can
have character attribute values as well as any bonuses. The sum
is compared to the two d10, ONE-CHALLENGE and TWO-CHALLENGE.
The optional MOMENTUM can be specified to add a message that the
use could burn that in order to improve the roll."
(unless momentum (unless momentum
(setq momentum 0)) (setq momentum 0))
(cl-flet ((strong-p (value dice1 dice2) (and (> value dice1) (> value dice2))) (cl-flet ((strong-p (value dice1 dice2) (and (> value dice1) (> value dice2)))
(weak-p (value dice1 dice2) (or (> value dice1) (> value dice2))) (weak-p (value dice1 dice2) (or (> value dice1) (> value dice2)))
(miss-p (value dice1 dice2) (and (<= value dice1) (<= value dice2))) (miss-p (value dice1 dice2) (and (<= value dice1) (<= value dice2)))
(faded (str) (propertize str 'face '(:foreground "#888"))) (faded (str) (propertize str 'face '(:foreground "#888")))
(noted (str) (propertize str 'face '(:foreground "light blue"))) (noted (str) (propertize str 'face '(:foreground "light blue")))
(strong (str) (propertize str 'face '(:foreground "green"))) (strong (str) (propertize str 'face '(:foreground "green")))
(weak (str) (propertize str 'face '(:foreground "yellow"))) (weak (str) (propertize str 'face '(:foreground "yellow")))
(interest (str) (propertize str 'face '(:foreground "orange"))) (interest (str) (propertize str 'face '(:foreground "orange")))
(miss (str) (propertize str 'face '(:foreground "red")))) (miss (str) (propertize str 'face '(:foreground "red"))))
(let* ((action-results (+ action modifier)) (let* ((action-results (+ action modifier))
(str-results (cond (str-results (cond
((strong-p action-results one-challenge two-challenge) ((strong-p action-results one-challenge two-challenge)
(strong "Strong hit")) (strong "Strong hit"))
((weak-p action-results one-challenge two-challenge) ((weak-p action-results one-challenge two-challenge)
(weak "Weak hit")) (weak "Weak hit"))
(t (miss "Miss")))) (t (miss "Miss"))))
(burn-msg (if (> momentum action-results) (burn-msg (if (> momentum action-results)
(cond (cond
((and (strong-p momentum one-challenge two-challenge) ((and (strong-p momentum one-challenge two-challenge)
(not (strong-p action-results one-challenge two-challenge))) (not (strong-p action-results one-challenge two-challenge)))
(concat " -- Burn momentum for a " (strong "Strong hit"))) (concat " -- Burn momentum for a " (strong "Strong hit")))
((and (weak-p momentum one-challenge two-challenge) ((and (weak-p momentum one-challenge two-challenge)
(miss-p action-results one-challenge two-challenge)) (miss-p action-results one-challenge two-challenge))
(concat " -- Burn momentum for a " (weak "Weak hit"))) (concat " -- Burn momentum for a " (weak "Weak hit")))
(t "")) (t ""))
"")) ""))
(matched-msg (if (= one-challenge two-challenge) (matched-msg (if (= one-challenge two-challenge)
(concat "" (interest "Create a Twist")) (concat "" (interest "Create a Twist"))
""))) "")))
(format "%s %s %d %s%d %s %d%s %s %d %s %d%s%s" (format "%s %s %d %s%d %s %d%s %s %d %s %d%s%s"
str-results (faded "::") str-results (faded "::")
(+ action modifier) (faded "(") (+ action modifier) (faded "(")
action (faded "+") action (faded "+")
modifier (faded ")") modifier (faded ")")
(noted "") (noted "")
one-challenge (faded "/") one-challenge (faded "/")
two-challenge two-challenge
matched-msg burn-msg)))) matched-msg burn-msg))))
(defun rpgdm-ironsworn-roll (modifier &optional momentum) (defun rpgdm-ironsworn-roll (modifier &optional momentum)
"Display a Hit/Miss message based on comparing a d6 action "Display a Hit/Miss message based on an Ironsworn roll.
roll (added to MODIFIER) vs. two d10 challenge dice." Done by rolling and comparing a d6 action roll (summed with
MODIFIER) vs two d10 challenge dice. If given, the MOMENTUM may
trigger a message to the user that they can burn that for better
results."
(interactive "nModifier: ") (interactive "nModifier: ")
(let ((one-challenge (rpgdm--roll-die 10)) (let ((one-challenge (rpgdm--roll-die 10))
(two-challenge (rpgdm--roll-die 10)) (two-challenge (rpgdm--roll-die 10))
(action-roll (rpgdm--roll-die 6))) (action-roll (rpgdm--roll-die 6)))
(rpgdm-message (rpgdm-ironsworn--results action-roll modifier (rpgdm-message (rpgdm-ironsworn--results action-roll modifier
one-challenge two-challenge one-challenge two-challenge
momentum)))) momentum))))
(defun rpgdm-ironsworn--new-character-template (name) (defun rpgdm-ironsworn--new-character-template (name)
"Insert a basic Ironsworn template at the end of the current buffer." "Insert basic Ironsworn template at the end of the current buffer.
A header is created with NAME, but if this is an empty string,
a random name is generated for the purposes of the template."
(when (s-blank? name) (when (s-blank? name)
(setq name (rpgdm-tables-choose "names-ironlander"))) (setq name (rpgdm-tables-choose "names-ironlander")))
(let ((frmt (seq-random-elt '("* The Adventures of %s" (let ((frmt (seq-random-elt '("* The Adventures of %s"
"* The Journeys of %s" "* The Journeys of %s"
"* %s, an Epic Saga" "* %s, an Epic Saga"
"* The Epic of %s" "* The Epic of %s"
"* Travels of %s")))) "* Travels of %s"))))
(goto-char (point-max)) (goto-char (point-max))
(insert "# Local Variables: (insert "# Local Variables:
# eval: (progn (require 'rpgdm-ironsworn) (rpgdm-mode)) # eval: (progn (require 'rpgdm-ironsworn) (rpgdm-mode))
@ -101,7 +128,7 @@ roll (added to MODIFIER) vs. two d10 challenge dice."
(defun rpgdm-ironsworn-character-assets () (defun rpgdm-ironsworn-character-assets ()
"Return an association list of all available assets. "Return an association list of all available assets.
The `car' is a label for the asset, and the `cdr' is the filename The `car' is a label for the asset, and the `cdr' is the filename
that contains the text. The first time we call this, we read from that contains the text. The first time we call this, we read from
the `assets' directory, otherwise, we return a cached version." the `assets' directory, otherwise, we return a cached version."
(unless rpgdm-ironsworn-character-assets (unless rpgdm-ironsworn-character-assets
(let ((asset-files (thread-first rpgdm-ironsworn-project (let ((asset-files (thread-first rpgdm-ironsworn-project
@ -122,7 +149,7 @@ the `assets' directory, otherwise, we return a cached version."
(cdr)))) (cdr))))
(defun rpgdm-ironsworn-insert-character-asset (asset) (defun rpgdm-ironsworn-insert-character-asset (asset)
"Choose and insert the contents of an asset in the current buffer." "Choose and insert the contents of an ASSET in the current buffer."
(interactive (list (rpgdm-ironsworn--pick-character-asset))) (interactive (list (rpgdm-ironsworn--pick-character-asset)))
(let ((file (if (consp asset) (cdr asset) asset))) (let ((file (if (consp asset) (cdr asset) asset)))
(insert-file-contents file nil) (insert-file-contents file nil)
@ -133,7 +160,7 @@ the `assets' directory, otherwise, we return a cached version."
(defun rpgdm-ironsworn--good-character-assets (asset-files) (defun rpgdm-ironsworn--good-character-assets (asset-files)
"Return ASSET-FILES if all given are _good enough_. "Return ASSET-FILES if all given are _good enough_.
That is, all are unique, only one companion, etc." That is, all are unique, only one companion, etc."
(cl-flet ((companion-p (entry) (cl-flet ((companion-p (entry)
(when (consp entry) (when (consp entry)
(setq entry (cdr entry))) (setq entry (cdr entry)))
@ -167,6 +194,8 @@ The chosen assets are _good_ in that they won't have duplicates, etc."
(good-enough-list (rpgdm-ironsworn-character-assets))) (good-enough-list (rpgdm-ironsworn-character-assets)))
(defun rpgdm-ironsworn-random-character-assets (&optional number-of-assets) (defun rpgdm-ironsworn-random-character-assets (&optional number-of-assets)
"Return the file names of NUMBER-OF-ASSETS from the `assets' directory.
The chosen assets are _good_ in that they won't have duplicates, etc."
(interactive "nHow many random assets should we insert? ") (interactive "nHow many random assets should we insert? ")
(dolist (file (rpgdm-ironsworn--random-character-assets number-of-assets)) (dolist (file (rpgdm-ironsworn--random-character-assets number-of-assets))
(rpgdm-ironsworn-insert-character-asset file))) (rpgdm-ironsworn-insert-character-asset file)))
@ -181,12 +210,13 @@ The chosen assets are _good_ in that they won't have duplicates, etc."
(call-interactively 'rpgdm-ironsworn-insert-character-asset)))) (call-interactively 'rpgdm-ironsworn-insert-character-asset))))
(defun rpgdm-ironsworn--new-character-stats () (defun rpgdm-ironsworn--new-character-stats ()
"Query the user for a new character's stats, and add them as "Insert character stats after querying user for them.
properties using the `rpgdm-ironsworn-store-character-state' and Note: The stats are added as properties using the
the `rpgdm-ironsworn-progress-create' functions." `rpgdm-ironsworn-store-character-state' and the
`rpgdm-ironsworn-progress-create' functions."
(dolist (stat '(edge heart iron shadow wits)) (dolist (stat '(edge heart iron shadow wits))
(rpgdm-ironsworn-store-character-state stat (rpgdm-ironsworn-store-character-state stat
(read-string (format "What '%s' stat: " stat)))) (read-string (format "What '%s' stat: " stat))))
(dolist (stat '(health spirit supply)) (dolist (stat '(health spirit supply))
(rpgdm-ironsworn-store-character-state stat 5)) (rpgdm-ironsworn-store-character-state stat 5))
@ -200,13 +230,15 @@ the `rpgdm-ironsworn-progress-create' functions."
(insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement-names")))) (insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement-names"))))
(defun rpgdm-ironsworn--new-character-stats-first (&optional name) (defun rpgdm-ironsworn--new-character-stats-first (&optional name)
"Insert a new character template, query for the stats, then insert assets." "Insert a new character template for character, NAME.
The character stats are first queried, and then assets inserted."
(rpgdm-ironsworn--new-character-template name) (rpgdm-ironsworn--new-character-template name)
(rpgdm-ironsworn--new-character-stats) (rpgdm-ironsworn--new-character-stats)
(rpgdm-ironsworn--new-character-assets)) (rpgdm-ironsworn--new-character-assets))
(defun rpgdm-ironsworn--new-character-assets-first (&optional name) (defun rpgdm-ironsworn--new-character-assets-first (&optional name)
"Insert a new character template, insert assets then query for the stats." "Insert a new character template for character, NAME.
The assets are inserted first, and then character stats are queried."
(rpgdm-ironsworn--new-character-template name) (rpgdm-ironsworn--new-character-template name)
;; Saving and restoring point, means the properties should be in the ;; Saving and restoring point, means the properties should be in the
;; correct, top-level position. ;; correct, top-level position.
@ -216,24 +248,28 @@ the `rpgdm-ironsworn-progress-create' functions."
(defun rpgdm-ironsworn-new-character (name order) (defun rpgdm-ironsworn-new-character (name order)
"Interactively query the user for a new character's attribute. "Interactively query the user for a new character's attribute.
This function _appends_ this information to the current buffer, The NAME is the character's name, and ORDER determines how the
which should be using the `org-mode' major mode." template will generate and query the user for the rest of the data.
This function _appends_ this information to the current buffer,
which should be using the `org-mode' major mode."
(interactive (list (interactive (list
(read-string "What is the new character's name? ") (read-string "What is the new character's name? ")
(completing-read "What order should we build this? " '("Statistics first" "Assets first")))) (completing-read "What order should we build this? " '("Statistics first" "Assets first"))))
(if (equal order "Assets first") (if (equal order "Assets first")
(rpgdm-ironsworn--new-character-assets-first) (rpgdm-ironsworn--new-character-assets-first)
(rpgdm-ironsworn--new-character-stats-first)) (rpgdm-ironsworn--new-character-stats-first))
(message "Alright, the template is complete. Edit away!" name)) (message "Alright, the template is complete. Edit away!" name))
(defun rpgdm-ironsworn--display-stat (stat character) (defun rpgdm-ironsworn--display-stat (stat character)
"Colorized the STAT from a CHARACTER hash containing it.
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 ()
@ -242,20 +278,20 @@ the `rpgdm-ironsworn-progress-create' functions."
(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"
(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))))
(defun rpgdm-ironsworn-to-string (a) (defun rpgdm-ironsworn-to-string (a)
"Return a lowercase string from either a string, keyword or symbol." "Return a lowercase string from either A, a string, keyword or symbol."
(downcase (downcase
(cond (cond
((keywordp a) (substring (symbol-name a) 1)) ((keywordp a) (substring (symbol-name a) 1))
@ -269,15 +305,18 @@ Health: %s Spirit: %s Supply: %s Momentum: %d"
(lambda (s) (sxhash-equal (rpgdm-ironsworn-to-string s)))) (lambda (s) (sxhash-equal (rpgdm-ironsworn-to-string s))))
(defun rpgdm-ironsworn-character-stat (stat &optional character) (defun rpgdm-ironsworn-character-stat (stat &optional character)
"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
the `rpgdm-ironsworn-current-character-state' function."
(when (null character) (when (null character)
(setq character (rpgdm-ironsworn-current-character-state))) (setq character (rpgdm-ironsworn-current-character-state)))
(gethash stat character 1)) (gethash stat character 1))
(defun rpgdm-ironsworn-adjust-stat (stat adj &optional default) (defun rpgdm-ironsworn-adjust-stat (stat adj &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."
(let* ((curr (rpgdm-ironsworn-character-stat stat)) (let* ((curr (rpgdm-ironsworn-character-stat stat))
(new (+ curr adj))) (new (+ curr adj)))
(rpgdm-ironsworn-store-character-state stat new))) (rpgdm-ironsworn-store-character-state stat new)))
(defun rpgdm-ironsworn-adjust-health (health-adj) (defun rpgdm-ironsworn-adjust-health (health-adj)
@ -353,42 +392,55 @@ Health: %s Spirit: %s Supply: %s Momentum: %d"
(rpgdm-ironsworn-character-stat :supply))) (rpgdm-ironsworn-character-stat :supply)))
(defun rpgdm-ironsworn--move-tuple (file) (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
directory and file name."
(let* ((regx (rx "moves/" (let* ((regx (rx "moves/"
(group (one-or-more (not "/"))) (group (one-or-more (not "/")))
"/" "/"
(group (one-or-more (not "."))) (group (one-or-more (not ".")))
".org" eol)) ".org" eol))
(mtch (string-match regx file)) (mtch (string-match regx file))
(type (thread-last file (type (thread-last file
(match-string 1) (match-string 1)
(s-titleize))) (s-titleize)))
(name (thread-last file (name (thread-last file
(match-string 2) (match-string 2)
(s-replace-regexp "-" " ")))) (s-replace-regexp "-" " "))))
(list (format "%s :: %s" type name) file))) (list (format "%s :: %s" type name) file)))
(defvar rpgdm-ironsworn-moves () "A list of tuples of the move and the file containing its goodness.") (defvar rpgdm-ironsworn-moves ()
"A list of tuples of the move and the file containing its goodness.")
(defun rpgdm-ironsworn-moves () (defun rpgdm-ironsworn-moves ()
"Return a list containing available moves, and the filename containing "Return a list containing available moves and its filename.
the moves instructions, and other properties. Note that this function is The file contains the move's instructions and other properties. Note
memoized, in that re-calling this function will return a cached copy." that this function is memoized, in that re-calling this function
will return a cached copy."
(unless rpgdm-ironsworn-moves (unless rpgdm-ironsworn-moves
(setq rpgdm-ironsworn-moves (setq rpgdm-ironsworn-moves
(mapcar 'rpgdm-ironsworn--move-tuple (mapcar 'rpgdm-ironsworn--move-tuple
(directory-files-recursively (directory-files-recursively
(f-join rpgdm-ironsworn-project "moves") (f-join rpgdm-ironsworn-project "moves")
".*\.org$")))) ".*\.org$"))))
rpgdm-ironsworn-moves) rpgdm-ironsworn-moves)
(defun completing-read-value (prompt values)
"Like `completing-read' but returns the value from VALUES instead of key.
Display PROMPT, and has a list of choices displayed for the user to select."
(thread-first prompt
(completing-read values)
(assoc values)
(second)))
(defun rpgdm-ironsworn-choose-move () (defun rpgdm-ironsworn-choose-move ()
(let* ((move (completing-read "Move: " (rpgdm-ironsworn-moves))) "A `completing-read' for moves, but returns the move filename."
(tuple (assoc move (rpgdm-ironsworn-moves)))) (completing-read-value "Move: " (rpgdm-ironsworn-moves)))
(cadr tuple)))
(defun rpgdm-ironsworn--store-move (title results) (defun rpgdm-ironsworn--store-move (title results)
"Store the results in a `m' register. It should also include "Store RESULTS in `m' register for later pasting.
the name of the move, based on the current file." The register also has TITLE, the name of the move, based on the
current file."
(set-register ?m (format "# %s ... %s " title results))) (set-register ?m (format "# %s ... %s " title results)))
(defun rpgdm-ironsworn-make-move (move-file) (defun rpgdm-ironsworn-make-move (move-file)
@ -438,11 +490,11 @@ See `rpgdm-ironsworn-roll-stat' for details."
(cl-flet* ((faded (str) (propertize str 'face '(:foreground "#888"))) (cl-flet* ((faded (str) (propertize str 'face '(:foreground "#888")))
(msg (a b) (format "%s %s %s" a (faded "--") (faded b)))) (msg (a b) (format "%s %s %s" a (faded "--") (faded b))))
(defvar (defvar
rpgdm-ironsworn-progress-levels `((,(msg "troublesome" "quick") . 12) rpgdm-ironsworn-progress-levels `((,(msg "troublesome" "quick") 12)
(,(msg "dangerous" "short") . 8) (,(msg "dangerous" "short") 8)
(,(msg "formidable" "long") . 4) (,(msg "formidable" "long") 4)
(,(msg "extreme" "very long") . 2) (,(msg "extreme" "very long") 2)
(,(msg "epic" "never-ending") . 1)) (,(msg "epic" "never-ending") 1))
"The five levels of progression for an Ironsworn progress track.")) "The five levels of progression for an Ironsworn progress track."))
(defun rpgdm-ironsworn-progress-level (label) (defun rpgdm-ironsworn-progress-level (label)
@ -473,13 +525,27 @@ of squares that have been marked against some progress."
Stored as a property in the org file. Keep in mind that the Stored as a property in the org file. Keep in mind that the
NAME should be a short title, not a description." NAME should be a short title, not a description."
(interactive (list (read-string "Progress Name: ") (interactive (list (read-string "Progress Name: ")
(completing-read "Progress Level: " (completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels))) rpgdm-ironsworn-progress-levels)))
(let* ((track-id (substring (secure-hash 'md5 name) 0 8))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level 0))
(title (org-get-heading))
(option '(("At the same level as a sibling?" same-level)
("As a subheading to this?" subheading)
("No new heading. Re-use this." no))))
(cl-case (completing-read-value "Create a new heading? " option)
('same-level (progn
(org-insert-heading-respect-content)
(insert name)))
('subheading (progn
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name))))
(let* ((level-value (rpgdm-ironsworn-progress-level level))
(track-id (substring (secure-hash 'md5 name) 0 8))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level-value 0)))
(org-set-property track-prop track-val))) (org-set-property track-prop track-val)))
(defun rpgdm-ironsworn-progress-mark (name &optional times) (defun rpgdm-ironsworn-progress-mark (name &optional times)
@ -523,8 +589,81 @@ to rolling two d10 challenge dice."
(ignore-errors (ignore-errors
(remhash name tracks)))) (remhash name tracks))))
(defun rpgdm-ironsworn-discover-a-site (theme domain)
"Store a Delve Site information in the org file."
(interactive
(list
(completing-read "What is the site theme? " rpgdm-ironsworn-site-themes)
(completing-read "What is the domain? " rpgdm-ironsworn-site-domains)))
(rpgdm-ironsworn-progress-create
(read-string "Site Name: "
(rpgdm-ironsworn-oracle-site-name domain))
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels))
(next-line 2)
(rpgdm-ironsworn-store-character-state 'site-theme (downcase theme))
(rpgdm-ironsworn-store-character-state 'site-domain (downcase domain)))
(defun rpgdm-ironsworn-delve-the-depths-weak (stat)
"Return random results from weak hit table for Delve the Depths."
(interactive (list (completing-read "Stat Choice: "
'("wits" "shadow" "edge"))))
(let ((table-name (format "delve/weak-hit/%s" stat)))
(message "Rolling on %s" table-name)
(rpgdm-tables-choose table-name)))
(defun rpgdm-ironsworn-delve-the-depths-weak-edge ()
(interactive)
(rpgdm-ironsworn-delve-the-depths-weak "edge"))
(defun rpgdm-ironsworn-delve-the-depths-weak-shadow ()
(interactive)
(rpgdm-ironsworn-delve-the-depths-weak "shadow"))
(defun rpgdm-ironsworn-delve-the-depths-weak-wits ()
(interactive)
(rpgdm-ironsworn-delve-the-depths-weak "wits"))
(defun rpgdm-ironsworn--reveal-a-danger ()
"Return a random danger appropriate for Delve sites.
For instance, if the `danger' table states to consult the theme
or domain tables, this reads the `site-theme' and `site-domain'
properties in the current org file, and rolls on the appropriate
chart."
(let* ((theme (rpgdm-ironsworn-character-stat 'site-theme))
(domain (rpgdm-ironsworn-character-stat 'site-domain))
(danger (rpgdm-tables-choose "danger")))
(cond
((equal danger "Check the theme card.")
(rpgdm-tables-choose (format "danger/theme/%s" theme)))
((equal danger "Check the domain card.")
(rpgdm-tables-choose (format "danger/domain/%s" theme)))
(t danger))))
(defun rpgdm-ironsworn-reveal-a-danger ()
"Display a random danger appropriate for Delve sites.
For instance, if the `danger' table states to consult the theme
or domain tables, this reads the `site-theme' and `site-domain'
properties in the current org file, and rolls on the appropriate
chart."
(interactive)
(rpgdm-message "Revealed Danger: %s" (rpgdm-ironsworn--reveal-a-danger)))
(rpgdm-tables-load (f-join rpgdm-ironsworn-project "tables")) (rpgdm-tables-load (f-join rpgdm-ironsworn-project "tables"))
(defvar rpgdm-ironsworn-site-themes
(progn (rpgdm-tables-choose "site/theme")
(gethash "site/theme" rpgdm-tables))
"A list of the Delve site themes.")
(defvar rpgdm-ironsworn-site-domains
(progn (rpgdm-tables-choose "site/domain")
(gethash "site/domain" rpgdm-tables))
"A list of the Delve site domains.")
(defun rpgdm-ironsworn-oracle-action-theme () (defun rpgdm-ironsworn-oracle-action-theme ()
"Rolls on two tables at one time." "Rolls on two tables at one time."
(interactive) (interactive)
@ -605,7 +744,7 @@ You'll need to pick and choose what works and discard what doesn't."
(let ((description (rpgdm-tables-choose "site/name/description")) (let ((description (rpgdm-tables-choose "site/name/description"))
(detail (rpgdm-tables-choose "site/name/detail")) (detail (rpgdm-tables-choose "site/name/detail"))
(namesake (rpgdm-tables-choose "site/name/namesake")) (namesake (rpgdm-tables-choose "site/name/namesake"))
(place (rpgdm-tables-choose (format "site/name/place/%s" place-type))) (place (rpgdm-tables-choose (format "site/name/place/%s" (downcase place-type))))
(roll (rpgdm--roll-die 100))) (roll (rpgdm--roll-die 100)))
(rpgdm-message (rpgdm-message
(cond (cond
@ -692,23 +831,33 @@ You'll need to pick and choose what works and discard what doesn't."
("s" rpgdm-ironsworn-oracle-site-nature "Site Nature") ("s" rpgdm-ironsworn-oracle-site-nature "Site Nature")
("t" rpgdm-ironsworn-oracle-threat-goal "Threat's Goal")) ("t" rpgdm-ironsworn-oracle-threat-goal "Threat's Goal"))
(defhydra hydra-rpgdm-delve (:color blue)
"Delve site actions"
("n" rpgdm-ironsworn-discover-a-site "discover a site")
("w" rpgdm-ironsworn-delve-the-depths-weak "weak hit table")
("d" rpgdm-ironsworn-reveal-a-danger "reveal a danger")
("m" rpgdm-ironsworn-progress-mark "mark progress")
("p" rpgdm-ironsworn-progress-amount "show progress")
("x" rpgdm-ironsworn-progress-delete "delete")
("r" rpgdm-ironsworn-progress-roll "escape the depths"))
(defhydra hydra-rpgdm-progress (:color blue) (defhydra hydra-rpgdm-progress (:color blue)
"Progress Tracks" "Progress Tracks"
("n" rpgdm-ironsworn-progress-create "new") ("n" rpgdm-ironsworn-progress-create "new")
("m" rpgdm-ironsworn-progress-mark "mark") ("m" rpgdm-ironsworn-progress-mark "mark")
("p" rpgdm-ironsworn-progress-amount "show") ("p" rpgdm-ironsworn-progress-amount "show")
("d" 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 (: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 _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 _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 _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 _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-progress-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)
("e" rpgdm-ironsworn-roll-edge) ("e" rpgdm-ironsworn-roll-edge)
@ -727,6 +876,8 @@ You'll need to pick and choose what works and discard what doesn't."
("M" rpgdm-ironsworn-adjust-momentum :color pink) ("M" rpgdm-ironsworn-adjust-momentum :color pink)
("O" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color pink) ("O" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color pink)
("d" hydra-rpgdm-delve/body)
("p" hydra-rpgdm-progress/body) ("p" hydra-rpgdm-progress/body)
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen) ("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
@ -791,17 +942,17 @@ 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 (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) (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-element)
@ -813,9 +964,15 @@ Note that values in sibling trees are ignored, and settings in
lower levels of the tree headings take precedence." 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 (eq 'headline (org-element-type (org-element-at-point))) (unless (org-at-heading-p)
(org-up-element)) (org-up-element))
;; 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) (rpgdm-ironsworn--current-character-state results)
results))) results)))

View file

@ -0,0 +1,8 @@
#+TITLE: Weak Hit Table for Edge
Roll on Table: d100
| 1-45 | Mark progress and Reveal a Danger. |
| 46-65 | Mark progress. |
| 66-75 | Choose one: Mark progress or Find an Opportunity. |
| 76-80 | Take both: Mark progress and Find an Opportunity. |
| 81-00 | Mark progress twice and Reveal a Danger. |

View file

@ -0,0 +1,8 @@
#+TITLE: Weak Hit Table for Shadow
Roll on Table: d100
| 1-30 | Mark progress and Reveal a Danger. |
| 31-65 | Mark progress. |
| 66-90 | Choose one: Mark progress or Find an Opportunity. |
| 91-99 | Take both: Mark progress and Find an Opportunity. |
| 00 | Mark progress twice and Reveal a Danger. |

View file

@ -0,0 +1,8 @@
#+TITLE: Weak Hit Table for Wits
Roll on Table: d100
| 1-40 | Mark progress and Reveal a Danger. |
| 41-55 | Mark progress. |
| 56-80 | Choose one: Mark progress or Find an Opportunity. |
| 81-99 | Take both: Mark progress and Find an Opportunity. |
| 00 | Mark progress twice and Reveal a Danger. |