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.
This commit is contained in:
Howard Abrams 2021-12-30 08:26:42 -08:00
parent 1602a9b01d
commit 5455785b08
18 changed files with 1915 additions and 29 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/TODO.org

View file

@ -1,13 +1,16 @@
#+TITLE: Dungeon Master Support in Emacs
#+AUTHOR: Howard X. Abrams
#+EMAIL: howard.abrams@workday.com
#+DATE: 2021-01-27 January
#+TAGS: rpg
The overlap between Emacs and running a Dungeons and Dragon campaign is... expected? Jotting notes and plans in an org-mode file has been great, but what if, during a game session, my notes became /more interactive/? I started creating some helper functions, which has now become a minor mode I use as a sort of layer on top of Org.
The overlap between Emacs and running a Dungeons & Dragons campaign is... expected? Jotting notes and plans in an org-mode file has been great, but what if, during a game session, my notes became /more interactive/? I started creating some helper functions, which has now become a minor mode I use as a sort of layer on top of Org. Let's blame this insanity on the pandemic, but it has been fun.
The primary interface is =f13= which calls up a /sticky/ Hydra to call my functions, but still allowing full cursor movement (but without changing the code):
[[file:README-hydra.png]]
The primary interface is =f12= which calls up a /sticky/ Hydra to call my functions, but still allowing full cursor movement (mostly):
#+attr_html: :width 800px
[[file:images/screenshot-of-hydra.png]]
*Note:* The screenshot isn't accurate, as I will change the code more often than updating this image.
* Themes
What emerged from some late night hacking is more than just a dice roller, but I may have to explain my rationale.
** Yes/No Complications
@ -26,16 +29,23 @@ I love this idea, and thought that I could extend it to d20 skill check rolls. P
According to [[https://www.hipstersanddragons.com/difficulty-classes-for-ability-checks-5e/][this essay]], the standard DC 15 skill check is actually /too hard/ for most situations, however, I don't trust my bias when choosing a value for a difficulty check. For the life of me, I can't find where I saw this idea, but you could choose a number of d6s for the challenge (1 is easy, 2 is moderate, etc.) and then add 7. That sounds pretty good, so after calling for a check, I can enter it, and Emacs confirm the results.
I want to be able to wrap both of these ideas into a single interface.
** Oracle
The [[https://cursenightgames.itch.io/victors-gm-less-oracle][Victor's GM-less Oracle]] game, has an interesting way to make decisions that I thought I would code. Essentially, you can have a good, middle and bad situation (that you pre-determine), and the oracle roll tells you which it is. While the idea is to generate a bell curve, it attempts to heighten the tension with some rolls adding a /cumulative modifier/.
I'm not sure if I will use this in-place (or in addition) to the previous luck-with-complication idea.
** Random Items
As a DM, we often have to invent unplanned details to our story...especially names, but even the trinkets in the goblin's pocket. Most DMs make lists of things, and some even pin some of these to their DM Screen, so they can look a player in the eye, and say, "My name is Samuel Gustgiver, I work at the bakery, but my friends just call me Sam."
I wanted to create a directory full of files containing tabular goodness, and have a function that would read all the files, and then allow me to choose a random item from anything on the list, for instance:
[[file:README-choose-table.png]]
#+attr_html: :width 576px
[[file:images/screenshot-choose-table.png]]
And then having the results show up easily:
[[file:README-results.png]]
#+attr_html: :width 682px
[[file:images/screenshot-results.png]]
Oh, and when the players ask what the name of that strange NPC was, I made a function to display the last randomly displayed message.
@ -68,16 +78,15 @@ So /often/ is four times likelier than /rarely/. I also have this list:
- 1 -- =legendary=
Where /common/ is ten times more likelier than /legendary/. Actually, after all the work in getting this working, I'm not sure how often, in an epic fantasy game, where rare should be commonplace for the player.
The best part of this project is my collection of tables, however, I'm not sure what content falls under fair use and what is proprietary, so at the moment, create a =tables= directory when you clone this project, and start adding your own files.
** DM Screen and Roll from my Notes
Finally, I wanted to quickly bring up a collection of rules and tables along with my session notes, a bit of a DM Screen for my screen.
Two things I noticed about org files, is that I could initially hide unnecessary meta information and focus on just the contents of the file's table or list by prepending this blurb:
#+begin_example
# Local Variables:
# eval: (narrow-to-region 121 633)
# End:
#+end_example
#+attr_html: :width 576px
[[file:images/screenshot-emacs-variables.png]]
Keep in mind, that this is only good for more /static/ files that don't change, as I have to figure out the range.
@ -90,7 +99,7 @@ The second thing I realized is that Org's links can call Emacs functions. This a
My initial ideas for listing a bunch of random NPC names and having a link that displayed one of them, got supplanted for the ideas I described above.
* Code
What do I have here:
- [[file:rpgdm.el][rpgdm.el]] :: Primary interface offering:
- [[file:rpgdm.el][rpgdm]] :: Primary interface offering:
- =rpgdm-mode=, plus a Hydra interface for easily calling the rest of these functions.
- =rpgdm-yes-and-50/50=, flip a coin and make a give a result with or without complications or bonuses.
- =rpgdm-skill-check=, given a target and a d20 dice result, returns yes/no, but possibly with complications or bonuses, depending on how well the result is.
@ -108,3 +117,14 @@ What do I have here:
- =rpgdm-tables-choose= and choose from one of the tables dynamically, and a result is displayed.
- [[file:rpgdm-screen.el][rpgdm-screen]] :: Still working on this one
- =rpgdm-screen= :: to display some tables in buffer windows on the right side of the screen.
Complicated details of the code is stored as literate files in the [[file:docs/][docs]] directory, specifically:
- [[file:docs/rpgdm-tables-freq.org][rpgdm-tables-freq]] :: Parsing /frequency/ tables, entries with labels for stating how often the occur.
- [[file:docs/rpgdm-tables-dice.org][rpgdm-tables-dice]] :: Parsing standard tables with dice ranges, so copy/pasting from rule books /just work/.
I'm also intrigued with rulesets that are unique, for instance:
- [[file:docs/fate-rpg.org][FATE]] :: Easy character creation and a nice bell-curve dice roll, but it requires [[https://fudgerpg.com/products/fudge-dice.html][special Fudge dice]], that are easy enough to recreate in Emacs. See [[https://fate-srd.com/][fate-srd.com]] for details about this game.
- [[file:docs/mythic-rpg.org][Mythic RPG]] :: A nice RPG for solo play as it has a GM-less option that I wanted to capture, see [[https://www.wordmillgames.com/mythic-rpg.html][Wordmill Games]] for details.
- [[file:docs/ironsworn-rpg.org][Ironsworn]] :: Another good solo RPG, I wanted to capture its quick check resolution. See [[https://www.ironswornrpg.com/][ironswornrpg.com]] for the free rules.

164
docs/fate-rpg.org Normal file
View file

@ -0,0 +1,164 @@
#+TITLE: Fate and Fate Accelerated RPG
#+AUTHOR: Howard X. Abrams
#+DATE: 2021-08-25 August
#+TAGS: rpg fate
In my quest for a simple, but generic RPG, I've noticed FATE, and the toolbox for crafting your own stuff.
* Fate Dice
Obviously, step one for me in looking at a new game system is to render their dice, which I want to do slightly differently than returning a random number. This function returns both the random number and its /visual aspect/ of a =+=, =-=, or blank:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm--fate-die ()
"Return a cons of a Fate die and its visual representation."
(seq-random-elt '((-1 . "[-]")
( 0 . "[ ]")
( 1 . "[+]"))))
#+END_SRC
Instead of an even spread (like in many dice ranged from 1-20 or 1-100), we roll FATE dice in groups of four to create a /bell curve/. Sure, the range is -4 to +4, but those edges show up a lot less.
The odds are easy enough to calculate, but I like to see the percentage to roll the actual results, but also to see the change of getting the result or lower/higher, as in the final three columns:
| | *Odds* | Rolling | Rolling this | Rolling this |
| | | Exactly | or Higher | or Lower |
|----+-------+---------+--------------+--------------|
| +4 | 1/81 | 1.2% | 1.2% | 100.0% |
| +3 | 4/81 | 4.9% | 6.2% | 98.8% |
| +2 | 10/81 | 12.3% | 18.5% | 93.8% |
| +1 | 16/81 | 19.8% | 38.3% | 81.5% |
| 0 | 19/81 | 23.5% | 61.7% | 61.7% |
| -1 | 16/81 | 19.8% | 81.5% | 38.3% |
| -2 | 10/81 | 12.3% | 93.8% | 18.5% |
| -3 | 4/81 | 4.9% | 98.8% | 6.2% |
| -4 | 1/81 | 1.2% | 100.0% | 1.2% |
Let's have a function that can roll four dice and give a list of the tuples.
#+BEGIN_SRC emacs-lisp
(defun rpgdm--fate-roll-dice (&optional number)
"Return a list of Fate roll results. Each element of the list
is a cons of its value and its visual representation, see
`rpgdm--fate-die'."
punless number
(setq number 4))
(let (results)
(--dotimes number
(push (rpgdm--fate-die) results))
results))
#+END_SRC
So calling the =rpgdm--fate-role-dice= would return something like:
#+begin_src emacs-lisp :tangle no
'((0 . "[ ]")
(1 . "[+]")
(0 . "[ ]")
(-1 . "[-]"))
#+end_src
My UI will collect the roll and figure out how to sum and display the results as a list where the first element is the total and the second is a string that we could display.
#+BEGIN_SRC emacs-lisp
(defun rpgdm--fate-roll (modifier &optional number)
"Simulate a FATE dice roll and return the results.
Return a list where the first element is the total results,
and the second element is a user-formatted view of the results. "
(unless number (setq number 4))
(let* ((roll (rpgdm--fate-roll-dice number))
(vals (-map 'car roll))
(dice (-map 'cdr roll))
(roll (s-join " " dice))
(sum (apply '+ vals))
(total (+ sum modifier))
(results (propertize (number-to-string total) 'face '(:foreground "green"))))
(list total (format "Rolled: %s :: %s + %d" results roll modifier))))
#+END_SRC
And we make an interactive version:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-fate-roll (modifier)
"Simulate a FATE dice roll and display the results.
The total is the difference of the `+' to `-' plus the MODIFIER.
Note that the NUMBER of dice defaults to 4."
(interactive (list (rpgdm-fate-ladder "effort level")))
(rpgdm-message (second (rpgdm--fate-roll modifier 4))))
#+END_SRC
* Skill Checks
First step is the Ladder, which is a simple table containing the numeric ranking (to use in calculations), a key-mnemonic, and a descriptive /skill/ label. The final column is a descriptive label for /challenge/ level:
#+name: ladder-table
| -2 | t | Terrible | Trivial |
| -1 | p | Poor | Easy |
| 0 | m | Mediocre | Simple |
| 1 | a | Average | Average |
| 2 | f | Fair | |
| 3 | g | Good | Hard |
| 4 | r | Great | Daunting |
| 5 | s | Superb | Extreme |
| 6 | f | Fantastic | Impossible |
| 7 | e | Epic | |
| 8 | l | Legendary | |
And use that table as a global variable:
#+BEGIN_SRC emacs-lisp :var rpgdm-fate-ladder-values=ladder-table
(defvar rpgdm-fate-ladder rpgdm-fate-ladder-values "The FATE RPG ladder of challenge levels.")
#+END_SRC
Now we can use it to prompt for the
#+BEGIN_SRC emacs-lisp
(defun rpgdm-fate-ladder (&optional request-type)
"Prompt for a choice on the FATE ladder, and return the numeric value of that level.
The REQUEST-TYPE is an optional string inserted into the prompt to describe the request."
(interactive)
(unless request-type (setq request-type "challenge level"))
(let* ((choices (mapconcat 'rpgdm--fate-ladder-prompt rpgdm-fate-ladder " "))
(prompt (format "What is the %s?\n%s"
(propertize request-type 'face '(:foreground "yellow"))
choices))
(choice (char-to-string (read-char prompt)))
(entry (--filter (equal (second it) choice) rpgdm-fate-ladder)))
(first (first entry))))
#+END_SRC
This assumes that we can create a prompt from one entry in our table:
#+BEGIN_SRC emacs-lisp
(defun rpgdm--fate-ladder-prompt (entry)
(let* ((entry-number (format "[%d]" (first entry)))
(render-number (propertize entry-number
'face '(:foreground "#888888")))
(keyed-prompt (propertize (second entry) 'face '(:foreground "green"))))
(format "%s) %s %s" keyed-prompt (third entry) render-number))))
#+END_SRC
Let's prompt for both the challenge level as well as the current effort:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-fate-challenge (opposition-level effort-level)
"Return a user message for a FATE dice challenge.
Given a numeric EFFORT-LEVEL as well as the OPPOSITION-LEVEL,
this function rolls the Fate dice and interprets the results."
(interactive (list (rpgdm-fate-ladder "opposition level")
(rpgdm-fate-ladder "effort level")))
(let* ((die-roll (rpgdm--fate-roll effort-level))
(shifts (- (first die-roll) opposition-level))
(results (cond
((< shifts 0) (propertize "Failed" 'face '(:foregound "red")))
((= shifts 0) (propertize "Tie" 'face '(:foreground "yellow")))
((> shifts 3) (propertize "Succeed with Style!" 'face '(:foregound "green")))
(t (propertize "Success" 'face '(:foregound "green"))))))
(rpgdm-message "%s ... %s" results (second die-roll))))
#+END_SRC
#+PROPERTY: header-args:sh :tangle no
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-fate.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
# Local Variables:
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
# End:

429
docs/ironsworn-rpg.org Normal file
View file

@ -0,0 +1,429 @@
#+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

545
docs/mythic-rpg.org Normal file
View file

@ -0,0 +1,545 @@
#+TITLE: Mythic TTRPG
#+AUTHOR: Howard X. Abrams
#+DATE: 2021-08-09 August
#+TAGS: rpg
I'm intrigued with the possibilities associated with Tana Pigeons's (from Word Mill Games) TTRPG, [[http://wordmillgames.com/mythic-rpg.html][Mythic]], however, to make things quicker (at least for me), I'm going to try to render the Fate Chart and other aspects from the book.
To summarize, we ask ourselves questions about the situation our characters find themselves. These questions much adhere to these /rules/:
- The question is often what a player would ask their game master.
- The answer must be either /yes/ or /no/.
- The question must be /logical/ (for the story, not necessarily reality).
We then rate the /challenge/ or /likelihood/ of the question on the following scale (so send this list to all other placyers):
This is the /likelihood/ gradient levels:
- impossible
- no way
- very unlikely
- unlikely
- *50/50* ... note that this is the middle ground, and everything above is less likely to be a /yes/ and everything below this is /more likely/ to be yes.
- maybe
- likely
- probably
- near sure
- sure thing
- absolutely
And this is the /challenge levels/:
- trivial
- miniscule
- weak
- low
- below average (will accept a negative)
- average
- above average
- high
- exceptional
- incredible
- awesome
- superhuman
- superhuman+
We typically don't bother with the challenge levels, as we just allow our D&D rules carry these situations. Also, usually the person that asks the question allows the other players to determine the likelihood level.
#+BEGIN_SRC emacs-lisp :exports none
;;; rpgdm-mythic.org --- Functions to help when playing Mythic RPG. -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@gmail.com>
;; Created: 9 August 2021
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; This source code is literally extracted from an essay about creating an
;; user interface to the Mythic RPG game system. See the book,
;; http://wordmillgames.com/mythic-rpg.html
;;
;;; Code:
#+END_SRC
* Introduction
I'm not sure why I'm here. I guess I'm just curious. Mumma said that was what would kill me. Just like that tentacled kitten my father found in the woods. Here I am, at Her House.
Am I here to deliver something? No. Am I here to get something? No. Am I here to /learn/ something? Not that either. Am I hear to deliver a message? Hrm ... really rolling high. No, not that.
I guess I just came here because I was curious.
Does the place look peculiar or /odd/? Yes. An exceptional yes.
I came here to see if the rumors are true. The house, if you could call it a house isn't anything like the others at the village. Is it a tower? No. Does it even look like a house? No. She must live in a stone sphere here in the middle of the forest.
Is there a door? I would assume so. Yes, there is. Is the stone ball floating? I mean, I have heard about the magics and that seems like something she might employ. Yes. It is floating just out of reach. I suppose I have seen what I came to see. The rumors are true, she lives in a floating stone ball.
Does She have windows, or does She use magics to view the land around her dwelling. No, She uses magics. The stone is featureless, except for the door. I move closer and wonder how She goes in and out. Is there an invisible stair to aid her? Seems very likely, but (exceptional no) I can walk right underneath it without noticing that. But when I come closer, the stone sphere floats further away.
Curiouser. I wonder what She is like. Would she be angry if I knocked on the door? Assuming I could reach it. I want to know more about her, otherwise, I couldn't live with the mystery. I pick up a rock and throw it at the door, while I'm a pretty strong thrower, the door is far away.
The stone hits the door. Does someone open the door? Yes! After some time, the stone door creaks open. Is it Her? No! It must be one of Her servants. Is it human? (Exceptional no). A small goblin girl peers around looking for what made the noise. Does the goblin look angry? I didn't hurt the door, so maybe she is just as curious as I. But she is! Exceptionally so! I quickly hide in the forest undergrowth. Does she see me? Due to my curious nature, I've always been sneaky (else how would I have heard all the delicious rumors), however, the goblin looks straight at me, as if I wasn't even hiding.
Does she do something to me? Yes. Does she use magic? Yes.
Curiosity may have killed the tentacled kitten, but curiosity turned this human into a tentacled cat.
* Interface
Since I will be using Mythic features primarily while either writing (or at least, taking notes), the user interface needs to be un-intrusive. I'll just put everything under leader keys using Doom macros:
#+BEGIN_SRC emacs-lisp
(map! :leader (:prefix-map ("a" . "abrams")
(:prefix-map ("r" . "rpg-dm")
:desc "table choice" "c" 'rpgdm-tables-choose
:desc "roll the dice" "r" 'rpgdm-roll
:desc "best/middling/worse" "z" 'rpgdm-oracle
:desc "flip coin, and..." "a" 'rpgdm-yes-and-50/50
:desc "new adventure" "N" 'rpgdm-mythic-new-adventure
:desc "new scene" "n" 'rpgdm-mythic-new-scene
:desc "what are the odds?" "o" 'rpgdm-mythic-fate-odds
:desc "skill challenge" "f" 'rpgdm-mythic-fate-challenge)))
#+END_SRC
What I've noticed is that I would like to run these commands while actually editing, so let's have a new key sequence that begins with ~F19~ on my fancy keyboard.
#+BEGIN_SRC emacs-lisp
(global-set-key (kbd "<f19> c") 'rpgdm-tables-choose)
(global-set-key (kbd "<f19> r") 'rpgdm-roll)
(global-set-key (kbd "<f19> z") 'rpgdm-oracle)
(global-set-key (kbd "<f19> a") 'rpgdm-yes-and-50/50)
(global-set-key (kbd "<f19> N") 'rpgdm-mythic-new-adventure)
(global-set-key (kbd "<f19> n") 'rpgdm-mythic-new-scene)
(global-set-key (kbd "<f19> o") 'rpgdm-mythic-fate-odds)
(global-set-key (kbd "<f19> f") 'rpgdm-mythic-fate-challenge)
#+END_SRC
And because I use the /odds/ chart a lot more than anything else. I'm going to bind it to another, easier key.
#+BEGIN_SRC emacs-lisp
(global-set-key (kbd "<f15>") 'rpgdm-mythic-fate-odds)
#+END_SRC
Some of these functions are from my [[file:rpgdm.el][rpgdm.el]] code, while the Mythic-specific ones, we define here, primarily:
- =new-adventure= :: To create a directory structure of org-mode files to document/retell the adventure as it unfolds.
- =fate-odds= :: To determine, based on the current chaos level, what are the odds of something. I will use that a lot.
- =fate-challenge= :: The primary Fate Chart use case where two "levels" compete on a bell curve of chance.
** Questions?
Can I ask a lengthy question with a one letter response? This function will issue a prompt with all the ranks available, get a single keystroke (using the =read-char= function), and return the numeric value of the rank, we can send to the other functions for the Fate Chart.
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic-rank-level (&optional rank-type)
"Query user and return a numeric 'rank' level.
This number is from -5 to 7, where 0 is a average."
(let* ((prompt (format "Choose a %sMythic Rank:
t) Trivial m) Miniscule w) Weak l) Low b) Below Average a) Average A) Above Average
H) High E) Exceptional I) Incredible W) Awesome S) Superhuman V) Very Superhuman "
(or rank-type "")))
(ascii (read-char (rpgdm--prompt-emphasis prompt))))
(cond
((eq ascii ?t) -5) ; t -> trivial
((eq ascii ?m) -4) ; m -> miniscule
((eq ascii ?w) -3) ; w -> weak -
((eq ascii ?l) -2) ; l -> low - terrible
((or (eq ascii ?b) (eq ascii ?-)) -1) ; b -> below average (will accept a negative) - poor
((or (eq ascii ?A) (eq ascii ?1) (eq ascii ?+)) 1) ; A -> above average - average
((or (eq ascii ?H) (eq ascii ?h) (eq ascii ?2)) 2) ; h -> high - Fair
((or (eq ascii ?E) (eq ascii ?e) (eq ascii ?3)) 3) ; e -> exceptional - Good
((or (eq ascii ?I) (eq ascii ?i) (eq ascii ?4)) 4) ; i -> incredible - Great
((or (eq ascii ?W) (eq ascii ?5)) 5) ; W -> awesome - Superb
((or (eq ascii ?S) (eq ascii ?6)) 6) ; s -> superhuman - Fantastic
((or (eq ascii ?V) (eq ascii ?7)) 7) ; S -> superhuman+ - Epic
(t 0)))) ; * -> Average - mediocre
#+END_SRC
And we can also convert a list of /what are the odds/ for events and whatnot:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic-odds-level ()
"Query user and return a numeric 'odds' level.
This number is from -4 to 6, where 0 is a 50/50."
(let ((ascii (read-char (rpgdm--prompt-emphasis "What is the likelihood of your question?
i) Impossible n) No way v) Very unlikely u) Unlikely h) 50/50
M) Maybe L) Likely P) Probably N) Near Sure S) Sure thing A) Absolutely"))))
(cond
((eq ascii ?i) -4) ; i -> impossible
((eq ascii ?n) -3) ; n -> no way
((eq ascii ?v) -2) ; v -> very unlikely
((or (eq ascii ?u) (eq ascii ?-)) -1) ; u -> unlikely
((or (eq ascii ?M) (eq ascii ?m) (eq ascii ?1)) 1) ; M -> maybe
((or (eq ascii ?L) (eq ascii ?l) (eq ascii ?2)) 2) ; L -> likely
((or (eq ascii ?P) (eq ascii ?p) (eq ascii ?3)) 3) ; P -> probably
((or (eq ascii ?N) (eq ascii ?s) (eq ascii ?4)) 4) ; N -> near sure
((or (eq ascii ?S) (eq ascii ?t) (eq ascii ?5)) 5) ; S -> sure thing
((or (eq ascii ?A) (eq ascii ?a)
(eq ascii ?Y) (eq ascii ?y) (eq ascii ?6)) 6) ; A -> absolutely
(t 0)))) ; * -> 50/50
#+END_SRC
And finally, we need to get the /chaos level/. From the Mythic RPG book about the chaos level:
#+begin_quote
The higher the number, the more unexpected events occur. Chaos can also influence the results of odds questions.
The higher the chaos, the more frequently odds questions come up yes. Since yes answers usually add elements to an adventure, the higher the chaos factor, the more action you will have as a result of odds questions.
#+end_quote
While the chart's labels are 1 to 9, we need to convert them to the -4 to 4 range similar to the others:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic-chaos-level ()
"Query user and return a numeric 'chaos' level.
Where `1' means a stable environment where most yes/no questions
are no, and `9' is chaotic, and more often responds with yes.
Return number is from -4 to 4, where 0 is normal."
(let* ((prompt (format "What is the Chaos level (1-9)?
Where 1 is very stable (more noes), and 9 is chaotic (more yeses), and <RET> selects the current value, %d"
rpgdm-mythic-current-chaos-level))
(ascii (read-char prompt)))
(cond
((eq ascii ?9) -4)
((eq ascii ?8) -3)
((eq ascii ?7) -2)
((eq ascii ?6) -1)
((eq ascii ?5) 0)
((eq ascii ?4) 1)
((eq ascii ?3) 2)
((eq ascii ?2) 3)
((eq ascii ?1) 4)
(t (- rpgdm-mythic-current-chaos-level 5)))))
#+END_SRC
Would be nice to visually see the keystrokes in a different color. Can I do this automatically?
The =put-text-property= can change textual properties within a string, and since all of these prompts have a particular pattern, I can use it to easily identify the keys:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm--prompt-emphasis (message)
"Add emphasizing properties to the keystroke prompts in MESSAGE."
(let ((start 0)
(re (rx bow (one-or-more (not space)) ") ")))
(while (string-match re message start)
(let* ((key-start (match-beginning 0))
(key-end (1+ key-start))
(par-start key-end)
(par-end (1+ par-start)))
(put-text-property key-start key-end 'face '(:foreground "green") message)
(put-text-property par-start par-end 'face '(:foreground "#666666") message))
(setq start (match-end 0))))
message)
#+END_SRC
** Requests?
The initial request in Mythic RPG is the /challenge/. It requires two /ranks/, the initial actor (probably the player) vs. a contested /difficulty/ rank, like another NPC, or the strength of the door, etc. After querying for the ranks, we can just pass those values to the =rpgdm-mythic-fate-chart= function to do all the work:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic-fate-challenge (acting-rank acting-modifier difficulty-rank)
"Request a challange on the tables of fate.
Send a message of the results of rolling a d100 on the Mythic
Fate Chart. The ACTING-RANK and DIFFICULTY-RANK are numeric
values from -5 to 7 (with 0 being average)."
(interactive (list (rpgdm-mythic-rank-level "Acting ")
(read-number "Actiing modifier? " 0)
(rpgdm-mythic-rank-level "Difficulty ")))
(rpgdm-mythic-fate-chart "Challenge " (+ acting-rank acting-modifier) difficulty-rank))
#+END_SRC
What are the odds of something happening? A little luck, a little logic, and a bit of the /chaos/ associated with the unfolding of the story:
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic-fate-odds (odds chaos-level)
"Request a results of what are the odds on the tables of fate.
Send a message of the results of rolling a d100 on the Mythic
Fate Chart. The ODDS is the likelihood of something, and the
CHAOS-LEVEL is a numeric values about how likely yes answer
happen."
(interactive (list (rpgdm-mythic-odds-level)
(rpgdm-mythic-chaos-level)))
(rpgdm-mythic-fate-chart "Odds " odds chaos-level))
#+END_SRC
* Fate Chart
The main table/chart in the Mythic RPG is the *Fate Chart* that specifies all questions and conflict resolution. This interactive function should return the range, a d100 die roll, and also interpret the results. We can then use any aspect of the results.
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic-fate-chart (chart-type x-rank y-rank)
"Return a colorized message of rolling dice against the Fate Chart.
Use the X-RANK and Y-RANK as indexes in the Mythic RPG Fate
chart (see `rpgdm-mythic--fate-chart'), and format the collected
messages."
(let* ((range (rpgdm-mythic--fate-chart x-rank y-rank))
(roll (rpgdm--roll-die 100))
(results (rpgdm-mythic--result-message range roll))
(event? (rpgdm-mythic--result-event-p range roll))
(message (format "Mythic %s- %d < %d < %d :: %d ... %s %s"
chart-type (first range) (second range) (third range) roll results event?)))
(rpgdm-message (rpgdm-mythic--fate-chart-emphasize message))))
#+END_SRC
** The Actual Chart
Obviously, the author calculated the chart, and then simplified it to be easier when rolling dice.
Should we render the chart as an actual table?
#+name: fate-table
| | -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----|
| -5 | 50 | 25 | 10 | 5 | 5 | 0 | 0 | -20 | -20 | -40 | -40 | -55 | -65 |
| -4 | 75 | 50 | 25 | 15 | 10 | 5 | 5 | 0 | 0 | -20 | -20 | -35 | -45 |
| -3 | 90 | 75 | 50 | 35 | 25 | 15 | 10 | 5 | 5 | 0 | 0 | -15 | -25 |
| -2 | 95 | 85 | 65 | 50 | 45 | 25 | 15 | 10 | 5 | 5 | 5 | 5 | -15 |
| -1 | 100 | 90 | 75 | 55 | 50 | 35 | 20 | 15 | 10 | 5 | 5 | 0 | -10 |
| 0 | 105 | 95 | 85 | 75 | 65 | 50 | 35 | 25 | 15 | 10 | 10 | 5 | -5 |
| 1 | 110 | 95 | 90 | 85 | 80 | 65 | 50 | 45 | 25 | 20 | 15 | 5 | 0 |
| 2 | 115 | 100 | 95 | 90 | 85 | 75 | 55 | 50 | 35 | 25 | 20 | 10 | 5 |
| 3 | 120 | 105 | 95 | 95 | 90 | 85 | 75 | 65 | 50 | 45 | 35 | 15 | 5 |
| 4 | 125 | 115 | 100 | 95 | 95 | 90 | 80 | 75 | 55 | 50 | 45 | 20 | 10 |
| 5 | 130 | 125 | 110 | 95 | 95 | 90 | 85 | 80 | 65 | 55 | 50 | 25 | 10 |
| 6 | 150 | 145 | 130 | 100 | 100 | 95 | 95 | 90 | 85 | 80 | 75 | 50 | 25 |
| 7 | 170 | 165 | 150 | 120 | 120 | 100 | 100 | 95 | 95 | 90 | 90 | 75 | 50 |
We convert that table into a matrix (list of lists) in Emacs Lisp:
#+BEGIN_SRC emacs-lisp :var fate-table-values=fate-table :rownames yes :results silent
(defvar rpgdm-mythic-fate-table fate-table-values
"Only contains the medium boundary values of the Mythic Fate Chart.")
#+END_SRC
** Supporting Fate Functions
A lookup function for the Fate Chart would be nice, but since the fate table data starts from 0, but the Mythic chart /visually/ starts from -5, we need to add 5 to the given parameter values.
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic--fate-boundary (acting-rank difficulty-rank)
"Return the boundary value for a fate contest. The ACTING-RANK
and DIFFICULTY-RANK are numeric values from -5 to 7 that
correspond to the rows and colums of the Fate Chart from the
Mythic RPG."
(nth (+ difficulty-rank 5)
(nth (+ acting-rank 5) rpgdm-mythic-fate-table)))
#+END_SRC
Now, can we get the exceptional values from the given average/medium boundary value?
#+BEGIN_SRC emacs-lisp :results silent
(defun rpgdm-mythic--fate-chart (acting-rank difficulty-rank)
"Return a list of the lower, medium and upper boundaries of the Fate Chart.
The ACTING-RANK and DIFFICULTY-RANK are numeric values from -5 to
7 that correspond to the rows and colums of the Fate Chart, for
instance, a value of _weak_ on the chart is a -3, and a value of
_incredible_ is a 4."
(let* ((medium (rpgdm-mythic--fate-boundary acting-rank difficulty-rank))
(lower (if (> medium 0) (/ medium 5) 0))
(upper (if (< medium 100)
(- 100 (/ (- 100 medium) 5)) 100)))
(list lower medium upper)))
#+END_SRC
Let's make sure this works:
#+BEGIN_SRC emacs-lisp
(ert-deftest rpgdm-mythic--fate-chart-test ()
(should (equal (rpgdm-mythic--fate-chart 0 0) '(10 50 90)))
(should (equal (rpgdm-mythic--fate-chart 7 7) '(10 50 90)))
(should (equal (rpgdm-mythic--fate-chart -5 -5) '(10 50 90)))
(should (equal (rpgdm-mythic--fate-chart -5 7) '(0 -65 67)))
(should (equal (rpgdm-mythic--fate-chart 7 -5) '(34 170 100)))
(should (equal (rpgdm-mythic--fate-chart -1 2) '(3 15 83))))
#+END_SRC
Given a three-number range and the die roll results, we can respond with a yes/no:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic--result-message (chart-range die-roll)
"Return result message of a DIE-ROLL within the three numbers from the CHART-RANGE."
(cond
((<= die-roll (first chart-range)) "Exceptional yes")
((<= die-roll (second chart-range)) "Yes")
((<= die-roll (third chart-range)) "No")
(t "Exceptional no")))
#+END_SRC
If you roll a /yes/ result, and your the two d100 die have the same number (in other words, 11, 22, 33, etc), a random event occurs.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic--result-event-p (chart-range die-roll)
"Return a random event message of a DIE-ROLL within the yes values of the CHART-RANGE."
(let ((tens (/ die-roll 10))
(ones (mod die-roll 10)))
(when (and (= ones tens)
(<= die-roll (second chart-range)))
" <Random Event>")))
#+END_SRC
Let's make some tests to verify this:
#+begin_src emacs-lisp
(ert-deftest rpgdm-mythic--result-event-p-test ()
(should (rpgdm-mythic--result-event-p '(10 50 90) 11))
(should (rpgdm-mythic--result-event-p '(10 50 90) 44))
(should (not (rpgdm-mythic--result-event-p '(10 50 90) 88)))
(should (not (rpgdm-mythic--result-event-p '(10 50 90) 13))))
#+end_src
** Fate Chart Embellishment
Would be nice to tailor the message output to make it easier to read the roll:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic--fate-chart-emphasize (message)
"We make certain assumption about the format of the message."
(let ((roll-re (rx ":: " (group (one-or-more digit))))
(main-re (rx "< " (group (one-or-more digit)) " <"))
(punc-re (rx "..."))
(rest-re (rx "... " (group (one-or-more any)))))
(string-match roll-re message)
(put-text-property (match-beginning 1) (match-end 1) 'face '(:foreground "yellow") message)
(string-match main-re message)
(put-text-property (match-beginning 1) (match-end 1) 'face '(:foreground "green") message)
(string-match rest-re message)
(put-text-property (match-beginning 1) (match-end 1) 'face '(:foreground "white") message)
(string-match punc-re message)
(put-text-property (match-beginning 0) (match-end 0) 'face '(:foreground "#666666") message)
message))
#+END_SRC
** Chaos Level
While the Chaos level is a single digit, I have =Return= on the =rpgdm-mythic-choose-chaos-level= select the game's /current/ chaos level from this variable:
#+BEGIN_SRC emacs-lisp
(defvar rpgdm-mythic-current-chaos-level 5 "The current, and adjustable, default chaos level")
#+END_SRC
And a function to adjust it:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic-set-chaos-level (level)
"Change the `rpgdm-mythic-current-chaos-level' to LEVEL.
This value can be an absolute number, or can be a relative value
if prefixed with a `+' or `-'. Notice it takes a string..."
(interactive "sNew chaos level for current game: ")
(let* ((parse-re (rx (optional (group (any "+" "-")))
(group digit)))
(matcher (string-match parse-re level))
(sign (match-string 1 level))
(value (string-to-number (match-string 2 level))))
(setq rpgdm-mythic-current-chaos-level (cond
((equal sign "=") (- rpgdm-mythic-current-chaos-level value))
((equal sign "+") (+ rpgdm-mythic-current-chaos-level value))
((> value 0) value)))))
#+END_SRC
However, most adjustments are up or down a single number, so maybe we have these functions:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic-increase-chaos-level ()
"Increase the current chaos level by 1."
(interactive)
(setq rpgdm-mythic-current-chaos-level (1+ rpgdm-mythic-current-chaos-level)))
(defun rpgdm-mythic-decrease-chaos-level ()
"Decrease the current chaos level by 1."
(interactive)
(setq rpgdm-mythic-current-chaos-level (1- rpgdm-mythic-current-chaos-level)))
#+END_SRC
* Random Events
Now that we have our [[file:rpgdm-tables.el][tables code]], displaying a /random event/ is pretty trivial:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic-random-event ()
"Use `rpgdm-tables-choose' to select items from tables designed for Mythic.
Make sure the following tables have been loaded before calling this function:
• `mythic/event-focus'
• `mythic/actions'
• `mythic/subject'
Perhaps we should make sure that we load this beforehand."
(interactive)
(let ((focus (rpgdm-tables-choose "mythic/event-focus"))
(action (rpgdm-tables-choose "mythic/actions"))
(subject (rpgdm-tables-choose "mythic/subject")))
(rpgdm-message "Mythic Random Event- Focus: %s Action: %s Subject: %s"
(propertize focus 'face '(:foreground "yellow"))
(propertize action 'face '(:foreground "green"))
(propertize subject 'face '(:foreground "green")))))
#+END_SRC
* Adventure Sequence
Not sure what I want to do for an adventure sequence, but perhaps this could be a collection of files in a directory, with each file a scene. We might have sub-directories for the various lists:
- Player Characters
- Non-Player Characters
- Threads (remember PCs should have their own threads)
- Other Information:
- Skill Scaling boxes (what is average, what is above and below that)
- Resolution charts (types of acting ranks and what opposes it and modifiers)
And of course, somewhere is the current chaos factor, which starts at 5, but changes /after/ each scene.
Note: The higher the chaos factor, the greater likelihood of a random event occurring and the greater are the odds of fate questions coming out yes.
If nothing unexpected happened (or if the PCs are /in control/), lower the chaos factor, otherwise, increase it by one.
** New Scene
While each scene should be /logical/, Mythic rolls could interrupt or alter it, based on the current level of Chaos.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic-new-scene (chaos-factor)
"After planning the next scene, call this to see if that happens.
The scene may change based on the given CHAOS-FACTOR.
The message display may state that the scene is altered or interrupted."
(interactive (list (rpgdm-mythic-chaos-level)))
(let* ((roll (rpgdm--roll-die 10))
(mess (cond
((and (<= roll chaos-factor) (evenp roll)) "The scene is interrupted")
((and (<= roll chaos-factor) (oddp roll)) "The scene is altered")
(t "The scene proceeds as planned"))))
(rpgdm-message mess)))
#+END_SRC
** Taking Notes
To keep things straight and consistent, one really should take judicious notes. Perhaps what should begin with some sort of skeletal template creation, and a collection of YAS snippets for autoinserting the contents.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-mythic-new-adventure (name)
"Create directory structure for adventure of NAME."
(interactive "sName this adventure: ")
(let* ((dirname (->> "Hunting Goblins"
(downcase)
(s-replace-all '((" " . "-")))))
(fullpath (format "~/projects/mythic-adventures/%s" dirname))
(overview (format "%s/overview.org" fullpath)))
(make-directory (format "%s/players" fullpath) t)
(make-directory (format "%s/characters" fullpath) t)
(make-directory (format "%s/scaling-boxes" fullpath) t)
(make-directory (format "%s/resolution-charts" fullpath) t)
(find-file overview)))
#+END_SRC
And the connection for snippets for each directory:
#+BEGIN_SRC emacs-lisp
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/players/")
:trigger "__mythic_rpg_player" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/characters/")
:trigger "__mythic_rpg_character" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/scaling-boxes/")
:trigger "__mythic_rpg_scaling" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/resolution-charts/")
:trigger "__mythic_rpg_resolution" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/overview.org")
:trigger "__mythic_rpg_overview" :mode 'org-mode)
#+END_SRC
The =set-file-template!= function is a Doom helper function, but it should be clear what snippet templates should be use for various directories.
* 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-mythic)
;;; rpgdm.el ends here
#+END_SRC
#+DESCRIPTION: A literate programming file for generating support functions for Mythic RPG
#+PROPERTY: header-args:sh :tangle no
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-mythic.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
# Local Variables:
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
# End:

View file

@ -1,12 +1,9 @@
#+title: Dice Tables for Games
#+author: Howard X. Abrams
#+email: howard.abrams@gmail.com
#+FILETAGS: :org-mode:emacs:rpgdm:
#+STARTUP: inlineimages yes
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-tables-dice.el :comments no
#+PROPERTY: header-args :eval no-export
#+PROPERTY: header-args :results silent
#+PROPERTY: header-args :exports both
#+PROPERTY: header-args :eval no-export :results silent :exports both
#+BEGIN_SRC emacs-lisp
;;; rpgdm-tables-dice.el --- Rolling dice for choosing items from Tables -*- lexical-binding: t; -*-

View file

@ -1,12 +1,9 @@
#+title: Frequency Tables for Games
#+author: Howard X. Abrams
#+email: howard.abrams@gmail.com
#+FILETAGS: :org-mode:emacs:rpgdm:
#+STARTUP: inlineimages yes
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-tables-freq.el :comments no
#+PROPERTY: header-args :eval no-export
#+PROPERTY: header-args :results silent
#+PROPERTY: header-args :exports both
#+PROPERTY: header-args :eval no-export :results silent :exports both
#+BEGIN_SRC emacs-lisp
;;; rpgdm-tables-freq.el --- Rolling dice for choosing items from Tables -*- lexical-binding: t; -*-

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -3,7 +3,7 @@
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@workday.com>
;; Maintainer: Howard X. Abrams
;; Created: February 26, 2021
;;
;; This file is not part of GNU Emacs.

76
rpgdm-fate.el Normal file
View file

@ -0,0 +1,76 @@
(defun rpgdm--fate-die ()
"Return a cons of a Fate die and its visual representation."
(seq-random-elt '((-1 . "[-]")
( 0 . "[ ]")
( 1 . "[+]"))))
(defun rpgdm--fate-roll-dice (&optional number)
"Return a list of Fate roll results. Each element of the list
is a cons of its value and its visual representation, see
`rpgdm--fate-die'."
punless number
(setq number 4))
(let (results)
(--dotimes number
(push (rpgdm--fate-die) results))
results))
(defun rpgdm--fate-roll (modifier &optional number)
"Simulate a FATE dice roll and return the results.
Return a list where the first element is the total results,
and the second element is a user-formatted view of the results. "
(unless number (setq number 4))
(let* ((roll (rpgdm--fate-roll-dice number))
(vals (-map 'car roll))
(dice (-map 'cdr roll))
(roll (s-join " " dice))
(sum (apply '+ vals))
(total (+ sum modifier))
(results (propertize (number-to-string total) 'face '(:foreground "green"))))
(list total (format "Rolled: %s :: %s + %d" results roll modifier))))
(defun rpgdm-fate-roll (modifier)
"Simulate a FATE dice roll and display the results.
The total is the difference of the `+' to `-' plus the MODIFIER.
Note that the NUMBER of dice defaults to 4."
(interactive (list (rpgdm-fate-ladder "effort level")))
(rpgdm-message (second (rpgdm--fate-roll modifier 4))))
(let ((rpgdm-fate-ladder-values '((-2 "t" "Terrible" "Trivial") (-1 "p" "Poor" "Easy") (0 "m" "Mediocre" "Simple") (1 "a" "Average" "Average") (2 "f" "Fair" "") (3 "g" "Good" "Hard") (4 "r" "Great" "Daunting") (5 "s" "Superb" "Extreme") (6 "f" "Fantastic" "Impossible") (7 "e" "Epic" "") (8 "l" "Legendary" ""))))
(defvar rpgdm-fate-ladder rpgdm-fate-ladder-values "The FATE RPG ladder of challenge levels.")
)
(defun rpgdm-fate-ladder (&optional request-type)
"Prompt for a choice on the FATE ladder, and return the numeric value of that level.
The REQUEST-TYPE is an optional string inserted into the prompt to describe the request."
(interactive)
(unless request-type (setq request-type "challenge level"))
(let* ((choices (mapconcat 'rpgdm--fate-ladder-prompt rpgdm-fate-ladder " "))
(prompt (format "What is the %s?\n%s"
(propertize request-type 'face '(:foreground "yellow"))
choices))
(choice (char-to-string (read-char prompt)))
(entry (--filter (equal (second it) choice) rpgdm-fate-ladder)))
(first (first entry))))
(defun rpgdm--fate-ladder-prompt (entry)
(let* ((entry-number (format "[%d]" (first entry)))
(render-number (propertize entry-number
'face '(:foreground "#888888")))
(keyed-prompt (propertize (second entry) 'face '(:foreground "green"))))
(format "%s) %s %s" keyed-prompt (third entry) render-number))))
(defun rpgdm-fate-challenge (opposition-level effort-level)
"Return a user message for a FATE dice challenge.
Given a numeric EFFORT-LEVEL as well as the OPPOSITION-LEVEL,
this function rolls the Fate dice and interprets the results."
(interactive (list (rpgdm-fate-ladder "opposition level")
(rpgdm-fate-ladder "effort level")))
(let* ((die-roll (rpgdm--fate-roll effort-level))
(shifts (- (first die-roll) opposition-level))
(results (cond
((< shifts 0) (propertize "Failed" 'face '(:foregound "red")))
((= shifts 0) (propertize "Tie" 'face '(:foreground "yellow")))
((> shifts 3) (propertize "Succeed with Style!" 'face '(:foregound "green")))
(t (propertize "Success" 'face '(:foregound "green"))))))
(rpgdm-message "%s ... %s" results (second die-roll))))

313
rpgdm-ironsworn.el Normal file
View file

@ -0,0 +1,313 @@
(require 'rpgdm)
(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)))
(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))))
(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))))
(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")
(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))
(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))
(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)))
(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)))
(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))
(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)))
(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)))
(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)))
(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)))
(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)))
(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))))))
(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))))
(rpgdm-ironsworn-oracle-threat-goal)
(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." ""))))
(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))
(provide 'rpgdm-ironsworn)
;;; rpgdm-ironsworn.el ends here

304
rpgdm-mythic.el Normal file
View file

@ -0,0 +1,304 @@
;;; rpgdm-mythic.org --- Functions to help when playing Mythic RPG. -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@gmail.com>
;; Created: 9 August 2021
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; This source code is literally extracted from an essay about creating an
;; user interface to the Mythic RPG game system. See the book,
;; http://wordmillgames.com/mythic-rpg.html
;;
;;; Code:
(map! :leader (:prefix-map ("a" . "abrams")
(:prefix-map ("r" . "rpg-dm")
:desc "table choice" "c" 'rpgdm-tables-choose
:desc "roll the dice" "r" 'rpgdm-roll
:desc "best/middling/worse" "z" 'rpgdm-oracle
:desc "flip coin, and..." "a" 'rpgdm-yes-and-50/50
:desc "new adventure" "N" 'rpgdm-mythic-new-adventure
:desc "new scene" "n" 'rpgdm-mythic-new-scene
:desc "what are the odds?" "o" 'rpgdm-mythic-fate-odds
:desc "skill challenge" "f" 'rpgdm-mythic-fate-challenge)))
(global-set-key (kbd "<f19> c") 'rpgdm-tables-choose)
(global-set-key (kbd "<f19> r") 'rpgdm-roll)
(global-set-key (kbd "<f19> z") 'rpgdm-oracle)
(global-set-key (kbd "<f19> a") 'rpgdm-yes-and-50/50)
(global-set-key (kbd "<f19> N") 'rpgdm-mythic-new-adventure)
(global-set-key (kbd "<f19> n") 'rpgdm-mythic-new-scene)
(global-set-key (kbd "<f19> o") 'rpgdm-mythic-fate-odds)
(global-set-key (kbd "<f19> f") 'rpgdm-mythic-fate-challenge)
(global-set-key (kbd "<f15>") 'rpgdm-mythic-fate-odds)
(defun rpgdm-mythic-rank-level (&optional rank-type)
"Query user and return a numeric 'rank' level.
This number is from -5 to 7, where 0 is a average."
(let* ((prompt (format "Choose a %sMythic Rank:
t) Trivial m) Miniscule w) Weak l) Low b) Below Average a) Average A) Above Average
H) High E) Exceptional I) Incredible W) Awesome S) Superhuman V) Very Superhuman "
(or rank-type "")))
(ascii (read-char (rpgdm--prompt-emphasis prompt))))
(cond
((eq ascii ?t) -5) ; t -> trivial
((eq ascii ?m) -4) ; m -> miniscule
((eq ascii ?w) -3) ; w -> weak -
((eq ascii ?l) -2) ; l -> low - terrible
((or (eq ascii ?b) (eq ascii ?-)) -1) ; b -> below average (will accept a negative) - poor
((or (eq ascii ?A) (eq ascii ?1) (eq ascii ?+)) 1) ; A -> above average - average
((or (eq ascii ?H) (eq ascii ?h) (eq ascii ?2)) 2) ; h -> high - Fair
((or (eq ascii ?E) (eq ascii ?e) (eq ascii ?3)) 3) ; e -> exceptional - Good
((or (eq ascii ?I) (eq ascii ?i) (eq ascii ?4)) 4) ; i -> incredible - Great
((or (eq ascii ?W) (eq ascii ?5)) 5) ; W -> awesome - Superb
((or (eq ascii ?S) (eq ascii ?6)) 6) ; s -> superhuman - Fantastic
((or (eq ascii ?V) (eq ascii ?7)) 7) ; S -> superhuman+ - Epic
(t 0)))) ; * -> Average - mediocre
(defun rpgdm-mythic-odds-level ()
"Query user and return a numeric 'odds' level.
This number is from -4 to 6, where 0 is a 50/50."
(let ((ascii (read-char (rpgdm--prompt-emphasis "What is the likelihood of your question?
i) Impossible n) No way v) Very unlikely u) Unlikely h) 50/50
M) Maybe L) Likely P) Probably N) Near Sure S) Sure thing A) Absolutely"))))
(cond
((eq ascii ?i) -4) ; i -> impossible
((eq ascii ?n) -3) ; n -> no way
((eq ascii ?v) -2) ; v -> very unlikely
((or (eq ascii ?u) (eq ascii ?-)) -1) ; u -> unlikely
((or (eq ascii ?M) (eq ascii ?m) (eq ascii ?1)) 1) ; M -> maybe
((or (eq ascii ?L) (eq ascii ?l) (eq ascii ?2)) 2) ; L -> likely
((or (eq ascii ?P) (eq ascii ?p) (eq ascii ?3)) 3) ; P -> probably
((or (eq ascii ?N) (eq ascii ?s) (eq ascii ?4)) 4) ; N -> near sure
((or (eq ascii ?S) (eq ascii ?t) (eq ascii ?5)) 5) ; S -> sure thing
((or (eq ascii ?A) (eq ascii ?a)
(eq ascii ?Y) (eq ascii ?y) (eq ascii ?6)) 6) ; A -> absolutely
(t 0)))) ; * -> 50/50
(defun rpgdm-mythic-chaos-level ()
"Query user and return a numeric 'chaos' level.
Where `1' means a stable environment where most yes/no questions
are no, and `9' is chaotic, and more often responds with yes.
Return number is from -4 to 4, where 0 is normal."
(let* ((prompt (format "What is the Chaos level (1-9)?
Where 1 is very stable (more noes), and 9 is chaotic (more yeses), and <RET> selects the current value, %d"
rpgdm-mythic-current-chaos-level))
(ascii (read-char prompt)))
(cond
((eq ascii ?9) -4)
((eq ascii ?8) -3)
((eq ascii ?7) -2)
((eq ascii ?6) -1)
((eq ascii ?5) 0)
((eq ascii ?4) 1)
((eq ascii ?3) 2)
((eq ascii ?2) 3)
((eq ascii ?1) 4)
(t (- rpgdm-mythic-current-chaos-level 5)))))
(defun rpgdm--prompt-emphasis (message)
"Add emphasizing properties to the keystroke prompts in MESSAGE."
(let ((start 0)
(re (rx bow (one-or-more (not space)) ") ")))
(while (string-match re message start)
(let* ((key-start (match-beginning 0))
(key-end (1+ key-start))
(par-start key-end)
(par-end (1+ par-start)))
(put-text-property key-start key-end 'face '(:foreground "green") message)
(put-text-property par-start par-end 'face '(:foreground "#666666") message))
(setq start (match-end 0))))
message)
(defun rpgdm-mythic-fate-challenge (acting-rank acting-modifier difficulty-rank)
"Request a challange on the tables of fate.
Send a message of the results of rolling a d100 on the Mythic
Fate Chart. The ACTING-RANK and DIFFICULTY-RANK are numeric
values from -5 to 7 (with 0 being average)."
(interactive (list (rpgdm-mythic-rank-level "Acting ")
(read-number "Actiing modifier? " 0)
(rpgdm-mythic-rank-level "Difficulty ")))
(rpgdm-mythic-fate-chart "Challenge " (+ acting-rank acting-modifier) difficulty-rank))
(defun rpgdm-mythic-fate-odds (odds chaos-level)
"Request a results of what are the odds on the tables of fate.
Send a message of the results of rolling a d100 on the Mythic
Fate Chart. The ODDS is the likelihood of something, and the
CHAOS-LEVEL is a numeric values about how likely yes answer
happen."
(interactive (list (rpgdm-mythic-odds-level)
(rpgdm-mythic-chaos-level)))
(rpgdm-mythic-fate-chart "Odds " odds chaos-level))
(defun rpgdm-mythic-fate-chart (chart-type x-rank y-rank)
"Return a colorized message of rolling dice against the Fate Chart.
Use the X-RANK and Y-RANK as indexes in the Mythic RPG Fate
chart (see `rpgdm-mythic--fate-chart'), and format the collected
messages."
(let* ((range (rpgdm-mythic--fate-chart x-rank y-rank))
(roll (rpgdm--roll-die 100))
(results (rpgdm-mythic--result-message range roll))
(event? (rpgdm-mythic--result-event-p range roll))
(message (format "Mythic %s- %d < %d < %d :: %d ... %s %s"
chart-type (first range) (second range) (third range) roll results event?)))
(rpgdm-message (rpgdm-mythic--fate-chart-emphasize message))))
(let ((fate-table-values '((50 25 10 5 5 0 0 -20 -20 -40 -40 -55 -65) (75 50 25 15 10 5 5 0 0 -20 -20 -35 -45) (90 75 50 35 25 15 10 5 5 0 0 -15 -25) (95 85 65 50 45 25 15 10 5 5 5 5 -15) (100 90 75 55 50 35 20 15 10 5 5 0 -10) (105 95 85 75 65 50 35 25 15 10 10 5 -5) (110 95 90 85 80 65 50 45 25 20 15 5 0) (115 100 95 90 85 75 55 50 35 25 20 10 5) (120 105 95 95 90 85 75 65 50 45 35 15 5) (125 115 100 95 95 90 80 75 55 50 45 20 10) (130 125 110 95 95 90 85 80 65 55 50 25 10) (150 145 130 100 100 95 95 90 85 80 75 50 25) (170 165 150 120 120 100 100 95 95 90 90 75 50))))
(defvar rpgdm-mythic-fate-table fate-table-values
"Only contains the medium boundary values of the Mythic Fate Chart.")
)
(defun rpgdm-mythic--fate-boundary (acting-rank difficulty-rank)
"Return the boundary value for a fate contest. The ACTING-RANK
and DIFFICULTY-RANK are numeric values from -5 to 7 that
correspond to the rows and colums of the Fate Chart from the
Mythic RPG."
(nth (+ difficulty-rank 5)
(nth (+ acting-rank 5) rpgdm-mythic-fate-table)))
(defun rpgdm-mythic--fate-chart (acting-rank difficulty-rank)
"Return a list of the lower, medium and upper boundaries of the Fate Chart.
The ACTING-RANK and DIFFICULTY-RANK are numeric values from -5 to
7 that correspond to the rows and colums of the Fate Chart, for
instance, a value of _weak_ on the chart is a -3, and a value of
_incredible_ is a 4."
(let* ((medium (rpgdm-mythic--fate-boundary acting-rank difficulty-rank))
(lower (if (> medium 0) (/ medium 5) 0))
(upper (if (< medium 100)
(- 100 (/ (- 100 medium) 5)) 100)))
(list lower medium upper)))
(ert-deftest rpgdm-mythic--fate-chart-test ()
(should (equal (rpgdm-mythic--fate-chart 0 0) '(10 50 90)))
(should (equal (rpgdm-mythic--fate-chart 7 7) '(10 50 90)))
(should (equal (rpgdm-mythic--fate-chart -5 -5) '(10 50 90)))
(should (equal (rpgdm-mythic--fate-chart -5 7) '(0 -65 67)))
(should (equal (rpgdm-mythic--fate-chart 7 -5) '(34 170 100)))
(should (equal (rpgdm-mythic--fate-chart -1 2) '(3 15 83))))
(defun rpgdm-mythic--result-message (chart-range die-roll)
"Return result message of a DIE-ROLL within the three numbers from the CHART-RANGE."
(cond
((<= die-roll (first chart-range)) "Exceptional yes")
((<= die-roll (second chart-range)) "Yes")
((<= die-roll (third chart-range)) "No")
(t "Exceptional no")))
(defun rpgdm-mythic--result-event-p (chart-range die-roll)
"Return a random event message of a DIE-ROLL within the yes values of the CHART-RANGE."
(let ((tens (/ die-roll 10))
(ones (mod die-roll 10)))
(when (and (= ones tens)
(<= die-roll (second chart-range)))
" <Random Event>")))
(ert-deftest rpgdm-mythic--result-event-p-test ()
(should (rpgdm-mythic--result-event-p '(10 50 90) 11))
(should (rpgdm-mythic--result-event-p '(10 50 90) 44))
(should (not (rpgdm-mythic--result-event-p '(10 50 90) 88)))
(should (not (rpgdm-mythic--result-event-p '(10 50 90) 13))))
(defun rpgdm-mythic--fate-chart-emphasize (message)
"We make certain assumption about the format of the message."
(let ((roll-re (rx ":: " (group (one-or-more digit))))
(main-re (rx "< " (group (one-or-more digit)) " <"))
(punc-re (rx "..."))
(rest-re (rx "... " (group (one-or-more any)))))
(string-match roll-re message)
(put-text-property (match-beginning 1) (match-end 1) 'face '(:foreground "yellow") message)
(string-match main-re message)
(put-text-property (match-beginning 1) (match-end 1) 'face '(:foreground "green") message)
(string-match rest-re message)
(put-text-property (match-beginning 1) (match-end 1) 'face '(:foreground "white") message)
(string-match punc-re message)
(put-text-property (match-beginning 0) (match-end 0) 'face '(:foreground "#666666") message)
message))
(defvar rpgdm-mythic-current-chaos-level 5 "The current, and adjustable, default chaos level")
(defun rpgdm-mythic-set-chaos-level (level)
"Change the `rpgdm-mythic-current-chaos-level' to LEVEL.
This value can be an absolute number, or can be a relative value
if prefixed with a `+' or `-'. Notice it takes a string..."
(interactive "sNew chaos level for current game: ")
(let* ((parse-re (rx (optional (group (any "+" "-")))
(group digit)))
(matcher (string-match parse-re level))
(sign (match-string 1 level))
(value (string-to-number (match-string 2 level))))
(setq rpgdm-mythic-current-chaos-level (cond
((equal sign "=") (- rpgdm-mythic-current-chaos-level value))
((equal sign "+") (+ rpgdm-mythic-current-chaos-level value))
((> value 0) value)))))
(defun rpgdm-mythic-increase-chaos-level ()
"Increase the current chaos level by 1."
(interactive)
(setq rpgdm-mythic-current-chaos-level (1+ rpgdm-mythic-current-chaos-level)))
(defun rpgdm-mythic-decrease-chaos-level ()
"Decrease the current chaos level by 1."
(interactive)
(setq rpgdm-mythic-current-chaos-level (1- rpgdm-mythic-current-chaos-level)))
(defun rpgdm-mythic-random-event ()
"Use `rpgdm-tables-choose' to select items from tables designed for Mythic.
Make sure the following tables have been loaded before calling this function:
`mythic/event-focus'
`mythic/actions'
`mythic/subject'
Perhaps we should make sure that we load this beforehand."
(interactive)
(let ((focus (rpgdm-tables-choose "mythic/event-focus"))
(action (rpgdm-tables-choose "mythic/actions"))
(subject (rpgdm-tables-choose "mythic/subject")))
(rpgdm-message "Mythic Random Event- Focus: %s Action: %s Subject: %s"
(propertize focus 'face '(:foreground "yellow"))
(propertize action 'face '(:foreground "green"))
(propertize subject 'face '(:foreground "green")))))
(defun rpgdm-mythic-new-scene (chaos-factor)
"After planning the next scene, call this to see if that happens.
The scene may change based on the given CHAOS-FACTOR.
The message display may state that the scene is altered or interrupted."
(interactive (list (rpgdm-mythic-chaos-level)))
(let* ((roll (rpgdm--roll-die 10))
(mess (cond
((and (<= roll chaos-factor) (evenp roll)) "The scene is interrupted")
((and (<= roll chaos-factor) (oddp roll)) "The scene is altered")
(t "The scene proceeds as planned"))))
(rpgdm-message mess)))
(defun rpgdm-mythic-new-adventure (name)
"Create directory structure for adventure of NAME."
(interactive "sName this adventure: ")
(let* ((dirname (->> "Hunting Goblins"
(downcase)
(s-replace-all '((" " . "-")))))
(fullpath (format "~/projects/mythic-adventures/%s" dirname))
(overview (format "%s/overview.org" fullpath)))
(make-directory (format "%s/players" fullpath) t)
(make-directory (format "%s/characters" fullpath) t)
(make-directory (format "%s/scaling-boxes" fullpath) t)
(make-directory (format "%s/resolution-charts" fullpath) t)
(find-file overview)))
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/players/")
:trigger "__mythic_rpg_player" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/characters/")
:trigger "__mythic_rpg_character" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/scaling-boxes/")
:trigger "__mythic_rpg_scaling" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/" (one-or-more any) "/resolution-charts/")
:trigger "__mythic_rpg_resolution" :mode 'org-mode)
(set-file-template! (rx "/mythic-adventures/overview.org")
:trigger "__mythic_rpg_overview" :mode 'org-mode)
(provide 'rpgdm-mythic)
;;; rpgdm.el ends here

View file

@ -3,7 +3,7 @@
;; Copyright (C) 2020 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@gmail.com>
;; Maintainer: Howard X. Abrams
;; Created: December 15, 2020
;;
;;; Commentary:

View file

@ -3,7 +3,7 @@
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@workday.com>
;; Maintainer: Howard X. Abrams
;; Created: January 8, 2021
;;
;; This file is not part of GNU Emacs.
@ -38,6 +38,10 @@
(defvar rpgdm-tables (make-hash-table :test 'equal)
"Collection of tables and lists for the Dungeon Master.")
(defun rpgdm-tables-clear ()
"Clear previously loaded tables."
(interactive)
(setq rpgdm-tables (make-hash-table :test 'equal)))
(defun rpgdm-tables-load (&optional filepath)
"Read and parse table files located in FILEPATH directory.
@ -161,6 +165,15 @@ would be converted randomly to something like: 'You found a box.'"
(goto-char (point-min))
(flush-lines (rx bol (zero-or-more space) "#"))
;; I noticed that org-mode links we screwing up the output, so we strip them out:
(goto-char (point-min))
(let ((org-link-re (rx "[[" (one-or-more (not "]")) "]["
(group (one-or-more (not "]")))
"]]")))
(while (re-search-forward org-link-re nil t)
(replace-match (match-string 1) nil nil)))
;; The following predicates are not /pure functions/, as they scan the
;; current buffer, leaving the initial match in the 'hopper', so the parsing
;; function called makes that assumption, and will immediately grab that

View file

@ -3,7 +3,7 @@
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@workday.com>
;; Maintainer: Howard X. Abrams
;; Created: January 4, 2021
;;
;; This file is not part of GNU Emacs.
@ -40,18 +40,19 @@
"
^Dice^ ^Tables^ ^Checks^ ^Moving^ ^Messages^
-----------------------------------------------------------------------------------------------------------------
_d_: Roll Dice _z_: Flip a coin _r_: Dashboard _s_: d20 Skill _m_: Moderate _o_: Links -l: Last Results
_b_: Previous _f_: Next Dice Expr _t_: Load Tables _e_: Easy check _h_: Hard check _J_/_K_: Page up/dn -k: Previous
_a_/_A_: Advantage/Disadvantage _c_: Choose Item _v_: Difficult _i_: Impossible _N_/_W_: Narrow/Widen -j: Next "
_d_: Roll Dice / _D_: Reroll Dice _r_: Dashboard _s_: d20 Skill _m_: Moderate _o_: Links -l: Last Results
_z_: Flip a coin _O_: Oracle roll _t_: Load Tables _e_: Easy check _h_: Hard check _J_/_K_: Page up/dn -k: Previous
_b_: Previous _f_: Next Dice Expr _c_: Choose Item _v_: Difficult _i_: Impossible _N_/_W_: Narrow/Widen -j: Next
_a_/_A_: Advantage/Disadvantage _N_: Show NPC "
("d" rpgdm-roll) ("D" rpgdm-roll-again)
("f" rpgdm-forward-roll) ("b" rpgdm-forward-roll)
("a" rpgdm-roll-advantage) ("A" rpgdm-roll-disadvantage)
("z" rpgdm-yes-and-50/50)
("z" rpgdm-yes-and-50/50) ("O" rpgdm-oracle)
("s" rpgdm-skill-check) ("i" rpgdm-skill-check-impossible)
("e" rpgdm-skill-check-easy) ("m" rpgdm-skill-check-moderate)
("h" rpgdm-skill-check-hard) ("v" rpgdm-skill-check-difficult)
("t" rpgdm-tables-load) ("c" rpgdm-tables-choose)
("t" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color blue)
("r" rpgdm-screen-show) ("R" rpgdm-quick-close)
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
("K" scroll-down) ("J" scroll-up)
@ -118,6 +119,32 @@ Meant to be used with `rpgdm-last-results-previous'."
(should (equal "0> Almost the newest" (rpgdm-last-results-next)))
(should (equal "0> Almost the newest" (rpgdm-last-results-next))))
(defvar rpgdm-oracle-mod 0 "Cummulative skew to create more tension.")
(defun rpgdm-oracle ()
"Return a good/fair/bad message for GM-less result.
The algorithm here comes from Victor's GM-less Oracle game, see
https://cursenightgames.itch.io/victors-gm-less-oracle
This results of this idea mimic a bell curve, but creates
tension, for with more good outcomes, a bad outcome is more
likely. How this works in game play vs. a standard bell curve is
not clear."
(interactive)
(let* ((rolled (rpgdm--roll-die 6))
(outcome (+ rolled rpgdm-oracle-mod))
(diffmsg (if (= rolled outcome) "" ; empty string if true
(format " with mod of %d" rpgdm-oracle-mod)))
(results (cond ((<= outcome 1) "Best outcome")
((<= outcome 3) (progn
(setq rpgdm-oracle-mod (1+ rpgdm-oracle-mod))
"Best outcome"))
((<= outcome 5) "Middling outcome")
(t (progn
(setq rpgdm-oracle-mod 0)
"Worst outcome")))))
(rpgdm-message "Oracle says: %s (Rolled %d%s ... Now mod is %d)"
results rolled diffmsg rpgdm-oracle-mod)))
(defun rpgdm-yes-and-50/50 ()
"Add spice to your 50/50 events (luck) with Yes/No+complications.