emacs-rpgdm/docs/ironsworn-rpg.org
Howard Abrams 5455785b08 Like Athena, this project emerged fully formed from my head
At least, from this historical record as preserved by git, it is.
In reality, this project represents a year of off-and-on development
in another git repository, and has been converted and reformatted
for (potentially) public consumption.

Particularly lacking is the Tables and other charts that make this
useful, but I need to make sure I don't violate any copyright laws, as
many of my tables were copy/pasted from digital books I own.
2022-02-18 15:25:05 -08:00

429 lines
19 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+TITLE: Ironsworn
#+AUTHOR: Howard X. Abrams
#+DATE: 2021-12-21 December
#+TAGS: rpg
#+PROPERTY: header-args:sh :tangle no
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-ironsworn.el
#+PROPERTY: header-args :results none :eval no-export :comments no
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
Can I make Ironsworn-specific interface and dice rolls?
First let's load my previous code, assuming that we have added this directory to =load-path=:
#+BEGIN_SRC emacs-lisp
(require 'rpgdm)
#+END_SRC
* Dice Roller
How should the results look like when rolling? Perhaps like this formatted string:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--results (action modifier one-challenge two-challenge)
(let* ((action-results (+ action modifier))
(str-results (cond
((and (> action-results one-challenge) (> action-results two-challenge))
(propertize "Strong hit" 'face '(:foreground "green")))
((or (> action-results one-challenge) (> action-results two-challenge))
(propertize "Weak hit" 'face '(:foreground "yellow")))
(t (propertize "Miss" 'face '(:foreground "red")))))
(matched-msg (if (= one-challenge two-challenge)
(propertize " ← Create a Twist" 'face '(:foreground "orange"))
"")))
(format "%s %s %d %s %d %s %d %s %d %s" str-results
(propertize "::" 'face '(:foreground "#888"))
action
(propertize "+" 'face '(:foreground "#888"))
modifier
(propertize "" 'face '(:foreground "#888"))
one-challenge
(propertize "/" 'face '(:foreground "#888"))
two-challenge matched-msg)))
#+END_SRC
So the following should work:
#+BEGIN_SRC emacs-lisp :tangle no
(rpgdm-ironsworn--results 3 2 4 1)
(rpgdm-ironsworn--results 3 2 8 1)
(rpgdm-ironsworn--results 3 2 8 6)
(rpgdm-ironsworn--results 3 2 6 6)
#+END_SRC
The basic interface will query for a modifer, and then display the results:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-ironsworn-roll (modifier)
"Display a Hit/Miss message based on comparing a d6 action
roll (added to MODIFIER) vs. two d10 challenge dice."
(interactive "nModifier: ")
(let ((one-challenge (rpgdm--roll-die 10))
(two-challenge (rpgdm--roll-die 10))
(action-roll (rpgdm--roll-die 6)))
(rpgdm-message (rpgdm-ironsworn--results action-roll modifier
one-challenge two-challenge))))
#+END_SRC
Rolling against the progress just means we need to request that value instead of rolling the d6:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-progress-roll (progress-value)
"Display a Hit/Miss message based on comparing the PROGRESS-VALUE
to rolling two d10 challenge dice."
(interactive "nCurrent Progress Value: ")
(let ((one-challenge (rpgdm--roll-die 10))
(two-challenge (rpgdm--roll-die 10)))
(rpgdm-message (rpgdm-ironsworn--results progress-value 0
one-challenge two-challenge))))
#+END_SRC
* Characters
But what might be nice is to have a character sheet that has the default modifiers:
Internal representation of a character should be pretty simple:
#+BEGIN_SRC emacs-lisp
(define-hash-table-test 'str-or-keys
(lambda (a b)
(string-equal
(downcase
(if (symbolp a) (symbol-name a) a))
(downcase
(if (symbolp b) (symbol-name b) b))))
(lambda (s) (sxhash-equal (downcase
(if (symbolp s) (symbol-name s) s)))))
(defvar rpgdm-ironsworn-character (make-hash-table :test 'str-or-keys)
"Stats and attributes for the currently loaded character")
#+END_SRC
And a function to set them all. We may want another function that could parse an Org table or something.
#+BEGIN_SRC emacs-lisp :results silent
(cl-defun rpgdm-ironsworn-character (&key edge heart iron shadow wits
(health 5) (spirit 5) (supply 5)
(momentum 2))
"Store the player character's stats, as well as set up the defaults for the others."
(clrhash rpgdm-ironsworn-character)
;; (setq rpgdm-ironsworn-character (make-hash-table :test 'str-or-keys))
(puthash 'edge edge rpgdm-ironsworn-character)
(puthash 'heart heart rpgdm-ironsworn-character)
(puthash 'iron iron rpgdm-ironsworn-character)
(puthash 'shadow shadow rpgdm-ironsworn-character)
(puthash 'wits wits rpgdm-ironsworn-character)
(puthash 'health health rpgdm-ironsworn-character)
(puthash 'spirit spirit rpgdm-ironsworn-character)
(puthash 'supply supply rpgdm-ironsworn-character)
(puthash 'momentum momentum rpgdm-ironsworn-character))
#+END_SRC
This allows us to define our character:
#+BEGIN_SRC emacs-lisp :tangle no :results silent
(rpgdm-ironsworn-character :edge 1 :heart 2 :iron 1 :shadow 2 :wits 3)
#+END_SRC
We need to modify the values stored.
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-ironsworn-adjust-health (health-adj)
"Increase or decrease the current character's health by HEALTH-ADJ."
(interactive "nHealth Adjustment: ")
(puthash 'health
(+ (gethash 'health rpgdm-ironsworn-character 5) health-adj)
rpgdm-ironsworn-character))
(defun rpgdm-ironsworn-adjust-spirit (spirit-adj)
"Increase or decrease the current character's spirit by SPIRIT-ADJ."
(interactive "nSpirit Adjustment: ")
(puthash 'spirit
(+ (gethash 'spirit rpgdm-ironsworn-character 5) spirit-adj)
rpgdm-ironsworn-character))
(defun rpgdm-ironsworn-adjust-supply (supply-adj)
"Increase or decrease the current character's supply by SUPPLY-ADJ."
(interactive "nSupply Adjustment: ")
(puthash 'supply
(+ (gethash 'supply rpgdm-ironsworn-character 5) supply-adj)
rpgdm-ironsworn-character))
(defun rpgdm-ironsworn-adjust-momentum (momentum-adj)
"Increase or decrease the current character's momentum by MOMENTUM-ADJ."
(interactive "nMomentum Adjustment: ")
(puthash 'momentum
(+ (gethash 'momentum rpgdm-ironsworn-character 5) momentum-adj)
rpgdm-ironsworn-character))
#+END_SRC
Perhaps we need a special way to display these changing stats?
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--display-stat (stat)
(let* ((value (gethash stat rpgdm-ironsworn-character 5))
(s-val (number-to-string value))
(color (cond
((< value 1) "red")
((< value 3) "orange")
((< value 4) "yellow")
(t "green"))))
(propertize s-val 'face `(:foreground ,color))))
(defun rpgdm-ironsworn-character-display ()
"Easily display the character's stats and other things."
(interactive)
(rpgdm-message "Edge: %d Heart: %d Iron: %d Shadow: %d Wits: %d
Health: %s Spirit: %s Supply: %s Momentum: %d"
(gethash 'edge rpgdm-ironsworn-character 0)
(gethash 'heart rpgdm-ironsworn-character 0)
(gethash 'iron rpgdm-ironsworn-character 0)
(gethash 'shadow rpgdm-ironsworn-character 0)
(gethash 'wits rpgdm-ironsworn-character 0)
(rpgdm-ironsworn--display-stat 'health)
(rpgdm-ironsworn--display-stat 'spirit)
(rpgdm-ironsworn--display-stat 'supply)
(gethash 'momentum rpgdm-ironsworn-character 5)))
#+END_SRC
* Roll against Character Stats
Which allows us to create helper rolling functions:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-roll-stat (stat modifier)
"Roll an action based on a loaded character's STAT with a MODIFIER."
(interactive (list (completing-read "Stat Modifier: " '(Edge Heart Iron Shadow Wits))
(read-string "Other Modifier: ")))
(let ((all-mods (+ (gethash stat rpgdm-ironsworn-character)
(string-to-number modifier))))
(rpgdm-ironsworn-roll all-mods)))
#+END_SRC
And we could have a function for each:
#+BEGIN_SRC emacs-lisp :results silent
(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: ")))
(rpgdm-ironsworn-roll-stat 'edge modifier))
(defun rpgdm-ironsworn-roll-heart (modifier)
"Roll an action based on a loaded character's Heart stat with a MODIFIER."
(interactive (list (read-string "Heart + Modifier: ")))
(rpgdm-ironsworn-roll-stat 'heart modifier))
(defun rpgdm-ironsworn-roll-iron (modifier)
"Roll an action based on a loaded character's Iron stat with a MODIFIER."
(interactive (list (read-string "Iron + Modifier: ")))
(rpgdm-ironsworn-roll-stat 'iron modifier))
(defun rpgdm-ironsworn-roll-shadow (modifier)
"Roll an action based on a loaded character's Shadow stat with a MODIFIER."
(interactive (list (read-string "Shadow + Modifier: ")))
(rpgdm-ironsworn-roll-stat 'shadow modifier))
(defun rpgdm-ironsworn-roll-wits (modifier)
"Roll an action based on a loaded character's Wits stat with a MODIFIER."
(interactive (list (read-string "Wits + Modifier: ")))
(rpgdm-ironsworn-roll-stat 'wits modifier))
#+END_SRC
* Oracles
** Action-Theme
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-oracle-action-theme ()
"Rolls on two tables at one time."
(interactive)
(let ((action (rpgdm-tables-choose "actions"))
(theme (rpgdm-tables-choose "themes")))
(rpgdm-message "%s / %s" action theme)))
#+END_SRC
** Character
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-oracle-npc ()
(interactive)
(let ((name (rpgdm-tables-choose "names-ironlander"))
(goal (rpgdm-tables-choose "character-goal"))
(role (rpgdm-tables-choose "character-role"))
(activity (rpgdm-tables-choose "character-activity"))
(description (rpgdm-tables-choose "character-descriptor"))
(disposition (rpgdm-tables-choose "character-disposition")))
(rpgdm-message "%s, %s %s (Activity: %s Disposition: %s Goal: %s)"
name description role activity disposition goal)))
#+END_SRC
** Combat Action
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-oracle-combat ()
(interactive)
(let ((action (rpgdm-tables-choose "combat-action"))
(method (rpgdm-tables-choose "combat-event-method"))
(target (rpgdm-tables-choose "combat-event-target")))
(rpgdm-message "%s %s or %s" method target action)))
#+END_SRC
** Feature
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-oracle-feature ()
"Rolls on two tables at one time for a Site's feature."
(interactive)
(let ((aspect (rpgdm-tables-choose "feature-aspect"))
(focus (rpgdm-tables-choose "feature-focus")))
(rpgdm-message "%s / %s" aspect focus)))
#+END_SRC
** Site Nature
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-oracle-site-nature ()
"Rolls on two tables at one time for a random Site."
(interactive)
(let* ((theme (rpgdm-tables-choose "site-theme"))
(domain (rpgdm-tables-choose "site-domain"))
(place (downcase domain))
(name (rpgdm-ironsworn-oracle-site-name place)))
(rpgdm-message "%s %s :: %s" theme domain name)))
#+END_SRC
** Site Name
Using the interesting random name generator from the Ironsworn: Delve source book.
Requires a =place-type= to help limit the values that can be in /place/ and then looks up the details on the tables in the =ironsworn= directory.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-oracle-site-name (&optional place-type)
"Rolling on multiple tables to return a random site name."
(interactive (list (completing-read "Place type: "
'(barrow cavern icereach mine pass ruin
sea-cave shadowfen stronghold
tanglewood underkeep))))
(unless place-type
(setq place-type "unknown"))
(let ((description (rpgdm-tables-choose "site-name-description"))
(detail (rpgdm-tables-choose "site-name-detail"))
(namesake (rpgdm-tables-choose "site-name-namesake"))
(place (rpgdm-tables-choose (format "site-name-place-%s" place-type)))
(roll (rpgdm--roll-die 100)))
(rpgdm-message
(cond
((<= roll 25) (format "%s %s" description place))
((<= roll 50) (format "%s of %s" place detail))
((<= roll 70) (format "%s of %s %s" place description detail))
((<= roll 80) (format "%s of %s's %s" place namesake detail))
((<= roll 85) (format "%s's %s" namesake place))
((<= roll 95) (format "%s %s of %s" description place namesake))
(t (format "%s of %s" place namesake))))))
#+END_SRC
So, let's generate some random place names:
#+BEGIN_SRC emacs-lisp :tangle no
(rpgdm-ironsworn-oracle-site-name "barrow") ; "Tomb of Storms"
(rpgdm-ironsworn-oracle-site-name "cavern") ; "Lair of Khulans Truth"
(rpgdm-ironsworn-oracle-site-name "icereach") ; "Barrens of Erisias Winter"
(rpgdm-ironsworn-oracle-site-name "mine") ; "Lode of Ashen Lament"
(rpgdm-ironsworn-oracle-site-name "pass") ; "Sunken Highlands"
(rpgdm-ironsworn-oracle-site-name "ruin") ; "Sanctum of Khulans Truth"
(rpgdm-ironsworn-oracle-site-name "sea-cave") ; "Silent Caves"
(rpgdm-ironsworn-oracle-site-name "shadowfen") ; "Floodlands of Nightmare Despair"
(rpgdm-ironsworn-oracle-site-name "stronghold") ; "Crumbling Bastion"
(rpgdm-ironsworn-oracle-site-name "tanglewood") ; "Bramble of Endless Strife"
(rpgdm-ironsworn-oracle-site-name "underkeep") ; "Underkeep of Lament"
(rpgdm-ironsworn-oracle-site-name) ; "Sundered Mists of Khulan"
#+END_SRC
** Threat
Generate a random threat and its motivations.
#+BEGIN_SRC emacs-lisp
(defvar rpgdm-ironsworn-oracle-threats '("Burgeoning Conflict" "Ravaging Horde"
"Cursed Site" "Malignant Plague"
"Scheming Leader" "Zealous Cult"
"Environmental Calamity" "Power-Hungry Mystic"
"Rampaging Creature")
"A list of threats that correspond to tables")
(defun rpgdm-ironsworn-oracle-threat-goal (&optional category)
"Given a CATEGORY, display a threat goal."
(interactive (list (completing-read "Threat: " rpgdm-ironsworn-oracle-threats)))
(unless category
(setq category (seq-random-elt rpgdm-ironsworn-oracle-threats)))
(let ((table-name (format "threat-%s" (downcase (string-replace " " "-" category)))))
(rpgdm-message "%s: %s" category (rpgdm-tables-choose table-name))))
#+END_SRC
And we can have a random threat:
#+BEGIN_SRC emacs-lisp
(rpgdm-ironsworn-oracle-threat-goal)
#+END_SRC
* Ironsworn Interface
Ironsworn introduces a simplified /flip-a-coin/ oracle, that might be nice to integrate.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-oracle ()
"Given a LIKLIHOOD as a single character, return weighted coin flip."
(interactive)
(let* ((prompt "What are the odds?
c) Almost Certain l) Likely h) 50/50 u) Unlikely n) Small Chance ")
(odds (read-char prompt))
(roll (rpgdm--roll-die 100))
(yes! (when (or (and (= roll 11) (eq odds ?c))
(and (= roll 26) (eq odds ?l))
(and (= roll 51) (eq odds ?h))
(and (= roll 76) (eq odds ?u))
(and (= roll 91) (eq odds ?n)))
t))
(yes (when (or (and (> roll 11) (eq odds ?c))
(and (> roll 26) (eq odds ?l))
(and (> roll 51) (eq odds ?h))
(and (> roll 76) (eq odds ?u))
(and (> roll 91) (eq odds ?n)))
t)))
(rpgdm-message "%s %s %s"
(if yes! "Extreme" "")
(if yes "Yes" "No")
(if yes! "or a twist." ""))))
#+END_SRC
I'd like the Hydra to be more specific to Ironsworn:
#+BEGIN_SRC emacs-lisp :results silent
(defhydra hydra-rpgdm (:color blue :hint nil)
"
^Dice^ ^Adjust^ ^Oracles/Tables^ ^Moving^ ^Messages^
----------------------------------------------------------------------------------------------------------------------------------------------------
_d_: Roll Dice _D_: Progress Dice _H_: Health _z_: Yes/No Oracle _a_: Action/Theme _n_: NPC _o_: Links ⌘-‿: Show Stats
_e_: Roll Edge _s_: Roll Shadow _S_: Spirit _c_: Show Oracle _c_: Combat Action _f_: Feature _J_/_K_: Page up/dn ⌘-l: Last Results
_h_: Roll Heart _w_: Roll Wits _G_: Supply _O_: Other Oracle _p_: Place Name _P_: Place _N_/_W_: Narrow/Widen ⌘-k: ↑ Previous
_i_: Roll Iron _x_: Roll Stat _M_: Momentum _T_: Load Oracles _t_: Threat Goal ⌘-j: ↓ Next "
("d" rpgdm-ironsworn-roll) ("D" rpgdm-ironsworn-progress-roll)
("z" rpgdm-ironsworn-oracle) ("O" rpgdm-oracle)
("a" rpgdm-ironsworn-oracle-action-theme)
("n" rpgdm-ironsworn-oracle-npc)
("c" rpgdm-ironsworn-oracle-combat)
("f" rpgdm-ironsworn-oracle-feature)
("P" rpgdm-ironsworn-oracle-site-nature)
("p" rpgdm-ironsworn-oracle-site-name)
("t" rpgdm-ironsworn-oracle-threat-goal)
("e" rpgdm-ironsworn-roll-edge)
("h" rpgdm-ironsworn-roll-heart)
("i" rpgdm-ironsworn-roll-iron)
("s" rpgdm-ironsworn-roll-shadow)
("w" rpgdm-ironsworn-roll-wits)
("x" rpgdm-ironsworn-roll-stat :color pink)
("H" rpgdm-ironsworn-adjust-health :color pink)
("S" rpgdm-ironsworn-adjust-spirit :color pink)
("G" rpgdm-ironsworn-adjust-supply :color pink)
("M" rpgdm-ironsworn-adjust-momentum :color pink)
("T" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color pink)
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
("K" scroll-down :color pink) ("J" scroll-up :color pink)
("s-SPC" rpgdm-ironsworn-character-display)
("C-m" rpgdm-last-results :color pink)
("C-n" rpgdm-last-results-next :color pink)
("C-p" rpgdm-last-results-previous :color pink)
("s-l" rpgdm-last-results :color pink)
("s-j" rpgdm-last-results-next :color pink)
("s-k" rpgdm-last-results-previous :color pink)
("q" nil "quit") ("<f12>" nil))
#+END_SRC
* Summary
Funny that I wrote the code here before even playing the game. Hope I like playing it as much as hacking this out.
#+BEGIN_SRC emacs-lisp
(provide 'rpgdm-ironsworn)
;;; rpgdm-ironsworn.el ends here
#+END_SRC