emacs-rpgdm/rpgdm.el
Howard Abrams a61dfcc201 Wide view for the Hydra
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.
2021-03-23 13:46:50 -07:00

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