emacs-rpgdm/rpgdm.el
Howard Abrams 558d7eb984 Code functional with a good README and Interface
Wrote some reusable instructions as well as polished a minor mode and a
hydra for easily using the code. Time to try it out in production.
2021-02-01 17:52:40 -08:00

233 lines
9.8 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))
(defgroup rpgdm nil
"Customization for the Dungeon Master support package."
:prefix "rpgdm-"
:group 'applications
:link '(url-link :tag "Github" "https://gitlab.com/howardabrams/emacs-rpgdm"))
(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 "<f13>") 'hydra-rpgdm/body)
map))
(defhydra hydra-rpgdm (:color pink :hint nil)
"
^Dice^ ^Tables^ ^Checks^
^^^^^^^----------------------------------------------------------------------------------------
_d_: Roll Dice _h_: Dashboard _s_: d20 Skill
_f_: Next Dice Expr _t_: Load Tables _e_: Easy check
_b_: Previous Expr _c_: Choose from _m_: Moderate
_z_: Flip a coin a table _h_: Hard check
_a_/_A_: Advantage/Disadvantage _v_: Difficult "
("d" rpgdm-roll) ("<f13>" rpgdm-last-results)
("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)
("h" rpgdm-screen)
("q" nil "quit"))
(defvar rpgdm-last-results ""
"The results from calls to `rpgdm-screen-' functions are stored here.")
(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."
(setq rpgdm-last-results (apply 'format format-string args))
(rpgdm-last-results))
(defun rpgdm-last-results ()
"Display results from the last call to a `rpgdm-screen-' function."
(interactive)
(message rpgdm-last-results))
(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))
(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)"))))
;; ----------------------------------------------------------------------
;; 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)
"Given a TARGET skill check, and ROLLED-RESULTS, return pass/fail.
The string can return a bit of complications, from `rpgdm--yes-and'."
(interactive (list (completing-read "Target Level: "
'(Trivial Easy Moderate Hard Difficult Impossible))
(read-number "Rolled Results: ")))
(rpgdm-message (rpgdm--yes-and target rolled-results)))
(defun rpgdm-skill-check-easy (rolled-results)
"Return an embellished pass/fail from ROLLED-RESULTS for an easy skill check."
(interactive "nRolled 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 "nRolled 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 "nRolled 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 "nRolled 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 "nRolled Results: ")
(rpgdm-skill-check (rpgdm--skill-level 'impossible) rolled-results))
(provide 'rpgdm)
;;; rpgdm.el ends here