diff --git a/README.org b/README.org index d72fa03..4e9ea12 100644 --- a/README.org +++ b/README.org @@ -169,7 +169,12 @@ We also need the name of the directory for this project, so that we can load tab "The root directory to the rpgdm-ironsworn project.") #+END_SRC ** Dice Roller -In *Ironsworn*, all dice rolls follow a pattern where you set the challenge level for a check by rolling /challenge dice/ (two d10s) and compare that against rolling an /action die/ (a single d6 ... adding all modifiers to that six-sided die). You always three possible values: +In *Ironsworn*, all dice rolls follow a pattern where you set the challenge level for a check by rolling /challenge dice/ (two d10s) and compare that against rolling an /action die/ (a single d6 ... adding all modifiers to that six-sided die). + +#+ATTR_HTML: :width 800px +[[file:images/dice-roll-all.png]] + +You always three possible values: - A *strong hit* where your action die + modifiers is larger than both d10 challenge dice. - A *weak hit* is where your action die + modifiers is larger than /only one/ of the d10 challenge dice. @@ -236,7 +241,7 @@ When we roll, I want one of those three results printed, but in different colors So the following messages, given various /rolls/ should cover those possibilities with text properties: -#+BEGIN_SRC emacs-lisp :tangle no +#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el (ert-deftest rpgdm-ironsworn--results-test () (should (equal (rpgdm-ironsworn--results 3 2 4 1) "Strong hit :: 5 (3 + 2) → 4 / 1")) @@ -250,7 +255,7 @@ So the following messages, given various /rolls/ should cover those possibilitie (should (equal (rpgdm-ironsworn--results 3 2 8 6 7) "Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Weak hit")) (should (equal (rpgdm-ironsworn--results 3 2 8 6 9) - "Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Strong hit"))))) + "Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Strong hit"))) #+END_SRC The basic interface will query for a modifer, roll all three dice, and then display the results using the [[https://gitlab.com/howardabrams/emacs-rpgdm/-/blob/main/rpgdm.el#L76][rpgdm-message]] function: @@ -279,12 +284,12 @@ Assuming we have a new character, let's query the user for all of these stats, a We assume you have created an org-file, and the /template/ will just append some basic text at the end of the buffer. Note that if you don't give this function a name, it will randomly choose one. #+BEGIN_SRC emacs-lisp - (defun rpgdm-ironsworn--new-character-template (name) + (defun rpgdm-ironsworn--new-character-template (&optional name) "Insert basic Ironsworn template at the end of the current buffer. A header is created with NAME, but if this is an empty string, a random name is generated for the purposes of the template." - (when (s-blank? name) - (setq name (rpgdm-tables-choose "names-ironlander"))) + (when (or (null name) (s-blank? name)) + (setq name (rpgdm-tables-choose "name/ironlander"))) (let ((frmt (seq-random-elt '("* The Adventures of %s" "* The Journeys of %s" @@ -335,8 +340,8 @@ Let's fill in those asset values /lazily/. The first time we call this function, the `assets' directory, otherwise, we return a cached version." (unless rpgdm-ironsworn-character-assets (let ((asset-files (thread-first rpgdm-ironsworn-project - (f-join "assets") - (directory-files-recursively (rx (one-or-more any) ".org") nil)))) + (f-join "assets") + (directory-files-recursively (rx (one-or-more any) ".org") nil)))) (setq rpgdm-ironsworn-character-assets (seq-map (lambda (file) (cons (rpgdm-ironsworn--character-asset-label file) file)) @@ -390,7 +395,7 @@ When you start a character, you choose three assets, but what if we choose them And I can write a little unit test to verify my test cases: -#+BEGIN_SRC emacs-lisp :tangle no +#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el (ert-deftest rpgdm-ironsworn--good-character-assets-test () (should (rpgdm-ironsworn--good-character-assets '("foo" "bar" "baz"))) (should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "foo"))) @@ -399,7 +404,7 @@ And I can write a little unit test to verify my test cases: "assets/companions/monkey.org"))) (should-not (rpgdm-ironsworn--good-character-assets '(("Companions :: Dog" . "assets/companions/dog.org") ("Paths :: Good Guy" . "assets/paths/good-guy.org") - ("Companions :: Monkey" . "assets/companions/monkey.org")))))) + ("Companions :: Monkey" . "assets/companions/monkey.org"))))) #+END_SRC The rules say to start off with three assets, so let's have a function that can give us three random assets: @@ -416,7 +421,7 @@ The rules say to start off with three assets, so let's have a function that can And again, a little unit test would be nice: -#+BEGIN_SRC emacs-lisp :tangle no +#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el (ert-deftest rpgdm-ironsworn--some-character-assets-test () (should (= 4 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6) 4)))) (should (= 3 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6)))))) @@ -459,7 +464,7 @@ Whew. We finally can have a function that queries the user about a new character (goto-char (point-max)) (insert "\n** Assets\n") (if (y-or-n-p "Would you like three random assets? ") - (rpgdm-ironsworn-random-character-assets asset-files) + (rpgdm-ironsworn-random-character-assets 3) (if (y-or-n-p "Would you like to choose your assets? ") (call-interactively 'rpgdm-ironsworn-insert-character-asset)))) #+END_SRC @@ -474,7 +479,7 @@ This function will query the user for all of the stats and other properties that `rpgdm-ironsworn-progress-create' functions." (dolist (stat '(edge heart iron shadow wits)) (rpgdm-ironsworn-store-character-state stat - (read-string (format "What '%s' stat: " stat)))) + (read-string (format "What '%s' stat: " stat)))) (dolist (stat '(health spirit supply)) (rpgdm-ironsworn-store-character-state stat 5)) @@ -482,10 +487,11 @@ This function will query the user for all of the stats and other properties that (rpgdm-ironsworn-progress-create (read-string "What title should we give this new character's Epic vow? ") 1) (rpgdm-ironsworn-progress-create "Bonds" 1) + (rpgdm-ironsworn-progress-mark "Bonds") (search-forward ":END:") (end-of-line) (insert "\n** Bonds\n") - (insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement-names")))) + (insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement/name")))) #+END_SRC **** New Character Interface Do we choose the stats first, or the assets? Do we ask, or have two separate functions? @@ -495,14 +501,12 @@ Perhaps the clearest approach is to do both, create two process functions, and t (defun rpgdm-ironsworn--new-character-stats-first (&optional name) "Insert a new character template for character, NAME. The character stats are first queried, and then assets inserted." - (rpgdm-ironsworn--new-character-template name) (rpgdm-ironsworn--new-character-stats) (rpgdm-ironsworn--new-character-assets)) (defun rpgdm-ironsworn--new-character-assets-first (&optional name) "Insert a new character template for character, NAME. The assets are inserted first, and then character stats are queried." - (rpgdm-ironsworn--new-character-template name) ;; Saving and restoring point, means the properties should be in the ;; correct, top-level position. (save-excursion @@ -516,16 +520,18 @@ Perhaps the clearest approach is to do both, create two process functions, and t This function _appends_ this information to the current buffer, which should be using the `org-mode' major mode." (interactive (list - (read-string "What is the new character's name? ") - (completing-read "What order should we build this? " '("Statistics first" "Assets first")))) + (read-string "What is the new character's name? ") + (completing-read "What order should we build this? " '("Statistics first" "Assets first")))) + + (rpgdm-ironsworn--new-character-template name) (if (equal order "Assets first") - (rpgdm-ironsworn--new-character-assets-first) + (rpgdm-ironsworn--new-character-assets-first) (rpgdm-ironsworn--new-character-stats-first)) (message "Alright, the template is complete. Edit away!" name)) #+END_SRC *** Showing a Character Stats -Sure, you could open up the appropriate drawer to see a character's stats, but we could do better? An updated org table? A separate buffer? For the moment, I am just going to display it in the mini-buffer whenever I want to see it: +Sure, you could open up the appropriate drawer to see a character's stats, but we could do better? An updated org table? A separate buffer? For the moment, I am just going to display it in the mini-buffer whenever I want to see it. #+BEGIN_SRC emacs-lisp (defun rpgdm-ironsworn--display-stat (stat character) @@ -719,13 +725,13 @@ The [[file:moves][moves]] directory contains one org file for each move. These f And let's verify the format: -#+BEGIN_SRC emacs-lisp :tangle no +#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el (ert-deftest rpgdm-ironsworn--move-tuple-test () (let ((file "moves/fate/ask-the-oracle.org") (full "~/other/over/here/moves/fate/ask-the-oracle.org")) - (should (equal (list "Fate :: Ask The Oracle" file) + (should (equal (list "Fate :: ask the oracle" file) (rpgdm-ironsworn--move-tuple file))) - (should (equal (list "Fate :: Ask The Oracle" full) + (should (equal (list "Fate :: ask the oracle" full) (rpgdm-ironsworn--move-tuple full))))) #+END_SRC @@ -876,12 +882,26 @@ Given a label, can we get the level as a value: (alist-get label rpgdm-ironsworn-progress-levels 8 nil 'equal)) #+END_SRC -Since we'll store only the numbers, we might want to get back the label: +Since we'll store only the numbers, we might want to get back the label. Originally, I used an =rassoc=, but since I =propertized= the labels, my new function is a bit more complicated: #+BEGIN_SRC emacs-lisp (defun rpgdm-ironsworn-progress-level-label (level) "Return the label associated with the progress LEVEL, e.g. dangerous." - (car (rassoc level rpgdm-ironsworn-progress-levels))) + (thread-last rpgdm-ironsworn-progress-levels + (--filter (= (second it) level)) + (first) ; Assume we only get one match + (first) ; Get the textual label + (s-split " ") + (first))) ; Word before the space +#+END_SRC + +Basic tests for sanity: + +#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el + (ert-deftest rpgdm-ironsworn-progress-level-label-test () + (should (equal (rpgdm-ironsworn-progress-level-label 1) "epic")) + (should (equal (rpgdm-ironsworn-progress-level-label 12) "troublesome")) + (should (equal (rpgdm-ironsworn-progress-level-label 4) "formidable"))) #+END_SRC A helper function for allowing the user to choose which track to mark progress against: @@ -911,26 +931,27 @@ Adding a progress to a character amounts to an arbitrary name, and the number of Stored as a property in the org file. Keep in mind that the NAME should be a short title, not a description." (interactive (list (read-string "Progress Name: ") - (completing-read-value "Progress Level: " - rpgdm-ironsworn-progress-levels))) + (completing-read-value "Progress Level: " + rpgdm-ironsworn-progress-levels))) (let* ((track-id (substring (secure-hash 'md5 name) 0 8)) - (track-prop (format "ironsworn-progress-%s" track-id)) - (track-val (format "\"%s\" %d %d" name level 0)) + (track-prop (format "ironsworn-progress-%s" track-id)) + (track-val (format "\"%s\" %d %d" name level 0)) - (title (org-get-heading)) - (option '(("At the same level as a sibling?" same-level) - ("As a subheading to this?" subheading) - ("No new heading. Re-use this." no)))) + (title (org-get-heading)) + (option '(("At the same level as a sibling?" same-level) + ("As a subheading to this?" subheading) + ("No new heading. Re-use this." no)))) - (cl-case (completing-read-value "Create a new heading? " option) - ('same-level (progn - (org-insert-heading-respect-content) - (insert name))) - ('subheading (progn - (org-insert-heading-respect-content) - (org-shiftmetaright) - (insert name)))) + (when (called-interactively-p) + (cl-case (completing-read-value "Create a new heading? " option) + ('same-level (progn + (org-insert-heading-respect-content) + (insert name))) + ('subheading (progn + (org-insert-heading-respect-content) + (org-shiftmetaright) + (insert name))))) (org-set-property track-prop track-val))) #+END_SRC @@ -948,22 +969,97 @@ Interactively, we can call the =-mark= function multiple times, but we might wan (rpgdm-ironsworn-mark-progress-track name))) #+END_SRC +The =rpgdm-ironsworn--progress-amount= function returns the information about a progress, by name. It returns a list of all aspects, allowing us to decide how to display it: + +#+BEGIN_SRC emacs-lisp + (defun rpgdm-ironsworn--progress-amount (name) + "Return list of aspects of progress by NAME. + Including: + - track :: The name of the track (same as `name') + - value :: The number complexity level, e.g. how many ticks per marked progress + - level :: The string label of a progress, e.g. Epic or Troublesome + - ticks :: The amount of ticks marked, shnould be <= 40 + - boxes :: The number of completed boxes (ticks / 4, rounded down) + - remain :: The number of remaining ticks marked towards the next box." + (let* ((tracks (rpgdm-ironsworn-character-progresses)) + (matched (assoc name tracks)) + (track (first matched)) + (value (second matched)) + (level (rpgdm-ironsworn-progress-level-label value)) + (ticks (third matched)) + (boxes (/ ticks 4)) + (remain (% ticks 4))) + (list track value level ticks boxes remain))) +#+END_SRC + +Make a progress to visual table of boxes. Not sure if this is very helpful or not, but it sure will look cool: + +#+BEGIN_SRC emacs-lisp + (defun rpgdm-ironsworn--progress-box (boxes leftover) + "Return a org-like table of current completion of an Ironsworn progress. + For instance, with 4 boxes and 2 leftover tick marks, this will return: + | ■ | ■ | ■ | ■ | x | | | | | | " + (defun make-box (boxes leftover-ticks blanks) + (cond + ((> boxes 0) (concat " ■ |" (make-box (1- boxes) leftover-ticks blanks))) + + ((> leftover-ticks 2) (concat " * |" (make-box 0 0 blanks))) + ((> leftover-ticks 1) (concat " x |" (make-box 0 0 blanks))) + ((> leftover-ticks 0) (concat " - |" (make-box 0 0 blanks))) + + ((> blanks 0) (concat " |" (make-box 0 0 (1- blanks)))))) + + (when (> leftover 3) + (setq boxes (+ boxes (/ leftover 4))) + (setq leftover (% leftover 4))) + (when (< boxes 0) + (setq boxes 0)) + (when (> boxes 10) + (setq boxes 10)) + (setq blanks (if (> leftover 0) + (- 9 boxes) + (- 10 boxes))) + + (concat "|" (make-box boxes leftover blanks))) +#+END_SRC + +Some tests may make it clear how that function will look: + +#+BEGIN_SRC emacs-lisp + (ert-deftest rpgdm-ironsworn--progress-box-test () + (should (equal (rpgdm-ironsworn--progress-box 0 0) "| | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 0 1) "| - | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 0 2) "| x | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 0 3) "| * | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 1 0) "| ■ | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 2 0) "| ■ | ■ | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 3 0) "| ■ | ■ | ■ | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 0) "| ■ | ■ | ■ | ■ | | | | | | |" + (should (equal (rpgdm-ironsworn--progress-box 4 1) "| ■ | ■ | ■ | ■ | - | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 2) "| ■ | ■ | ■ | ■ | x | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 3) "| ■ | ■ | ■ | ■ | * | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 5 0) "| ■ | ■ | ■ | ■ | ■ | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 10 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |")) + ;; Negative test cases + (should (equal (rpgdm-ironsworn--progress-box 11 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |")) + (should (equal (rpgdm-ironsworn--progress-box -1 0) "| | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 8) "| ■ | ■ | ■ | ■ | ■ | ■ | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 6) "| ■ | ■ | ■ | ■ | ■ | x | | | | |"))) +#+END_SRC + If we call the =-amount= function /interactively/, we should get the current results displayed, otherwise, we really just want the number of full boxes marked: #+BEGIN_SRC emacs-lisp (defun rpgdm-ironsworn-progress-amount (name) "Display the progress made against a track, NAME." (interactive (list (rpgdm-ironsworn-progress-track-choose))) - (let* ((tracks (rpgdm-ironsworn-character-progresses)) - (matched (--filter (equal name (first it)) tracks)) - (track (first matched)) - (value (second track)) - (level (rpgdm-ironsworn-progress-level-label value)) - (ticks (third track)) - (boxes (/ ticks 4))) - (if (called-interactively-p 'any) - (rpgdm-message "[%d] Progress on %s: %d (Ticks: %d)" level name boxes ticks) - boxes))) + (cl-destructuring-bind + (track value level ticks boxes leftover) + (rpgdm-ironsworn--progress-amount name) + (if (not (called-interactively-p 'any)) + boxes + (rpgdm-message "[%s] Progress on '%s': %d %s" level name boxes + (rpgdm-ironsworn--progress-box boxes leftover))))) #+END_SRC Rolling against the progress just means we need to request that value instead of rolling the d6, but in this case, we should either take a currently registered progress track, or simply specify a completed amount: @@ -993,9 +1089,9 @@ When we've finished a track, we can remove it from the hash to not clutter the i (remhash name tracks)))) #+END_SRC -Let's make sure these function work as we expect: +Let's make sure these function work as we expect (keep in mind, these can only be run in an Org file): -#+BEGIN_SRC emacs-lisp :tangle no +#+BEGIN_SRC emacs-lisp (ert-deftest rpgdm-ironsworn-progress-test () (let ((track "Battling a Grue")) (rpgdm-ironsworn-progress-delete track) @@ -1523,7 +1619,7 @@ We also need a property-symbol to string conversion. I don't know if the string A test is always explanatory for how this function should behave: -#+BEGIN_SRC emacs-lisp :tangle no +#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el (ert-deftest rpgdm-ironsworn--progress-to-str-test () (should (equal (rpgdm-ironsworn--progress-to-str :IRONSWORN-PROGRESS-EPIC) "ironsworn-progress-epic"))) diff --git a/images/dice-roll-all.png b/images/dice-roll-all.png new file mode 100644 index 0000000..73f439b Binary files /dev/null and b/images/dice-roll-all.png differ diff --git a/rpgdm-ironsworn-tests.el b/rpgdm-ironsworn-tests.el new file mode 100644 index 0000000..bb03c0e --- /dev/null +++ b/rpgdm-ironsworn-tests.el @@ -0,0 +1,65 @@ +(ert-deftest rpgdm-ironsworn--results-test () + (should (equal (rpgdm-ironsworn--results 3 2 4 1) + "Strong hit :: 5 (3 + 2) → 4 / 1")) + (should (equal (rpgdm-ironsworn--results 3 2 8 1) + "Weak hit :: 5 (3 + 2) → 8 / 1")) + (should (equal (rpgdm-ironsworn--results 3 2 8 6) + "Miss :: 5 (3 + 2) → 8 / 6")) + (should (equal (rpgdm-ironsworn--results 3 2 6 6) + "Miss :: 5 (3 + 2) → 6 / 6 ← Create a Twist")) + + (should (equal (rpgdm-ironsworn--results 3 2 8 6 7) + "Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Weak hit")) + (should (equal (rpgdm-ironsworn--results 3 2 8 6 9) + "Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Strong hit"))) + +(ert-deftest rpgdm-ironsworn--good-character-assets-test () + (should (rpgdm-ironsworn--good-character-assets '("foo" "bar" "baz"))) + (should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "foo"))) + (should-not (rpgdm-ironsworn--good-character-assets '("assets/companions/dog.org" + "assets/paths/good-guy.org" + "assets/companions/monkey.org"))) + (should-not (rpgdm-ironsworn--good-character-assets '(("Companions :: Dog" . "assets/companions/dog.org") + ("Paths :: Good Guy" . "assets/paths/good-guy.org") + ("Companions :: Monkey" . "assets/companions/monkey.org"))))) + +(ert-deftest rpgdm-ironsworn--some-character-assets-test () + (should (= 4 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6) 4)))) + (should (= 3 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6)))))) + +(ert-deftest rpgdm-ironsworn--move-tuple-test () + (let ((file "moves/fate/ask-the-oracle.org") + (full "~/other/over/here/moves/fate/ask-the-oracle.org")) + (should (equal (list "Fate :: ask the oracle" file) + (rpgdm-ironsworn--move-tuple file))) + (should (equal (list "Fate :: ask the oracle" full) + (rpgdm-ironsworn--move-tuple full))))) + +(ert-deftest rpgdm-ironsworn-progress-level-label-test () + (should (equal (rpgdm-ironsworn-progress-level-label 1) "epic")) + (should (equal (rpgdm-ironsworn-progress-level-label 12) "troublesome")) + (should (equal (rpgdm-ironsworn-progress-level-label 4) "formidable"))) + +(ert-deftest rpgdm-ironsworn-progress-test () + (let ((track "Battling a Grue")) + (rpgdm-ironsworn-progress-delete track) + (rpgdm-ironsworn-progress-create track "Dangerous") + (should (= (rpgdm-ironsworn-progress-amount track) 0)) + (rpgdm-ironsworn-progress-mark track) + (should (= (rpgdm-ironsworn-progress-amount track) 2)) + (rpgdm-ironsworn-progress-mark track 2) + (should (= (rpgdm-ironsworn-progress-amount track) 6)))) + +(ert-deftest rpgdm-ironsworn-progress-test () + (let ((track "Battling an Extreme Grue")) + (rpgdm-ironsworn-progress-delete track) + (rpgdm-ironsworn-progress-create track "Extreme") + (should (= (rpgdm-ironsworn-progress-amount track) 0)) + (rpgdm-ironsworn-progress-mark track) + (should (= (rpgdm-ironsworn-progress-amount track) 0)) + (rpgdm-ironsworn-progress-mark track 2) + (should (= (rpgdm-ironsworn-progress-amount track) 1)))) + +(ert-deftest rpgdm-ironsworn--progress-to-str-test () + (should (equal (rpgdm-ironsworn--progress-to-str :IRONSWORN-PROGRESS-EPIC) + "ironsworn-progress-epic"))) diff --git a/rpgdm-ironsworn.el b/rpgdm-ironsworn.el index ac137e3..b7e0e50 100644 --- a/rpgdm-ironsworn.el +++ b/rpgdm-ironsworn.el @@ -88,12 +88,12 @@ results." one-challenge two-challenge momentum)))) -(defun rpgdm-ironsworn--new-character-template (name) +(defun rpgdm-ironsworn--new-character-template (&optional name) "Insert basic Ironsworn template at the end of the current buffer. A header is created with NAME, but if this is an empty string, a random name is generated for the purposes of the template." - (when (s-blank? name) - (setq name (rpgdm-tables-choose "names-ironlander"))) + (when (or (null name) (s-blank? name)) + (setq name (rpgdm-tables-choose "name/ironlander"))) (let ((frmt (seq-random-elt '("* The Adventures of %s" "* The Journeys of %s" @@ -132,8 +132,8 @@ that contains the text. The first time we call this, we read from the `assets' directory, otherwise, we return a cached version." (unless rpgdm-ironsworn-character-assets (let ((asset-files (thread-first rpgdm-ironsworn-project - (f-join "assets") - (directory-files-recursively (rx (one-or-more any) ".org") nil)))) + (f-join "assets") + (directory-files-recursively (rx (one-or-more any) ".org") nil)))) (setq rpgdm-ironsworn-character-assets (seq-map (lambda (file) (cons (rpgdm-ironsworn--character-asset-label file) file)) @@ -205,7 +205,7 @@ The chosen assets are _good_ in that they won't have duplicates, etc." (goto-char (point-max)) (insert "\n** Assets\n") (if (y-or-n-p "Would you like three random assets? ") - (rpgdm-ironsworn-random-character-assets asset-files) + (rpgdm-ironsworn-random-character-assets 3) (if (y-or-n-p "Would you like to choose your assets? ") (call-interactively 'rpgdm-ironsworn-insert-character-asset)))) @@ -216,7 +216,7 @@ Note: The stats are added as properties using the `rpgdm-ironsworn-progress-create' functions." (dolist (stat '(edge heart iron shadow wits)) (rpgdm-ironsworn-store-character-state stat - (read-string (format "What '%s' stat: " stat)))) + (read-string (format "What '%s' stat: " stat)))) (dolist (stat '(health spirit supply)) (rpgdm-ironsworn-store-character-state stat 5)) @@ -224,22 +224,21 @@ Note: The stats are added as properties using the (rpgdm-ironsworn-progress-create (read-string "What title should we give this new character's Epic vow? ") 1) (rpgdm-ironsworn-progress-create "Bonds" 1) + (rpgdm-ironsworn-progress-mark "Bonds") (search-forward ":END:") (end-of-line) (insert "\n** Bonds\n") - (insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement-names")))) + (insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement/name")))) (defun rpgdm-ironsworn--new-character-stats-first (&optional name) "Insert a new character template for character, NAME. The character stats are first queried, and then assets inserted." - (rpgdm-ironsworn--new-character-template name) (rpgdm-ironsworn--new-character-stats) (rpgdm-ironsworn--new-character-assets)) (defun rpgdm-ironsworn--new-character-assets-first (&optional name) "Insert a new character template for character, NAME. The assets are inserted first, and then character stats are queried." - (rpgdm-ironsworn--new-character-template name) ;; Saving and restoring point, means the properties should be in the ;; correct, top-level position. (save-excursion @@ -253,8 +252,10 @@ template will generate and query the user for the rest of the data. This function _appends_ this information to the current buffer, which should be using the `org-mode' major mode." (interactive (list - (read-string "What is the new character's name? ") - (completing-read "What order should we build this? " '("Statistics first" "Assets first")))) + (read-string "What is the new character's name? ") + (completing-read "What order should we build this? " '("Statistics first" "Assets first")))) + + (rpgdm-ironsworn--new-character-template name) (if (equal order "Assets first") (rpgdm-ironsworn--new-character-assets-first) (rpgdm-ironsworn--new-character-stats-first)) @@ -505,7 +506,12 @@ For instance, if LABEL is `Dangerous', this returns `8'." (defun rpgdm-ironsworn-progress-level-label (level) "Return the label associated with the progress LEVEL, e.g. dangerous." - (car (rassoc level rpgdm-ironsworn-progress-levels))) + (thread-last rpgdm-ironsworn-progress-levels + (--filter (= (second it) level)) + (first) ; Assume we only get one match + (first) ; Get the textual label + (s-split " ") + (first))) ; Word before the space (defun rpgdm-ironsworn-progress-track-choose (&optional allow-other) "Query the user to choose a track stored in the org file. @@ -527,26 +533,27 @@ marked against some progress." Stored as a property in the org file. Keep in mind that the NAME should be a short title, not a description." (interactive (list (read-string "Progress Name: ") - (completing-read-value "Progress Level: " - rpgdm-ironsworn-progress-levels))) + (completing-read-value "Progress Level: " + rpgdm-ironsworn-progress-levels))) (let* ((track-id (substring (secure-hash 'md5 name) 0 8)) - (track-prop (format "ironsworn-progress-%s" track-id)) - (track-val (format "\"%s\" %d %d" name level 0)) + (track-prop (format "ironsworn-progress-%s" track-id)) + (track-val (format "\"%s\" %d %d" name level 0)) - (title (org-get-heading)) - (option '(("At the same level as a sibling?" same-level) - ("As a subheading to this?" subheading) - ("No new heading. Re-use this." no)))) + (title (org-get-heading)) + (option '(("At the same level as a sibling?" same-level) + ("As a subheading to this?" subheading) + ("No new heading. Re-use this." no)))) - (cl-case (completing-read-value "Create a new heading? " option) - ('same-level (progn - (org-insert-heading-respect-content) - (insert name))) - ('subheading (progn - (org-insert-heading-respect-content) - (org-shiftmetaright) - (insert name)))) + (when (called-interactively-p) + (cl-case (completing-read-value "Create a new heading? " option) + ('same-level (progn + (org-insert-heading-respect-content) + (insert name))) + ('subheading (progn + (org-insert-heading-respect-content) + (org-shiftmetaright) + (insert name))))) (org-set-property track-prop track-val))) @@ -559,19 +566,82 @@ the number of TIMES to mark progress." (dotimes (idx times) (rpgdm-ironsworn-mark-progress-track name))) +(defun rpgdm-ironsworn--progress-amount (name) + "Return list of aspects of progress by NAME. +Including: + - track :: The name of the track (same as `name') + - value :: The number complexity level, e.g. how many ticks per marked progress + - level :: The string label of a progress, e.g. Epic or Troublesome + - ticks :: The amount of ticks marked, shnould be <= 40 + - boxes :: The number of completed boxes (ticks / 4, rounded down) + - remain :: The number of remaining ticks marked towards the next box." + (let* ((tracks (rpgdm-ironsworn-character-progresses)) + (matched (assoc name tracks)) + (track (first matched)) + (value (second matched)) + (level (rpgdm-ironsworn-progress-level-label value)) + (ticks (third matched)) + (boxes (/ ticks 4)) + (remain (% ticks 4))) + (list track value level ticks boxes remain))) + +(defun rpgdm-ironsworn--progress-box (boxes leftover) + "Return a org-like table of current completion of an Ironsworn progress. +For instance, with 4 boxes and 2 leftover tick marks, this will return: +| ■ | ■ | ■ | ■ | x | | | | | | " + (defun make-box (boxes leftover-ticks blanks) + (cond + ((> boxes 0) (concat " ■ |" (make-box (1- boxes) leftover-ticks blanks))) + + ((> leftover-ticks 2) (concat " * |" (make-box 0 0 blanks))) + ((> leftover-ticks 1) (concat " x |" (make-box 0 0 blanks))) + ((> leftover-ticks 0) (concat " - |" (make-box 0 0 blanks))) + + ((> blanks 0) (concat " |" (make-box 0 0 (1- blanks)))))) + + (when (> leftover 3) + (setq boxes (+ boxes (/ leftover 4))) + (setq leftover (% leftover 4))) + (when (< boxes 0) + (setq boxes 0)) + (when (> boxes 10) + (setq boxes 10)) + (setq blanks (if (> leftover 0) + (- 9 boxes) + (- 10 boxes))) + + (concat "|" (make-box boxes leftover blanks))) + +(ert-deftest rpgdm-ironsworn--progress-box-test () + (should (equal (rpgdm-ironsworn--progress-box 0 0) "| | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 0 1) "| - | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 0 2) "| x | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 0 3) "| * | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 1 0) "| ■ | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 2 0) "| ■ | ■ | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 3 0) "| ■ | ■ | ■ | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 0) "| ■ | ■ | ■ | ■ | | | | | | |" + (should (equal (rpgdm-ironsworn--progress-box 4 1) "| ■ | ■ | ■ | ■ | - | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 2) "| ■ | ■ | ■ | ■ | x | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 3) "| ■ | ■ | ■ | ■ | * | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 5 0) "| ■ | ■ | ■ | ■ | ■ | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 10 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |")) + ;; Negative test cases + (should (equal (rpgdm-ironsworn--progress-box 11 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |")) + (should (equal (rpgdm-ironsworn--progress-box -1 0) "| | | | | | | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 8) "| ■ | ■ | ■ | ■ | ■ | ■ | | | | |")) + (should (equal (rpgdm-ironsworn--progress-box 4 6) "| ■ | ■ | ■ | ■ | ■ | x | | | | |"))) + (defun rpgdm-ironsworn-progress-amount (name) "Display the progress made against a track, NAME." (interactive (list (rpgdm-ironsworn-progress-track-choose))) - (let* ((tracks (rpgdm-ironsworn-character-progresses)) - (matched (--filter (equal name (first it)) tracks)) - (track (first matched)) - (value (second track)) - (level (rpgdm-ironsworn-progress-level-label value)) - (ticks (third track)) - (boxes (/ ticks 4))) - (if (called-interactively-p 'any) - (rpgdm-message "[%d] Progress on %s: %d (Ticks: %d)" level name boxes ticks) - boxes))) + (cl-destructuring-bind + (track value level ticks boxes leftover) + (rpgdm-ironsworn--progress-amount name) + (if (not (called-interactively-p 'any)) + boxes + (rpgdm-message "[%s] Progress on '%s': %d %s" level name boxes + (rpgdm-ironsworn--progress-box boxes leftover))))) (defun rpgdm-ironsworn-progress-roll (progress-value) "Display a Hit/Miss message based on the PROGRESS-VALUE. @@ -950,8 +1020,8 @@ Return 0 if not at a heading, or above first headline." (defun rpgdm-ironsworn--current-character-state (results) "Recursive helper to insert current header properties in RESULTS. -Calls itself if it is not looking at the top-level header in the -file. If a property is already in the hash table, RESULTS, it is +Calls itself if it is not looking at the top level header in the +file. If a property is already in the hash table, RESULTS, it is not overwritten, thereby having lower-level subtrees take precendence over similar settings in higher headers." (defun key-convert (ironsworn-prop)