And changing the hotkey to F12 so that it is easier to access on the laptop. Oh, and let's have the displayed message contain a little more details so that when we jump backwards through the history, we understand more what we are seeing.
277 lines
12 KiB
EmacsLisp
277 lines
12 KiB
EmacsLisp
;;; rpgdm.el --- Support utilities for the RPG Game Master -*- lexical-binding: t; -*-
|
|
;;
|
|
;; Copyright (C) 2021 Howard X. Abrams
|
|
;;
|
|
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
|
|
;; Maintainer: Howard X. Abrams <howard.abrams@workday.com>
|
|
;; Created: January 4, 2021
|
|
;;
|
|
;; This file is not part of GNU Emacs.
|
|
;;
|
|
;;; Commentary:
|
|
;;
|
|
;; This package includes all the help and support I can think for the Game
|
|
;; Master running a role-playing game.
|
|
;;
|
|
;; This include a minor mode, `rpgdm' that adds a few keybindings useful
|
|
;; with either org-mode or markdown-formatted files.
|
|
;;
|
|
;;; Code:
|
|
|
|
(require 'cl)
|
|
(require 'dash)
|
|
(require 'hydra)
|
|
(require 's)
|
|
|
|
(defconst rpgdm-base (file-name-directory load-file-name))
|
|
(load-file (expand-file-name "rpgdm-dice.el" rpgdm-base))
|
|
(load-file (expand-file-name "rpgdm-screen.el" rpgdm-base))
|
|
(load-file (expand-file-name "rpgdm-tables.el" rpgdm-base))
|
|
(load-file (expand-file-name "rpgdm-npc.el" rpgdm-base))
|
|
|
|
(define-minor-mode rpgdm-mode
|
|
"Minor mode for layering role-playing game master functions over your notes."
|
|
:lighter " D&D"
|
|
:keymap (let ((map (make-sparse-keymap)))
|
|
(define-key map (kbd "<f12>") 'hydra-rpgdm/body)
|
|
map))
|
|
|
|
(defhydra hydra-rpgdm (:color pink :hint nil)
|
|
"
|
|
^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" rpgdm-roll)
|
|
("f" rpgdm-forward-roll) ("b" rpgdm-forward-roll)
|
|
("a" rpgdm-roll-advantage) ("A" rpgdm-roll-disadvantage)
|
|
("z" rpgdm-yes-and-50/50)
|
|
("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)
|
|
("r" rpgdm-screen-show) ("R" rpgdm-quick-close)
|
|
("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen)
|
|
("K" scroll-down) ("J" scroll-up)
|
|
|
|
("N" rpgdm-npc)
|
|
|
|
("C-m" rpgdm-last-results)
|
|
("C-n" rpgdm-last-results-next) ("C-p" rpgdm-last-results-previous)
|
|
("s-l" rpgdm-last-results)
|
|
("s-j" rpgdm-last-results-next) ("s-k" rpgdm-last-results-previous)
|
|
|
|
("q" nil "quit") ("<f12>" nil))
|
|
|
|
|
|
(defvar rpgdm-last-results (make-ring 10)
|
|
"The results from calls to `rpgdm-screen-' functions are stored here.")
|
|
|
|
(defvar rpgdm-last-results-ptr 0
|
|
"Keeps track of where we are in the message display ring.
|
|
Each call to `rpgdm-last-results' resets this to 0.")
|
|
|
|
(defun rpgdm-message (format-string &rest args)
|
|
"Replace `messasge' function allowing it to be re-displayed.
|
|
The FORMAT-STRING is a standard string for the `format' function,
|
|
and ARGS are substitued values."
|
|
(let ((message (apply 'format format-string args)))
|
|
(ring-insert rpgdm-last-results message)
|
|
(rpgdm-last-results)))
|
|
|
|
(defun rpgdm-last-results ()
|
|
"Display results from the last call to a `rpgdm-message' function."
|
|
(interactive)
|
|
(setq rpgdm-last-results-ptr 0)
|
|
(message (ring-ref rpgdm-last-results rpgdm-last-results-ptr)))
|
|
|
|
(defun rpgdm-last-results-previous ()
|
|
"Display results from an earlier call to `rpgdm-message'."
|
|
(interactive)
|
|
(incf rpgdm-last-results-ptr)
|
|
(when (>= rpgdm-last-results-ptr (ring-length rpgdm-last-results))
|
|
(setq rpgdm-last-results-ptr 0))
|
|
(message "%d> %s" rpgdm-last-results-ptr (ring-ref rpgdm-last-results rpgdm-last-results-ptr)))
|
|
|
|
(defun rpgdm-last-results-next ()
|
|
"Display results from an later call to `rpgdm-message'.
|
|
Meant to be used with `rpgdm-last-results-previous'."
|
|
(interactive)
|
|
(when (> rpgdm-last-results-ptr 0)
|
|
(decf rpgdm-last-results-ptr))
|
|
(message "%d> %s" rpgdm-last-results-ptr (ring-ref rpgdm-last-results rpgdm-last-results-ptr)))
|
|
|
|
(ert-deftest rpgdm-last-results-test ()
|
|
(progn
|
|
(setq rpgdm-last-results (make-ring 10))
|
|
(rpgdm-message "First in, so this is the oldest")
|
|
(rpgdm-message "Something or other")
|
|
(rpgdm-message "Almost the newest")
|
|
(rpgdm-message "Newest"))
|
|
|
|
(should (equal "Newest" (rpgdm-last-results)))
|
|
(should (equal "1> Almost the newest" (rpgdm-last-results-previous)))
|
|
(should (equal "2> Something other" (rpgdm-last-results-previous)))
|
|
(should (equal "1> Almost the newest" (rpgdm-last-results-next)))
|
|
(should (equal "0> Almost the newest" (rpgdm-last-results-next)))
|
|
(should (equal "0> Almost the newest" (rpgdm-last-results-next))))
|
|
|
|
|
|
(defun rpgdm-yes-and-50/50 ()
|
|
"Add spice to your 50/50 events (luck) with Yes/No+complications.
|
|
|
|
The Freeform Universal RPG has the idea that you could succeed or
|
|
fail, but have extra complications or extra bonuses. This returns
|
|
one of six answers with equal frequency:
|
|
|
|
- No, and ... in other words, no luck, plus a minor complication.
|
|
- No. ... Nope, no luck at this time.
|
|
- No, but ... in other words, no luck, but something else good.
|
|
- Yes, but ... you got what you wanted, but with a complication.
|
|
- Yes. ... Yup, luck is on your side.
|
|
- Yes, and ... Yes, plus you get a little something-something.
|
|
|
|
https://www.drivethrurpg.com/product/89534/FU-The-Freeform-Universal-RPG-Classic-rules"
|
|
(interactive)
|
|
(let* ((rolled (rpgdm--roll-die 6))
|
|
(results (cond ((= rolled 1) "No, and... (fails badly that you add a complication)")
|
|
((= rolled 2) "No.")
|
|
((= rolled 3) "No, but... (fails, but add a little bonus or consolation prize)")
|
|
((= rolled 4) "Yes, but... (succeeds, but add a complication or caveat)")
|
|
((= rolled 5) "Yes.")
|
|
(t "Yes, and... (succeeds, plus add a litle extra something-something)"))))
|
|
(rpgdm-message "Luck results: %s" results)))
|
|
|
|
|
|
;; ----------------------------------------------------------------------
|
|
;; SKILL CHECKS
|
|
;; ----------------------------------------------------------------------
|
|
;; I would like to have a function that
|
|
|
|
(defun rpgdm--skill-level-dice (number-of-dice)
|
|
"Return a random skill challenge level.
|
|
The formula is based on the NUMBER-OF-DICE. According to the
|
|
Players Handbook in Dungeons and Dragons, we have this table
|
|
to determine difficulty skill check levels:
|
|
|
|
- Very easy 5
|
|
- Easy 10
|
|
- Medium 15
|
|
- Hard 20
|
|
- Very hard 25
|
|
- Nearly impossible 30
|
|
|
|
But I read somewhere that you could roll some 6 sided die to help
|
|
add a bit of randomness to the leve setting. Essentially, roll
|
|
the 6d and add 7.
|
|
|
|
Easy -- Die: 1 Lowest: 8 Highest: 13 Average: 10
|
|
Medium -- Die: 2 Lowest: 9 Highest: 19 Average: 14
|
|
Hard -- Die: 3 Lowest: 10 Highest: 25 Average: 17
|
|
Very hard -- Die: 4 Lowest: 11 Highest: 30 Average: 20
|
|
Nearly Impossible -- Die: 5 Lowest: 14 Highest: 35 Average: 24"
|
|
(rpgdm--sum
|
|
(rpgdm--roll number-of-dice 6 7)))
|
|
|
|
;; Let's verify my assumptions:
|
|
;; (dolist (die '(0 1 2 3 4 5))
|
|
;; (let* ((rolls (repeatedly 'rpgdm--skill-level-dice (list die) 1000))
|
|
;; (highest (apply 'max rolls))
|
|
;; (lowest (apply 'min rolls))
|
|
;; (average (/ (-sum rolls) 1000)))
|
|
;; (message
|
|
;; (format "Die: %d Lowest: %d Highest: %d Average: %d\n" die lowest highest average))))
|
|
|
|
(defun rpgdm--skill-level (target)
|
|
"Return a skill challenge level by Interpreting TARGET.
|
|
This parameter can be a symbol or string for 'easy', 'hard', etc.
|
|
Or it can be an actual number."
|
|
(when (symbolp target)
|
|
(setq target (symbol-name target)))
|
|
(cond ((string-prefix-p target "trivial" t) 5)
|
|
((string-prefix-p target "easy" t) (rpgdm--skill-level-dice 1))
|
|
((string-prefix-p target "moderate" t) (rpgdm--skill-level-dice 2))
|
|
((string-prefix-p target "medium" t) (rpgdm--skill-level-dice 2))
|
|
((string-prefix-p target "hard" t) (rpgdm--skill-level-dice 3))
|
|
((string-prefix-p target "difficult" t) (rpgdm--skill-level-dice 4))
|
|
((string-prefix-p target "very hard" t) (rpgdm--skill-level-dice 4))
|
|
((string-prefix-p target "impossible" t) (rpgdm--skill-level-dice 5))
|
|
((numberp target) target)
|
|
(t (max (string-to-number target) 12))))
|
|
|
|
(defun rpgdm--yes-and (target rolled-results)
|
|
"Instead of returning a pass/fail, return 'Yes, but' strings.
|
|
|
|
The Freeform Universal RPG has the idea that you could succeed or
|
|
fail, but have extra complications or extra bonuses based on how
|
|
high/low you pass/fail. I have expanded the idea with a d20, so
|
|
given a TARGET number, like '12', and the ROLLED-RESULTS from the
|
|
player, this returns a string based on a table.
|
|
|
|
https://www.drivethrurpg.com/product/89534/FU-The-Freeform-Universal-RPG-Classic-rules"
|
|
(cond ((< rolled-results (- target 7)) "No, and... !!")
|
|
((< rolled-results (- target 3)) "No.")
|
|
((< rolled-results target) "No, but...")
|
|
((< rolled-results (+ target 3)) "Yes, but...")
|
|
((< rolled-results (+ target 7)) "Yes.")
|
|
(t "Yes, and... !!")))
|
|
|
|
(ert-deftest rpgdm--yes-and-test ()
|
|
(should (equal (rpgdm--yes-and 10 1) "No, and..."))
|
|
(should (equal (rpgdm--yes-and 10 2) "No, and..."))
|
|
(should (equal (rpgdm--yes-and 10 3) "No."))
|
|
(should (equal (rpgdm--yes-and 10 4) "No."))
|
|
(should (equal (rpgdm--yes-and 10 5) "No."))
|
|
(should (equal (rpgdm--yes-and 10 6) "No."))
|
|
(should (equal (rpgdm--yes-and 10 7) "No, but..."))
|
|
(should (equal (rpgdm--yes-and 10 8) "No, but..."))
|
|
(should (equal (rpgdm--yes-and 10 9) "No, but..."))
|
|
(should (equal (rpgdm--yes-and 10 10) "Yes, but..."))
|
|
(should (equal (rpgdm--yes-and 10 11) "Yes, but..."))
|
|
(should (equal (rpgdm--yes-and 10 12) "Yes, but..."))
|
|
(should (equal (rpgdm--yes-and 10 13) "Yes."))
|
|
(should (equal (rpgdm--yes-and 10 14) "Yes."))
|
|
(should (equal (rpgdm--yes-and 10 15) "Yes."))
|
|
(should (equal (rpgdm--yes-and 10 16) "Yes."))
|
|
(should (equal (rpgdm--yes-and 10 17) "Yes, and...")))
|
|
|
|
(defun rpgdm-skill-check (target rolled-results &optional label)
|
|
"Given a TARGET skill check, and ROLLED-RESULTS, return pass/fail.
|
|
The string can return a bit of complications, from `rpgdm--yes-and'.
|
|
The LABEL will be append to the message, and used form other calls."
|
|
(interactive (list (completing-read "Target Level: "
|
|
'(Trivial Easy Moderate Hard Difficult Impossible))
|
|
(read-number "Rolled Results: ")))
|
|
(rpgdm-message "%s Skill Check: %s"
|
|
(capitalize target)
|
|
(rpgdm--yes-and (downcase target) rolled-results)))
|
|
|
|
(defun rpgdm-skill-check-easy (rolled-results)
|
|
"Return an embellished pass/fail from ROLLED-RESULTS for an easy skill check."
|
|
(interactive "nEasy Check- Rolled Results: ")
|
|
(rpgdm-skill-check (rpgdm--skill-level 'easy) rolled-results))
|
|
|
|
(defun rpgdm-skill-check-moderate (rolled-results)
|
|
"Return an embellished pass/fail from ROLLED-RESULTS for a moderately-difficult skill check."
|
|
(interactive "nModerate Check- Rolled Results: ")
|
|
(rpgdm-skill-check (rpgdm--skill-level 'medium) rolled-results))
|
|
|
|
(defun rpgdm-skill-check-hard (rolled-results)
|
|
"Return an embellished pass/fail from ROLLED-RESULTS for a hard skill check."
|
|
(interactive "nHard Check- Rolled Results: ")
|
|
(rpgdm-skill-check (rpgdm--skill-level 'hard) rolled-results))
|
|
|
|
(defun rpgdm-skill-check-difficult (rolled-results)
|
|
"Return an embellished pass/fail from ROLLED-RESULTS for a difficult skill check."
|
|
(interactive "nVery Hard Check- Rolled Results: ")
|
|
(rpgdm-skill-check (rpgdm--skill-level 'difficult) rolled-results))
|
|
|
|
(defun rpgdm-skill-check-impossible (rolled-results)
|
|
"Return an embellished pass/fail from ROLLED-RESULTS for an almost impossible skill check."
|
|
(interactive "nImpossible Check- Rolled Results: ")
|
|
(rpgdm-skill-check (rpgdm--skill-level 'impossible) rolled-results))
|
|
|
|
(provide 'rpgdm)
|
|
;;; rpgdm.el ends here
|