Limit assets not appropriate for new characters

Shouldn't be a Revenant without a little tragedy first.
Also, describe how progress tracks are nested.
This commit is contained in:
Howard Abrams 2022-04-29 21:39:44 -07:00
parent 9c8b030633
commit 1cb1e45efa
7 changed files with 245 additions and 125 deletions

View file

@ -58,6 +58,13 @@ What I do, is add the following "code" somewhere in my Ironsworn-specific org fi
# End:
#+END_SRC
Finally, many of the displayed org files contain embedded hyperlinks that actually call functions (defined below). I find it helpful to ignore the constant prompts for selecting this links by setting the [[help:org-link-elisp-skip-confirm-regexp][org-link-elisp-skip-confirm-regexp]] variable, as in:
#+BEGIN_SRC emacs-lisp :tangle no
(setq org-link-elisp-skip-confirm-regexp (rx string-start (optional "(") "rpgdm-"
(or "tables-" "ironsworn-")
(one-or-more any)))
#+END_SRC
Finally, define your character. While I describe the details later, first, load an org-mode file and hit ~M-x~ to type: =rpgdm-ironsworn-new-character= and answer the questions. This will create the necessary formatting and you are ready to play with either your key-binding (or ~F6~) to bring up the Hydra of commands:
#+ATTR_HTML: :width 1100px
@ -96,6 +103,72 @@ While the interface may change (and I may not update that screenshot too often),
- And ~q~ (or ~F6~) dismisses the UI.
This may be sufficient, but the rest of this document goes into details about how to use this, as well as the code to make it.
** File Organization
I really didnt want to dictate how Ironsworn notes should be organized, but I wanted the /progress tracks/ to be relevant. Sure, one can mark progress on one or more tracks, but I didnt want see tracks that were old. I toyed with the idea of deleting, or just closing, old tracks, but I realized that progress tracks are really just arcs in the stories plot. Some of long, like a character /inciting vow/ and others are short, like a battle with a Cave Lion, but they are all nested in a hierarchical tree.
Let me explain with an illustration:
#+BEGIN_SRC dot :file images/progress-tracks-tree.png :exports file :results file
digraph G {
bgcolor="transparent";
node [fillcolor="white" style="filled" fontname="Arial"];
Background [label="Background / Theme"]
Arc1 [label="Story Arc"]
Arc2 [label="Story Arc"]
Arc3 [label="Story Arc"]
Scene1 [label="Scene"]
Scene2 [label="Montage"]
Scene3 [label="Montage"]
Scene4 [label="Scene"]
Scene5 [label="Scene"]
Scene6 [label="Montage"]
Event1 [label="Event"]
Event2 [label="Challenge"]
Event3 [label="Event"]
Event4 [label="Challenge"]
Event5 [label="Event"]
Event6 [label="Challenge"]
Event7 [label="Event"]
Event8 [label="Challenge"]
Event9 [label="Event"]
Event10 [label="Challenge"]
Event11 [label="Event"]
Event12 [label="Challenge"]
Background -> Arc1;
Background -> Arc2;
Background -> Arc3;
Arc1 -> Scene1;
Arc1 -> Scene2;
Arc2 -> Scene3;
Arc2 -> Scene4;
Arc3 -> Scene5;
Arc3 -> Scene6;
Scene1 -> Event1;
Scene1 -> Event2;
Scene2 -> Event3;
Scene3 -> Event4;
Scene3 -> Event5;
Scene3 -> Event6;
Scene4 -> Event7;
Scene5 -> Event8;
Scene5 -> Event9;
Scene6 -> Event10;
Scene6 -> Event11;
Scene6 -> Event12;
}
#+END_SRC
[[file:images/progress-tracks-tree.png]]
A character or party has a theme or premise that drives the entire story, like Destroying the One Ring of Power or, teenagers solving mysteries with a talking dog. In Ironsworn, this is referred to as an /Epic Vow/. This epic story is broken into long-running arcs or character goals … perhaps they could be packaged into three books to make a Trilogy, or even seven books for each year in a school. These are the [[file:moves/quest/swear-an-iron-vow.org][various vows]], like the initial /Inciting Vow/.
These, in turn, are further divided into scenes like Journeying to Bree, Escaping Moria, or even each Saturday morning episode. In Ironsworn, these are moves like [[file:moves/adventure/undertake-a-journey.org][Undertake a Journey]] or [[file:moves/delve/delve-the-depths.org][Delve the Depths]]. And these segments can be punctuated by conflict and combat. These tracks are not concern with the length of a progress, as a battle with an Elder Beast will conclude before arriving at Grandmas House, even those the Elder Beast was /formidable/ and the journey was merely /troublesome/.
Notice that this structure works well with the outline of a typical document, and by choosing this structure and tying a progress track to a header, completed tracks would disappear on their own, and I didnt have to manage old tracks. As you call the progress functions, these prompt with a question:
[[file:images/progress-placement-prompt.png]]
** Character Sheets
A character sheet, for this project, is just an org mode file where you take notes, and =:PROPERTIES:= drawers contain the current stats for your character. While most of it is /whatever you like it to be/ ... you need to keep a few things in mind.
@ -325,7 +398,7 @@ We assume you have created an org-file, and the /template/ will just append some
")))
#+END_SRC
**** Character Assets
We store the assets in a collection of org files in the [[file:assets/][assets]] directory. We'd like the user to choose an asset, so we convert a filename into something nicer to read based on extracting the /description/ from the /filename/, for instance, =:
We store the assets in a collection of org files in the [[file:assets/][assets]] directory. We'd like the user to choose an asset, so we convert a filename into something nicer to read based on extracting the /description/ from the /filename/, for instance:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--character-asset-label (filename)
@ -410,19 +483,30 @@ Hrm. Perhaps we just want to /look/ at an asset before inserting it, similar to
When you start a character, you choose three assets, but what if we choose them randomly from our asset list? Could be fun, however, I don't want duplicates, or two companions, or ... well, I may come up with more rules, but I codify those rules into a function that returns a list, if it is good, or =nil= otherwise:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--good-character-assets (asset-files)
(defun rpgdm-ironsworn--good-character-assets (assets)
"Return ASSET-FILES if all given are _good enough_.
That is, all are unique, only one companion, etc."
(cl-flet ((companion-p (entry)
(when (consp entry)
(setq entry (cdr entry)))
(string-match (rx "companions") entry)))
(cl-flet ((only-files (entry) (if (consp entry) (cdr entry) entry))
(is-companion? (file) (string-match (rx "companions") file))
(not-at-first? (file) (when (or (s-ends-with? "revenant.org" file)
(s-ends-with? "weaponmaster.org" file)
(s-ends-with? "masked.org" file)
(s-ends-with? "battle-scarred.org" file)
(s-ends-with? "ritualist.org" file)
(s-ends-with? "shadow-kin.org" file)
(s-ends-with? "oathbreaker.org" file))
t)))
(let* ((asset-files (-map #'only-files assets))
(num-of-companions (seq-count #'is-companion? asset-files)))
(when (and
;; Are all the assets in the list unique?
(equal asset-files (seq-uniq asset-files))
(<= (seq-length
(seq-filter #'companion-p asset-files))
1))
asset-files)))
;; Does the list only include first-time-only?
(-none? #'not-at-first? asset-files)
;; Does the list include, at most, one companion?
(<= num-of-companions 1))
assets))))
#+END_SRC
And I can write a little unit test to verify my test cases:
@ -430,6 +514,10 @@ And I can write a little unit test to verify my test cases:
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn--good-character-assets-test ()
(should (rpgdm-ironsworn--good-character-assets '("foo" "bar" "baz")))
(should (rpgdm-ironsworn--good-character-assets '(("Companions :: Dog" . "assets/companions/dog.org")
("Paths :: Good Guy" . "assets/paths/good-guy.org")
("Ritual :: Booboo" . "assets/ritual/booboo.org"))))
(should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "paths/shadow-kin.org")))
(should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "foo")))
(should-not (rpgdm-ironsworn--good-character-assets '("assets/companions/dog.org"
"assets/paths/good-guy.org"
@ -736,7 +824,7 @@ Best if we wrote some unit tests to both explain and verify this function. This
The =rpgdm-ironsworn-adjust-stat= function takes one of the four stats, like =health= or =momentum=, as well as its =default= or /starting/ value, collects the /current value/ (the =curr= variable), and then creates a new value based on the /operator/ determined by the input from =rpgdm-ironsworn--read-stat=. It sets the new stat by calling =rpgdm-ironsworn-store-character-state= defined below.
#+BEGIN_SRC emacs-lisp :results silent
#+BEGIN_SRC emacs-lisp
(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."
@ -791,7 +879,7 @@ The previous functions allows us to create character stat-specific rolling funct
#+END_SRC
And we could have a function for each:
#+BEGIN_SRC emacs-lisp :results silent
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-roll-edge (modifier)
"Roll an action based on a loaded character's Edge stat with a MODIFIER."
(interactive (list (read-string "Edge + Modifier: ")))

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -4,7 +4,7 @@ When you seek to resolve questions, discover details in the world, determine how
- Draw a conclusion :: Decide the answer based on the most interesting and obvious result.
- Ask a yes/no question :: Decide the odds of a yes, and roll on the table below to check the answer.
- Pick two :: Envision two options. Rate one as likely, and [[elisp:(rpgdm-ironsworn-oracle)][roll on the table]] below to see if it is true. If not, it is the other.
- Pick two :: Envision two options. Rate one as likely, and [[elisp:rpgdm-ironsworn-oracle][roll on the table]] below to see if it is true. If not, it is the other.
- Spark an idea :: Brainstorm or use a random prompt.
| Odds | The answer is yes if you roll... |
@ -68,6 +68,3 @@ When youre unsure what a match might mean, you can roll on another oracle tab
In guided play, your GM is the oracle. You wont make this move unless you are talking things out and need a random result or a bit of inspiration. Your GM can use this move (or ask you to make it) to help guide the story.
#+STARTUP: showall
# Local Variables:
# eval: (flycheck-mode -1)
# End:

View file

@ -3,7 +3,9 @@
When you *suffer the outcome of a move*, choose one.
- Make the most obvious negative outcome happen.
- Envision two negative outcomes. Rate one as likely, and [[file:ask-the-oracle.org][Ask the Oracle]] using the yes/no table. On a yes, make that outcome happen. Otherwise, make it the other.
- Envision two negative outcomes. Rate one as likely, and [[file:ask-the-oracle.org][Ask the Oracle]] using the yes/no table.
On a yes, make that outcome happen.
Otherwise, make it the other.
- [[elisp:(rpgdm-tables-choose "pay-the-price")][Roll on the following table]]. If you have difficulty interpreting the result to fit the current situation, roll again.
| Roll | Result |

View file

@ -15,6 +15,10 @@
(ert-deftest rpgdm-ironsworn--good-character-assets-test ()
(should (rpgdm-ironsworn--good-character-assets '("foo" "bar" "baz")))
(should (rpgdm-ironsworn--good-character-assets '(("Companions :: Dog" . "assets/companions/dog.org")
("Paths :: Good Guy" . "assets/paths/good-guy.org")
("Ritual :: Booboo" . "assets/ritual/booboo.org"))))
(should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "paths/shadow-kin.org")))
(should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "foo")))
(should-not (rpgdm-ironsworn--good-character-assets '("assets/companions/dog.org"
"assets/paths/good-guy.org"

View file

@ -152,25 +152,42 @@ the `assets' directory, otherwise, we return a cached version."
(interactive (list (rpgdm-ironsworn--pick-character-asset)))
(when rpgdm-ironsworn-new-character (goto-char (point-max)))
(let ((file (if (consp asset) (cdr asset) asset)))
(insert-file-contents file nil)
(ignore-errors
(insert-file-contents file nil))))
(when (called-interactively-p)
(when (y-or-n-p "Insert another asset? ")
(call-interactively 'rpgdm-ironsworn-insert-character-asset)))))
(defun rpgdm-ironsworn-show-character-asset (asset)
"Choose and insert the contents of an ASSET in the current buffer."
(interactive (list (rpgdm-ironsworn--pick-character-asset)))
(let ((asset-file (if (consp asset) (cdr asset) asset))
(orig-buf (window-buffer)))
(ignore-errors
(find-file-other-window asset-file)
(goto-char (point-min))
(pop-to-buffer orig-buf))))
(defun rpgdm-ironsworn--good-character-assets (asset-files)
(defun rpgdm-ironsworn--good-character-assets (assets)
"Return ASSET-FILES if all given are _good enough_.
That is, all are unique, only one companion, etc."
(cl-flet ((companion-p (entry)
(when (consp entry)
(setq entry (cdr entry)))
(string-match (rx "companions") entry)))
That is, all are unique, only one companion, etc."
(cl-flet ((only-files (entry) (if (consp entry) (cdr entry) entry))
(is-companion? (file) (string-match (rx "companions") file))
(not-at-first? (file) (when (or (s-ends-with? "revenant.org" file)
(s-ends-with? "weaponmaster.org" file)
(s-ends-with? "masked.org" file)
(s-ends-with? "battle-scarred.org" file)
(s-ends-with? "ritualist.org" file)
(s-ends-with? "shadow-kin.org" file)
(s-ends-with? "oathbreaker.org" file))
t)))
(let* ((asset-files (-map #'only-files assets))
(num-of-companions (seq-count #'is-companion? asset-files)))
(when (and
;; Are all the assets in the list unique?
(equal asset-files (seq-uniq asset-files))
(<= (seq-length
(seq-filter #'companion-p asset-files))
1))
asset-files)))
;; Does the list only include first-time-only?
(-none? #'not-at-first? asset-files)
;; Does the list include, at most, one companion?
(<= num-of-companions 1))
assets))))
(defun rpgdm-ironsworn--some-character-assets (asset-filenames &optional number)
"Return a list of NUMBER elements from ASSET-FILENAMES... randomly.
@ -200,6 +217,12 @@ The chosen assets are _good_ in that they won't have duplicates, etc."
(dolist (file (rpgdm-ironsworn--random-character-assets number-of-assets))
(rpgdm-ironsworn-insert-character-asset file)))
(defun rpgdm-ironsworn-insert-character-assets ()
(ignore-errors
(call-interactively 'rpgdm-ironsworn-insert-character-asset))
(when (y-or-n-p "Insert another asset? ")
(rpgdm-ironsworn-insert-character-assets)))
(defun rpgdm-ironsworn--new-character-assets ()
"Insert the contents of three character assets from the assets directory."
(goto-char (point-max))
@ -207,7 +230,7 @@ The chosen assets are _good_ in that they won't have duplicates, etc."
(if (y-or-n-p "Would you like three random assets? ")
(rpgdm-ironsworn-random-character-assets 3)
(if (y-or-n-p "Would you like to choose your assets? ")
(call-interactively 'rpgdm-ironsworn-insert-character-asset))))
(rpgdm-ironsworn-insert-character-assets))))
(defun rpgdm-ironsworn--new-character-stats ()
"Insert character stats after querying user for them.
@ -225,8 +248,9 @@ Note: The stats are added as properties using the
(rpgdm-ironsworn-progress-create (read-string "What title should we give this new character's Epic vow: ") 1)
(rpgdm-ironsworn-progress-create "Bonds" 1)
(rpgdm-ironsworn-progress-mark "Bonds")
(next-line)
(insert "\n** Bonds\n")
(insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement/name"))))
(insert (format " - My home settlement of %s\n" (rpgdm-tables-choose "settlement/name"))))
(defun rpgdm-ironsworn--new-character-stats-first (&optional name)
"Insert a new character template for character, NAME.
@ -1075,10 +1099,11 @@ You'll need to pick and choose what works and discard what doesn't."
("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"))
("s" rpgdm-ironsworn-asset-stat-show "show asset stat")
("r" rpgdm-ironsworn-asset-stat-roll "roll asset as modifier")
("v" rpgdm-ironsworn-show-character-asset "view asset"))
(defhydra hydra-rpgdm (:color blue :hint nil)
(defhydra hydra-rpgdm (:color pink :hint nil)
"
^Dice^ 0=d100 1=d10 6=d6 ^Roll/Adjust^ ^Oracles/Tables^ ^Moving/Editing^ ^Messages^
------------------------------------------------------------------------------------------------------------------------------
@ -1106,9 +1131,9 @@ You'll need to pick and choose what works and discard what doesn't."
("O" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color pink)
("d" hydra-rpgdm-delve/body)
("p" hydra-rpgdm-progress/body)
("a" hydra-rpgdm-assets/body)
("d" hydra-rpgdm-delve/body :color blue)
("p" hydra-rpgdm-progress/body :color blue)
("a" hydra-rpgdm-assets/body :color blue)
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
("K" scroll-down :color pink) ("J" scroll-up :color pink)
@ -1124,9 +1149,10 @@ You'll need to pick and choose what works and discard what doesn't."
("0" rpgdm-roll-d100 :color pink)
("1" rpgdm-roll-d10 :color pink)
("6" rpgdm-roll-d6 :color pink)
("RET" evil-open-below :color blue)
("q" nil "quit") ("<f6>" nil))
(defun rpgdm-ironsworn-store-character-state (stat value)
(defun rpgdm-ironsworn-store-character-temp-state (stat value)
"Store the VALUE of a character's STAT in the current org tree property.
Note that STAT should be a symbol, like `supply' and VALUE should be a
number, but doesn't have to be."
@ -1135,7 +1161,7 @@ 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)
(defun rpgdm-ironsworn-store-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."
@ -1143,7 +1169,10 @@ number, but doesn't have to be."
(org-up-heading)
(while (> (org-heading-level) 1)
(org-up-heading))
(rpgdm-ironsworn-store-character-state stat value)))
(rpgdm-ironsworn-store-character-temp-state stat value)))
(defalias 'rpgdm-ironsworn-store-default-character-state
'rpgdm-ironsworn-store-character-state)
(defun rpgdm-ironsworn--property-p (prop)
"Given a symbol PROP, return non-nil if it is an ironsworn keyword.