egme.el 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. ;;Copyright (C) 2021 Category <category@[no_spam]quintendo.uk>
  2. ;;
  3. ;;This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2.
  4. ;;
  5. ;;This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  6. ;;
  7. ;;You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  8. (defgroup egme nil
  9. "Variables for customizing the Emacs Games Master Emulator."
  10. :group 'games)
  11. (defcustom egme-action-list '("Abandon"
  12. "Abuse"
  13. "Adversity"
  14. "Agree"
  15. "Ambush"
  16. "Antagonise"
  17. "Arrive"
  18. "Assist"
  19. "Attach"
  20. "Befriend"
  21. "Bestow"
  22. "Betray"
  23. "Block"
  24. "Break"
  25. "Celebrate"
  26. "Change"
  27. "Communicate"
  28. "Control"
  29. "Create"
  30. "Cruelty"
  31. "Deceive"
  32. "Delay"
  33. "Desert"
  34. "Develop"
  35. "Dispute"
  36. "Disrupt"
  37. "Divide"
  38. "Dominate"
  39. "Expose"
  40. "Failure"
  41. "Fight"
  42. "Gratify"
  43. "Guide"
  44. "Harm"
  45. "Heal"
  46. "Immitate"
  47. "Imprison"
  48. "Inform"
  49. "Inquire"
  50. "Inspect"
  51. "Intolerance"
  52. "Judge"
  53. "Kill"
  54. "Malice"
  55. "Mistrust"
  56. "Move"
  57. "Neglect"
  58. "Open"
  59. "Oppose"
  60. "Oppress"
  61. "Passion"
  62. "Persecute"
  63. "Praise"
  64. "Procrastinate"
  65. "Propose"
  66. "Punish"
  67. "Pursue"
  68. "Release"
  69. "Return"
  70. "Ruin"
  71. "Separate"
  72. "Spy"
  73. "Starting"
  74. "Stop"
  75. "Take"
  76. "Transform"
  77. "Travel"
  78. "Trick"
  79. "Trust"
  80. "Violate"
  81. "Waste"
  82. "Work")
  83. "List of 'Action' variables used in the random event generator."
  84. :type '(repeat string)
  85. :group 'egme)
  86. (defcustom egme-subject-list '("A path"
  87. "A project"
  88. "Adversities"
  89. "Advice"
  90. "Allies"
  91. "Ambush"
  92. "Animals"
  93. "Art"
  94. "Attention"
  95. "Balance"
  96. "Bureaucracy"
  97. "Business"
  98. "Competition"
  99. "Danger"
  100. "Death"
  101. "Dispute"
  102. "Disruption"
  103. "Dreams"
  104. "Elements"
  105. "Emotions"
  106. "Energy"
  107. "Environment"
  108. "Expectations"
  109. "Extravagance"
  110. "Failure"
  111. "Fame"
  112. "Fears"
  113. "Friendship"
  114. "Goals"
  115. "Home"
  116. "Hope"
  117. "Illness"
  118. "Information"
  119. "Inside"
  120. "Intrigues"
  121. "Jealousy"
  122. "Joy"
  123. "Leadership"
  124. "Lies"
  125. "Masses"
  126. "Messages"
  127. "Military"
  128. "Nature"
  129. "New ideas"
  130. "Opposition"
  131. "Outside"
  132. "Pain"
  133. "Peace"
  134. "Plans"
  135. "Pleasures"
  136. "Portals"
  137. "Possessions"
  138. "Power"
  139. "Prison"
  140. "Randomness"
  141. "Reality"
  142. "Riches"
  143. "Rumor"
  144. "Stalemate"
  145. "Status quo"
  146. "Success"
  147. "Suffering"
  148. "Tactics"
  149. "Technology"
  150. "Travel"
  151. "Trials"
  152. "Vehicle"
  153. "Victory"
  154. "Weapons"
  155. "Weather"
  156. "Wishes"
  157. "Wounds")
  158. "List of 'Subject' variables used in the random event generator."
  159. :type '(repeat string)
  160. :group 'egme)
  161. (defcustom egme--random-event-threshold 20
  162. "Set the upper limit of questions before a random event happens. Low values mean random events happen more frequently, high values and the are more sporadic. After this many questions a random event will definitely occur."
  163. :type 'natnum
  164. :group 'egme)
  165. (setq egme-dice-history (list))
  166. ;; Standard probability list for ido-completing-read
  167. (setq egme-probability-list (list
  168. "0 Even odds"
  169. "-1 Unlikely"
  170. "-2 Very Unlikely"
  171. "-3 Extremely Unlikely"
  172. "-4 Near Impossible"
  173. "+4 Near Certain"
  174. "+3 Extremely Likely"
  175. "+2 Very Likely"
  176. "+1 Likely"))
  177. (setq egme-random-counter 0)
  178. (setq egme--random-event-list (list
  179. "Remote event"
  180. "NPC action"
  181. "New NPC appears"
  182. "Move towards thread"
  183. "Move away from thread"
  184. "PC positive"
  185. "PC negative"
  186. "NPC positive"
  187. "NPC negative"
  188. "Ambiguous event"))
  189. (setq egme-npc-list (list))
  190. (setq egme-thread-list (list))
  191. (defun egme--random-list-item (list-to-pick-from)
  192. "This function takes a list as an argument, and returns a random element from within that list.
  193. Will return nil if provided list is nil."
  194. (cond
  195. (list-to-pick-from (nth (random (length list-to-pick-from)) list-to-pick-from))
  196. (t nil)))
  197. (defun egme--get-dice ()
  198. "Get the required dice-roll from user input on the mini-buffer. Dice rolls to be expected in the usual [number]D[dice-type][modifier] format used by RPGs, for example '2D6' for 2 six-sided dice, or '3d8+2' for 3 eight-sided dice, with 2 added to the result. If the format is given without number (for example 'd100'), then it is assume to be a single dice being rolled.
  199. If no input is given, then it will return the last dice rolled. A full history of rolls is stored in 'egme-dice-history', accessible via the arrow keys when asked for input.
  200. Returns the dice-type, which is also stored in the variable egme-current-dice - returns nil if input can't be parsed into a dice roll."
  201. (setq egme-current-dice (read-string (format "Enter dice roll (default %s): " (car egme-dice-history))
  202. nil
  203. 'egme-dice-history
  204. (car egme-dice-history)))
  205. ;; Add a leading "1" in case user inputs without type (i.e just "D100")
  206. (when (string-match "^[dD]" egme-current-dice)
  207. (setq egme-current-dice (concat "1" egme-current-dice)))
  208. ;; Look for string in dice-roll format, return if found
  209. (when (string-match "[1-9][0-9]?[dD][1-9][0-9]*\\([+-][0-9]+\\)?" egme-current-dice)
  210. (setq egme-current-dice (match-string 0 egme-current-dice))))
  211. (defun egme--calculate-dice (&optional dice-roll)
  212. "Calculates the current dice roll. If called alone, roll the variable egme-current-dice. If argument DICE-ROLL is provided, roll that - it must be in RPG dice notation ('1d20', '3d10+8', '2d6-4', etc). Return the result of the dice roll, and store in the variable egme-roll-result.
  213. Current roll is broken down into the following variable for calculating:-
  214. +egme-current-dice-quantity
  215. +egme-current-dice-type
  216. +egme-current-dice-modifier
  217. This function loops for the quantity of dice, summing up random numbers for the appropriate type, then applying the modifier. In the case of a multiple D6 type (ie D66/D666/D6666...) then instead of summing the results it treats each D6 roll as a different digit in the final result."
  218. ;; Reset last roll result
  219. (setq egme-roll-result 0)
  220. (setq egme-multi-6-temp nil)
  221. ;; Set egme-current-dice if an option was passed with the function call
  222. (when dice-roll
  223. (setq egme-current-dice dice-roll))
  224. ;; Get quantity of dice rolled
  225. (string-match "^[1-9]+" egme-current-dice)
  226. (setq egme-current-dice-quantity (string-to-number (match-string 0 egme-current-dice)))
  227. ;; Get current dice type
  228. (string-match "[dD][1-9][0-9]*" egme-current-dice)
  229. (setq egme-current-dice-type (string-to-number (string-trim-left (match-string 0 egme-current-dice) "[dD]")))
  230. ;; Get modifier (if present, else set to 0)
  231. (if (string-match "[+-][0-9]+$" egme-current-dice)
  232. (setq egme-current-dice-modifier (string-to-number (match-string 0 egme-current-dice)))
  233. (setq egme-current-dice-modifier 0))
  234. ;; Check if dice type is a D66/D666/D6666 etc
  235. (if (string-match "^66+$" (number-to-string egme-current-dice-type))
  236. ;; If a multi-6 dice, roll each D6 and combine as string, then repeat for each quantity of rolls
  237. (dotimes (n egme-current-dice-quantity)
  238. (dotimes (n (length (number-to-string egme-current-dice-type)))
  239. (setq egme-multi-6-temp (concat egme-multi-6-temp (number-to-string (+ 1 (random 6))))))
  240. (setq egme-roll-result (string-to-number egme-multi-6-temp)))
  241. ;; Else calculate dice as usual
  242. (dotimes (n egme-current-dice-quantity)
  243. (setq egme-roll-result (+ egme-roll-result (+ 1 (random egme-current-dice-type))))))
  244. ;; Add the modifier to the result, for the final roll
  245. (setq egme-roll-result (+ egme-roll-result egme-current-dice-modifier)))
  246. (defun egme--print-output (print-string)
  247. "This function takes a string in as an argument, and prints it's output into the current buffer, within a GameMaster org block.
  248. If the current buffer is not an org-mode document, it will check if it is fundamental. If it is, org-mode will be activated, else it will throw an error stating which major-mode is currently active. It will still print output regardless."
  249. ;; Move point to "safe" position
  250. (end-of-line)
  251. ;; Add additional newline if current line contains is non-blank
  252. (when (string-match "[:ascii:]" (thing-at-point 'line t))
  253. (newline))
  254. (newline)
  255. ;; Check mode, change to org if non-fundamental, else throw message
  256. (if (not (string-match "^org" (format "%s" major-mode)))
  257. (if (string-match "^fundamental" (format "%s" major-mode))
  258. (org-mode)
  259. (message "Current buffer is neither org nor fundamental, leaving as-is")))
  260. ;; Output the start line
  261. (insert "#+BEGIN_GameMaster")
  262. (newline)
  263. ;; Output text generated by egme functions
  264. (insert print-string)
  265. (newline)
  266. ;; Output the end line
  267. (insert "#+END_GameMaster")
  268. (newline 2)
  269. t)
  270. (defun egme--random-event ()
  271. "A function for genereating unexpected events.
  272. When an oracle question is asked, this function is called. It keeps a counter in the variable egme-random-counter, which is incremented each time this is called. A random number is generated between 0 and the variable egme--random-event-threshold - if the result is lower than the current egme-random-counter value, then a random event is generated. A focus, action and subject are randomly selected from the lists (egme--random-event-list, egme-action-list, and egme-subject-list respectively). If a random event was generated, the counter is reset to 0.
  273. If the chosen event concerns an NPC (ignoring the New NPC event), it will display a random NPC from the current list (if available). Likewise, if the event concerns a thread, it will pick a random one from the list.
  274. This function then returns the random event text, for the calling function to pass on to for user output."
  275. ;; Increment random counter
  276. (setq egme-random-counter (1+ egme-random-counter))
  277. ;; Clear random event output text
  278. (setq egme--random-event-output nil)
  279. ;; Generate a random number up to the egme--random-event-threshold, and compare against current counter
  280. (if (< (random egme--random-event-threshold) egme-random-counter)
  281. ;; Below batch of steps to take if
  282. (progn
  283. ;; Announce the event
  284. (setq egme--random-event-output "\n------------\nRandom Event!")
  285. ;; Pick random event from the random event focus list
  286. (setq egme--random-event-output (concat egme--random-event-output (format "\n Focus: %s" (egme--random-list-item egme--random-event-list))))
  287. ;; Check if it's an NPC event
  288. (if (string-match-p "NPC" egme--random-event-output)
  289. ;; Make sure it is /not/ a "New NPC" event
  290. (if (not (string-match-p "New NPC" egme--random-event-output))
  291. ;; Only change output if NPC list is non-nil
  292. (if (egme--parse-npc-list)
  293. (setq egme--random-event-output (concat egme--random-event-output (format "\n NPC: %s" (egme--random-list-item (egme--parse-npc-list))))))))
  294. ;; Check if it's a Thread event, add a random Thread from the list - just checks if "thread" is in the current print output variable
  295. (if (string-match-p "thread" egme--random-event-output)
  296. ;; Only change output if Thread list is non-nil
  297. (if (egme--parse-thread-list)
  298. (setq egme--random-event-output (concat egme--random-event-output (format "\n Thread: %s" (egme--random-list-item (egme--parse-thread-list)))))))
  299. ;; Add event details
  300. (setq egme--random-event-output (concat egme--random-event-output (format "\n Detail: %s" (egme--random-list-item egme-action-list))(format " / %s" (egme--random-list-item egme-subject-list))))
  301. ;; Reset the random counter
  302. (setq egme-random-counter 0)
  303. ;; Return text output
  304. egme--random-event-output)
  305. ;; Return nil if no event found
  306. nil))
  307. (defun egme--open-org-drawer ()
  308. "This function will open an org-mode drawer on the current line, if it is currently closed.
  309. Open state is determined by checking if current line is a drawer, and if the text at the end of the line is visible. If it is invisible, open the drawer with org-cycle."
  310. (interactive)
  311. (if (and (org-at-drawer-p) (invisible-p (point-at-eol)))
  312. (org-cycle)
  313. (message "No closed drawer to open.")))
  314. (defun egme--close-org-drawer ()
  315. "This function will close an org-mode drawer on the current line, if it is currently open.
  316. Open state is determined by checking if current line is a drawer, and if the text at the end of the line is visible. If it is not invisible, close the drawer with org-cycle."
  317. (interactive)
  318. (if (and (org-at-drawer-p) (not (invisible-p (point-at-eol))))
  319. (org-cycle)
  320. (message "No open drawer to close")))
  321. (defun egme-roll-dice ()
  322. "This function is for a user to generate the results from a dice roll, and output them into the current buffer.
  323. egme--get-dice is called to get user input, egme-calculate dice is used to generate the result, and egme--print-output is used to place this into the current buffer, creating new lines below the point.
  324. This function is interactively callable via M-x, and a prime input option for key-binding."
  325. ; Let user call via M-x
  326. (interactive)
  327. ; Get dice size from user
  328. (egme--get-dice)
  329. ; Check dice input was correct
  330. (if egme-current-dice
  331. ; If valid then calculate result
  332. (egme--calculate-dice)
  333. ; Else drop an error message and exit
  334. (user-error "Could not parse dice roll"))
  335. ; Print results
  336. (egme--print-output (concat (format "Rolled: %s" egme-current-dice) (format "\nResult: %s" egme-roll-result)))
  337. ;; Update dashboard
  338. (egme--update-display-buffer)
  339. egme-roll-result)
  340. (defun egme-y-n-oracle ()
  341. "The basic oracle function. This will provide Yes/No answers to questions posed to the games master, and outputs the results in the current buffer in the standard games master format.
  342. The user will be asked to input a question - if the end of the current line is parsed as a question, then that will be set as the initial user input. If a quesiton is provided, it will be printed along with the results.
  343. Next, the user will be asked for the likelihood of this result. These options are stored in the list egme-probability-list, and selected via ido-completing-read. Each option is a modifier between -4 and +4, along with a basic description of the probability. This basic description will be printed along with the results.
  344. The answer is generated by rolling 1D10 and applying the chosen modifier, any result of a 6+ will be a 'Yes', anything else a 'No'. A D6 is also rolled, to see if it is an extreme answer - on a 1 it is a minor result (', but...'), and on a 2 it is a major result (', and...').
  345. The function egme--random-event is also called to see if anything unexpected occurs - any change will be added to the variable egme-oracle-output before it gets passed on for user output."
  346. (interactive)
  347. ; Reset some variables
  348. (setq egme-oracle-ouput nil)
  349. (setq egme-oracle-answer nil)
  350. (setq egme-current-question nil)
  351. ; Check if the current line contains a question (ends in a question mark, and gets everything from the last ellipses to the end of the line)
  352. (setq egme-current-line (thing-at-point 'line t))
  353. (if (string-match "\\.?[0-9A-Za-z ,:;']+\\? *$" egme-current-line)
  354. ; If that current line is a question, strip any leading ellipses or spaces, then set as pre-filled input when asking for the current question
  355. (setq egme-current-question (read-string "What is the question? " (replace-regexp-in-string " *$" "" (replace-regexp-in-string "^\\.* *" "" (match-string 0 egme-current-line)))))
  356. ; Else just ask user for question
  357. (setq egme-current-question (read-string "What is the question?: ")))
  358. ; Get probability from the user
  359. (setq egme-current-probability-choice (ido-completing-read "Probability modifier: " egme-probability-list))
  360. (string-match "[+\-]?[0-9]" egme-current-probability-choice)
  361. (setq egme-current-probability-modifier (match-string 0 egme-current-probability-choice))
  362. ; Roll dice, apply modifier
  363. (setq egme-oracle-answer-roll (+ (egme--calculate-dice "1d10") (string-to-number egme-current-probability-modifier)))
  364. (setq egme-oracle-answer-modifier (egme--calculate-dice "1d6"))
  365. ; Convert dice rolls into result text - check if modified oracle roll is 6+ ('Yes')
  366. (if (>= egme-oracle-answer-roll 6)
  367. ; If greater, then answer yes
  368. (setq egme-oracle-answer "Yes")
  369. ; Else answer no
  370. (setq egme-oracle-answer "No"))
  371. ; Apply answer modifier (if applicable)
  372. ; Add 'but' if rolled 1, add 'and' if rolled 2
  373. (cond ((eq egme-oracle-answer-modifier 1) (setq egme-oracle-answer (concat (format "%s" egme-oracle-answer) ", but...")))
  374. ((eq egme-oracle-answer-modifier 2) (setq egme-oracle-answer (concat (format "%s" egme-oracle-answer) ", and..."))))
  375. ;; Prepare output for printing
  376. ; Check if a question was input...
  377. (if (> (length egme-current-question) 0)
  378. ; ..then add quesiton to the output with results
  379. (setq egme-oracle-output (format " Question: %s\n" egme-current-question))
  380. (setq egme-oracle-output ""))
  381. ; Get probability text
  382. (string-match "[A-Za-z][A-Za-z ]*" egme-current-probability-choice)
  383. (setq egme-probability-text (match-string 0 egme-current-probability-choice))
  384. ; Add probability and results to output
  385. (setq egme-oracle-output (concat egme-oracle-output (format "Probability: %s\n------------" egme-probability-text) (format "\n Answer: %s" egme-oracle-answer)))
  386. ; Check for Random events, add any text to output
  387. (setq egme-oracle-output (concat egme-oracle-output (egme--random-event)))
  388. ; Send output string to display to user
  389. (egme--print-output egme-oracle-output))
  390. (defun egme-add-npc (&optional npc-name)
  391. "This function adds an NPC to the current file.
  392. NPCS are stored at the end of the file, under an :NPCS: drawer. It will search backwards from the end of the file for the drawer, and create it if not found. npc-name is then inserted on at the beginning of the drawer."
  393. (interactive)
  394. ;; Ask for NPC name if nothing is passed to the function
  395. (if npc-name
  396. t
  397. (setq npc-name (read-string "New NPC name? ")))
  398. ;; save-excursion so cursor returns to users current position
  399. (save-excursion
  400. (progn
  401. (end-of-buffer)
  402. ;; Search backwards for ":NPCS:"
  403. (if (search-backward ":NPCS:" nil t)
  404. ;; The drawer has been found, check if npc-name already exists - add if missing, throw an error if it already exists
  405. (if (member npc-name (egme--parse-npc-list))
  406. (user-error "NPC is already in the list")
  407. (progn
  408. (egme--open-org-drawer)
  409. (end-of-line)
  410. (newline)
  411. (insert npc-name)))
  412. ;; The :NPCS: drawer doesn't exist, create it and add the new npc-name
  413. (insert (concat "\n:NPCS:\n" npc-name "\n:END:\n")))
  414. ;; Fold the Drawer closed
  415. (search-backward ":NPCS:" nil t)
  416. (egme--close-org-drawer)))
  417. ;; Refresh dispay buffer if open
  418. (egme--update-display-buffer)
  419. ;; Return the added npc-name
  420. npc-name)
  421. (defun egme--parse-npc-list ()
  422. "This function gets locates the NPC list in the given file, and store all the names in the list egme-npc-list
  423. If the :NPCS: drawer cannot be found, then an error message will be created, and the function returns nil. Otherwise, the generated list will be returned (in addtion to being added to egme-npc-list variable)."
  424. (setq egme-npc-list nil)
  425. (save-excursion
  426. (progn
  427. (end-of-buffer)
  428. ;; Find NPC drawer
  429. (if (search-backward ":NPCS:" nil t)
  430. ;; Drawer found, turn it into a list
  431. (progn
  432. ;; Open drawer before parsing
  433. (egme--open-org-drawer)
  434. (next-line)
  435. ;; Loop until end of drawer found
  436. (while (not (string-match "^:END:" (thing-at-point 'line t)))
  437. (progn
  438. ;; Add current element, minus final character (trailing newline), then move to next
  439. (push (substring (thing-at-point 'line t) 0 -1) egme-npc-list)
  440. (next-line)))
  441. ;; Close the drawer again
  442. (search-backward ":NPCS:" nil t)
  443. (egme--close-org-drawer))
  444. ;; No NPC drawer found
  445. (message "No NPC list in current file"))))
  446. ; Return list contents (or nil if nothing is found)
  447. egme-npc-list)
  448. (defun egme-delete-npc ()
  449. "This function deletes an NPC from the active list.
  450. The NPC list is parsed, and all are offered as options with ido-completing-read. This is then found within the NPC list drawer, and the chosen option is deleted. This function then re-parses and returns the updated list."
  451. (interactive)
  452. ;; Check NPC list has been created
  453. (if (egme--parse-npc-list)
  454. ;; Parse latest NPC list, and get user input for which to delete
  455. (setq deleting-npc (ido-completing-read "NPC to delete? " (egme--parse-npc-list)))
  456. ;; Throw an error if nothing found
  457. (user-error "No NPCs in current file"))
  458. (save-excursion
  459. (progn
  460. ;; Go to end of buffer, then look backwards for the NPC list and open it
  461. (end-of-buffer)
  462. (search-backward ":NPCS:" nil t)
  463. (egme--open-org-drawer)
  464. ;; Search forwards for the selected deletion
  465. (search-forward deleting-npc nil t)
  466. (beginning-of-line)
  467. ;; Delete line and remove the newline to avoid a blank entry
  468. (kill-line)
  469. (kill-line)
  470. ;; Close the NPC drawer
  471. (search-backward ":NPCS:" nil t)
  472. (egme--close-org-drawer)))
  473. ;; Refresh dispay buffer if open
  474. (egme--update-display-buffer)
  475. ;; Return updated list
  476. (egme--parse-npc-list))
  477. (defun egme-add-thread (&optional new-thread)
  478. "This function adds a Thread to the current file.
  479. Threads are stored at the end of the file, under an :THREADS: drawer. It will search backwards from the end of the file for the drawer, and create it if not found. new-thread is then inserted on at the beginning of the drawer."
  480. (interactive)
  481. ;; Ask for Thread if nothing is passed to the function
  482. (if new-thread
  483. t
  484. (setq new-thread (read-string "New Thread description? ")))
  485. ;; save-excursion so cursor returns to users current position
  486. (save-excursion
  487. (progn
  488. (end-of-buffer)
  489. ;; Search backwards for ":THREADS:"
  490. (if (search-backward ":THREADS:" nil t)
  491. ;; The drawer has been found, check if new-thread already exists - add if missing, throw an error if it already exists
  492. (if (member new-thread (egme--parse-thread-list))
  493. (user-error "Thread is already in the list")
  494. (progn
  495. (egme--open-org-drawer)
  496. (end-of-line)
  497. (newline)
  498. (insert new-thread)))
  499. ;; The :THREADS: drawer doesn't exist, create it and add the new-thread
  500. (insert (concat "\n:THREADS:\n" new-thread "\n:END:\n")))
  501. ;; Fold the Drawer closed
  502. (search-backward ":THREADS:" nil t)
  503. (egme--close-org-drawer)))
  504. ;; Refresh dispay buffer if open
  505. (egme--update-display-buffer)
  506. ;; Return the added new-thread
  507. new-thread)
  508. (defun egme--parse-thread-list ()
  509. "This function gets locates the Thread list in the given file, and store all the items in the list egme-thread-list
  510. If the :THREADS: drawer cannot be found, then an error message will be created, and the function returns nil. Otherwise, the generated list will be returned (in addtion to being added to egme-thread-list variable)."
  511. (setq egme-thread-list nil)
  512. (save-excursion
  513. (progn
  514. (end-of-buffer)
  515. ;; Find Thread drawer
  516. (if (search-backward ":THREADS:" nil t)
  517. ;; Drawer found, turn it into a list
  518. (progn
  519. ;; Open drawer before parsing
  520. (egme--open-org-drawer)
  521. (next-line)
  522. ;; Loop until end of drawer found
  523. (while (not (string-match "^:END:" (thing-at-point 'line t)))
  524. (progn
  525. ;; Add current element, minus final character (trailing newline), then move to next
  526. (push (substring (thing-at-point 'line t) 0 -1) egme-thread-list)
  527. (next-line)))
  528. ;; Close the drawer again
  529. (search-backward ":THREADS:" nil t)
  530. (egme--close-org-drawer))
  531. ;; No THREADS drawer found
  532. (message "No Thread list in current file"))))
  533. ; Return list contents (or nil if nothing is found)
  534. egme-thread-list)
  535. (defun egme-delete-thread ()
  536. "This function deletes a Thread from the active list.
  537. The Thread list is parsed, and all are offered as options with ido-completing-read. This is then found within the Thread list drawer, and the chosen option is deleted. This function then re-parses and returns the updated list."
  538. (interactive)
  539. ;; Check Thread list has been created
  540. (if (egme--parse-thread-list)
  541. ;; Parse latest Thread list, and get user input for which to delete
  542. (setq deleting-thread (ido-completing-read "Thread to delete? " (egme--parse-thread-list)))
  543. ;; Throw an error if nothing found
  544. (user-error "No Threads in current file"))
  545. (save-excursion
  546. (progn
  547. ;; Go to end of buffer, then look backwards for the Thread list and open it
  548. (end-of-buffer)
  549. (search-backward ":THREADS:" nil t)
  550. (egme--open-org-drawer)
  551. ;; Search forwards for the selected deletion
  552. (search-forward deleting-thread nil t)
  553. (beginning-of-line)
  554. ;; Delete line and remove the newline to avoid a blank entry
  555. (kill-line)
  556. (kill-line)
  557. ;; Close the Thread drawer
  558. (search-backward ":THREADS:" nil t)
  559. (egme--close-org-drawer)))
  560. ;; Refresh dispay buffer if open
  561. (egme--update-display-buffer)
  562. ;; Return updated list
  563. (egme--parse-thread-list))
  564. (defun egme-dashboard ()
  565. "This function will create a temporary buffer to display current details of the game state.
  566. At present this is the NPC list & Thread list (formatted 1 item per line), and the results of the last dice roll.
  567. This function always returns nil."
  568. (interactive)
  569. ;; Update all lists from curent file
  570. (egme--parse-npc-list)
  571. (egme--parse-thread-list)
  572. ;; Remember old window split thresholds, and change current to 1 too force a horizontal split
  573. (setq egme-old-threshold split-width-threshold)
  574. (setq split-width-threshold 1)
  575. ;; Save location in current buffer
  576. (save-excursion
  577. ;; Create temporary read-only buffer and move to it for output
  578. (with-output-to-temp-buffer "GameMaster"
  579. (set-buffer "GameMaster")
  580. ;; Print header
  581. (org-mode)
  582. (insert "*eGME- Emacs GameMaster Emulator*\n---\n\n\n")
  583. ;; Check if NPC list is empty
  584. (if (not egme-npc-list)
  585. ;; Output when no list found
  586. (insert "No NPCs at present")
  587. ;; Output when NPCs found
  588. (progn
  589. (insert "NPCs\n---\n")
  590. ;; Loop through NPC list
  591. (while egme-npc-list
  592. ;; Pop the list, using each item as output followed by newline
  593. (insert (pop egme-npc-list))
  594. (newline))))
  595. (newline)
  596. (newline)
  597. ;; Check if Thread list is empty
  598. (if (not egme-thread-list)
  599. ;; Output when no list found
  600. (insert "No Threads at present\n")
  601. ;; Output when Threads found
  602. (progn
  603. (insert "Threads\n---\n")
  604. ;; Loop through Thread list
  605. (while egme-thread-list
  606. ;; Pop the list, using each item as output followed by newline
  607. (insert (pop egme-thread-list))
  608. (newline))))
  609. ;; Insert last dice roll info
  610. (insert "\n\nLast Dice Roll\n---\n")
  611. (insert (concat (format " Roll: %s" egme-current-dice) (format "\nResult: %s" egme-roll-result))))
  612. ;; Switch to the new window, temporarily alow horizontal changes, and shrink it to fit the contents
  613. (other-window 1)
  614. (set-variable 'fit-window-to-buffer-horizontally 1)
  615. (fit-window-to-buffer)
  616. ;; Make it a bit bigger because it was shrinking too much...
  617. (enlarge-window-horizontally 7)
  618. (other-window 1))
  619. ;; Return to original split settings
  620. (setq split-width-threshold egme-old-threshold)
  621. nil)
  622. (defun egme--update-display-buffer ()
  623. "Simple function to reopen the game-state display if it is visible.
  624. This is to be called at the end of anything that changes displayed information."
  625. (if (get-buffer-window "GameMaster")
  626. (egme-dashboard)))
  627. (defun egme-toggle-dash ()
  628. "This function will toggle egme dashboard visibilty.
  629. If it is visible it will kill the window, if not it will load it and update it."
  630. (interactive)
  631. (if (get-buffer-window "GameMaster")
  632. (progn
  633. (delete-window (get-buffer-window "GameMaster"))
  634. (kill-buffer "GameMaster"))
  635. (egme-dashboard)))
  636. (define-prefix-command 'egme-map)
  637. (define-key mode-specific-map (kbd "C-g") 'egme-map)
  638. (define-key egme-map (kbd "r") 'egme-roll-dice)
  639. (define-key egme-map (kbd "q") 'egme-y-n-oracle)
  640. (define-key egme-map (kbd "n") 'egme-add-npc)
  641. (define-key egme-map (kbd "N") 'egme-delete-npc)
  642. (define-key egme-map (kbd "t") 'egme-add-thread)
  643. (define-key egme-map (kbd "T") 'egme-delete-thread)
  644. (define-key egme-map (kbd "d") 'egme-toggle-dash)