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.
429 lines
19 KiB
Org Mode
429 lines
19 KiB
Org Mode
#+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 Khulan’s Truth"
|
||
(rpgdm-ironsworn-oracle-site-name "icereach") ; "Barrens of Erisia’s 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 Khulan’s 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
|