dialogue.lisp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. ;;;; Copyright © 2023, Jaidyn Ann <jadedctrl@posteo.at>
  2. ;;;;
  3. ;;;; This program is free software: you can redistribute it and/or
  4. ;;;; modify it under the terms of the GNU General Public License as
  5. ;;;; published by the Free Software Foundation, either version 3 of
  6. ;;;; the License, or (at your option) any later version.
  7. ;;;;
  8. ;;;; This program is distributed in the hope that it will be useful,
  9. ;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;;;; GNU General Public License for more details.
  12. ;;;;
  13. ;;;; You should have received a copy of the GNU General Public License
  14. ;;;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. ;;;; FLORA-SEARCH-AURORA.DIALOGUE 💬
  16. ;;;; The dialogue-scripting part of the game. This handles all dialogue!
  17. (in-package :flora-search-aurora.dialogue)
  18. ;;; ———————————————————————————————————
  19. ;;; Dialogue-generation helpers
  20. ;;; ———————————————————————————————————
  21. (defun start-dialogue (&rest dialogue-tree)
  22. (reduce #'append dialogue-tree))
  23. (defun face (speaker face &optional (talking-face nil))
  24. (if talking-face
  25. (list
  26. (list :speaker speaker
  27. :face face
  28. :set :normal-face
  29. :to face)
  30. (list :speaker speaker
  31. :set :talking-face
  32. :to talking-face))
  33. (list
  34. (list :speaker speaker
  35. :face face
  36. :set :normal-face
  37. :to face))))
  38. (defun say (speaker &rest keys)
  39. (list
  40. (list :speaker speaker :progress 0
  41. :face (or (getf keys :face) 'talking-face)
  42. :text (…:getf-lang keys))
  43. (list :speaker speaker :face 'normal-face)))
  44. (defun mumble (speaker &rest keys)
  45. (list
  46. (list :speaker speaker :progress 0
  47. :text (…:getf-lang keys)
  48. :face (getf keys :face))))
  49. (defun move (speaker coords &key (delay .05))
  50. (if (or (getf coords :Δx) (getf coords :Δy))
  51. (list
  52. (list :speaker speaker :relative-coords coords :delay delay))
  53. (list
  54. (list :speaker speaker :coords coords :delay delay))))
  55. ;;; ———————————————————————————————————
  56. ;;; Accessors
  57. ;;; ———————————————————————————————————
  58. (defun dialogue-speaker (dialogue)
  59. "Get the DIALOGUE-speaker’s corresponding identifying symbol.
  60. Because they’re stored in strings. So we gotta, like, unstringify. Ya dig?"
  61. (getf dialogue :speaker))
  62. ;;; ———————————————————————————————————
  63. ;;; Dialogue logic
  64. ;;; ———————————————————————————————————
  65. (defun appropriate-face (map speaker face)
  66. "Return the face appropriate for the speaker.
  67. If FACE is a string, used that.
  68. If FACE is 'TALKING-FACE, then use their talking-face (if they have one).
  69. If FACE is 'NORMAL-FACE, then use their normal-face (if they’ve got one).
  70. If FACE is NIL… guess what that does. :^)"
  71. (let ((talking-face (🌍:getf-entity-data map speaker :talking-face))
  72. (normal-face (🌍:getf-entity-data map speaker :normal-face)))
  73. (cond ((and (eq face 'talking-face)
  74. talking-face)
  75. talking-face)
  76. ((and (eq face 'normal-face)
  77. normal-face)
  78. normal-face)
  79. ((stringp face)
  80. face))))
  81. (defun update-speaking-face (map dialogue)
  82. "Given a line (plist) of DIALOGUE, change speaker’s face to either their
  83. talking-face or the face given by the DIALOGUE."
  84. (let* ((speaker (getf dialogue :speaker))
  85. (new-face (appropriate-face map speaker (getf dialogue :face))))
  86. ;; Replace the face, when appropriate.
  87. (when new-face
  88. (setf (🌍:getf-entity-data map speaker :face) new-face))))
  89. (defun update-entity-data (map dialogue)
  90. "Given a plist of DIALOGUE, update an arbitrary bit of data in the speaker's
  91. data, using :SET and :TO of the DIALOGUE."
  92. (let* ((speaker (getf dialogue :speaker))
  93. (key (getf dialogue :set))
  94. (data (getf dialogue :to)))
  95. (when (and key data)
  96. (setf (🌍:getf-entity-data map speaker key) data))))
  97. ;; (format *error-output* "[~A] ~A → ~A???~%" dialogue key data)
  98. ;; (format *error-output* "~A!!!!~%" (🌍:getf-entity-data map speaker :normal-face)))))
  99. (defun progress-line-delivery (dialogue)
  100. "Progress the delivery of a line (plist) of DIALOGUE. That is, increment the
  101. “said character-count” :PROGRESS, which dictates the portion of the message that
  102. should be printed on the screen at any given moment."
  103. (let ((progress (getf dialogue :progress))
  104. (text (getf dialogue :text)))
  105. (when (and text
  106. (< progress (length text)))
  107. (incf (getf dialogue :progress) 1))))
  108. (defun progress-movement (map dialogue)
  109. "Move the entity by one tile in the targeted position — that is, the
  110. coordinates listed in the DIALOGUE’s :COORDS property. … If applicable, ofc."
  111. (let* ((speaker (dialogue-speaker dialogue))
  112. (target-coords (getf dialogue :coords))
  113. (speaker-coords (🌍:getf-entity-data map speaker :coords))
  114. (finished-moving-p (if target-coords (…:plist= speaker-coords target-coords) 't)))
  115. (when (not finished-moving-p)
  116. (🌍:move-entity
  117. map speaker
  118. :Δx (cond ((< (getf target-coords :x) (getf speaker-coords :x)) -1)
  119. ((> (getf target-coords :x) (getf speaker-coords :x)) 1)
  120. ('t 0))
  121. :Δy (cond ((< (getf target-coords :y) (getf speaker-coords :y)) -1)
  122. ((> (getf target-coords :y) (getf speaker-coords :y)) 1)
  123. ('t 0)))
  124. (sleep (or (getf dialogue :delay) 0)))
  125. finished-moving-p))
  126. (defun ensure-dialogue-movement (map dialogue-list)
  127. "Given a DIALOGUE-LIST, ensure that the first line of dialogue’s movement is
  128. absolute rather than relative, if it contains any movement at all."
  129. (let ((dialogue (car dialogue-list)))
  130. (when (and (getf dialogue :relative-coords)
  131. (not (getf dialogue :coords)))
  132. (let ((speaker-coords (🌍:getf-entity-data map (dialogue-speaker dialogue) :coords))
  133. (relative-coords (getf dialogue :relative-coords)))
  134. (setf (getf (car dialogue-list) :coords)
  135. (list :x (+ (getf speaker-coords :x) (or (getf relative-coords :Δx) 0))
  136. :y (+ (getf speaker-coords :y) (or (getf relative-coords :Δy) 0))))))))
  137. (defun finished-printing-p (dialogue)
  138. "Whether or not a line of dialogue has been completely printed to the screen."
  139. (or (not (getf dialogue :text))
  140. (eq (length (getf dialogue :text))
  141. (getf dialogue :progress))))
  142. (defun dialogue-state-update (map dialogue-list)
  143. "The logic/input-processing helper function for DIALOGUE-STATE.
  144. Progress through the lines of dialogue when the user hits ENTER, etc.
  145. Returns the state for use with STATE-LOOP, pay attention!"
  146. (update-speaking-face map (car dialogue-list))
  147. (update-entity-data map (car dialogue-list))
  148. (progress-line-delivery (car dialogue-list))
  149. (ensure-dialogue-movement map dialogue-list)
  150. ;; Progress to the next line of dialogue as appropriate.
  151. (let* ((dialogue (car dialogue-list))
  152. (text (getf dialogue :text))
  153. (did-press-enter-p (⌨:pressed-enter-p))
  154. (did-finish-printing-p (finished-printing-p dialogue))
  155. (did-finish-moving-p (progress-movement map dialogue)))
  156. ;; Only show the cursor when rendering text!
  157. (if (or did-finish-moving-p (not did-finish-printing-p))
  158. (✎:show-cursor)
  159. (✎:hide-cursor))
  160. (cond
  161. ;; When enter’s hit and most everything is done (rendering text, etc),
  162. ;; progress the dialogue.
  163. ((or (and did-press-enter-p did-finish-printing-p did-finish-moving-p)
  164. (and (not text) did-finish-moving-p))
  165. (if (cdr dialogue-list)
  166. (list :parameters (list :dialogue (cdr dialogue-list) :map map))
  167. (progn
  168. (✎:hide-cursor)
  169. (clear-input)
  170. (list :drop (1+ (or (getf dialogue :drop) 0))
  171. :function (getf dialogue :function)
  172. :parameters (if (member :parameters dialogue)
  173. (getf dialogue :parameters)
  174. (list :map map))))))
  175. ;; Allow interupting text-printing to end it!
  176. ((and did-press-enter-p (not did-finish-printing-p))
  177. (setf (getf (car dialogue-list) :progress) (length text))
  178. (list :parameters (list :dialogue dialogue-list :map map)))
  179. ;; If no input, keep steady!
  180. ('t
  181. (list :parameters (list :dialogue dialogue-list :map map))))))
  182. ;;; ———————————————————————————————————
  183. ;;; Dialogue drawing
  184. ;;; ———————————————————————————————————
  185. (defun optimal-text-placement-horizontally (text coords &key (rightp nil) (width 72) (height 20))
  186. "Given a horizontal direction (RIGHTP defined or nil) and a focal point COORDS,
  187. return the parameters of a text-box that can optimally fit the given TEXT in the
  188. direction specified relative to the focal point. If a legible position can’t be
  189. found, just give up! Return nil.
  190. Otherwise, return a list list with the coordinates, textbox width, and textbox
  191. height — all parameters for use with RENDER-STRING & co."
  192. (let* ((text-x-margin (if rightp
  193. (+ (getf coords :x) 3)
  194. 0))
  195. (text-width (…:at-most (floor (* width 3/5)) ;; Not _too_ wide!
  196. (if rightp
  197. (- width text-x-margin)
  198. (- (getf coords :x) 3))))
  199. (lines (ignore-errors (str:lines (…:linewrap-string text text-width))))
  200. (text-height (length lines)))
  201. ;; (format *error-output* "HORIZ COORD: ~A HEIGHT: ~A WIDTH ~A LINES:~%~S~%" coords text-height text-width lines)
  202. ;; When this layout is valid…
  203. (when (and lines
  204. (>= height text-height) ;; If the text’ll fit on screen
  205. (> text-width 10)) ;; If the text is wide enough to be legible
  206. (let* ((y (…:at-least 0 (- (getf coords :y)
  207. (if (eq text-height 1) ;; Align toward the speaker’s face
  208. 1 0)
  209. (floor (/ text-height 2)))))
  210. (x (if (and (not rightp)
  211. (eq (length lines) 1))
  212. (- text-width (length text))
  213. text-x-margin))
  214. (y-margin (if (> (+ y (length lines)) height) ;; How many lines are off-screen
  215. (- (+ y (length lines)) height)
  216. 0)))
  217. (list
  218. ;; Coords of text-box’es top-left corner
  219. (list :x x :y (- y y-margin))
  220. text-width ;; Width of text-box
  221. text-height))))) ;; Height of text-box
  222. (defun optimal-text-placement-vertically (text coords &key (downp nil) (width 72) (height 20))
  223. "Given a vertical direction (DOWNP defined or nil) and a focal point COORDS,)
  224. return the parameters of a text-box that can optimally fit the given TEXT in the
  225. direction specified relative to the focal point. Return nil if no such placement
  226. is found, otherwise return a list of the coordinates, textbox width, and textbox
  227. height (for use as parameters with RENDER-STRING et al.)."
  228. (let* ((text-y-margin (if downp
  229. (+ (getf coords :y) 2)
  230. (- (getf coords :y) 2)))
  231. (text-height (if downp
  232. (- height text-y-margin)
  233. (- text-y-margin 1)))
  234. (text-width (floor (* width 3/5))) ;; Too wide’s illegible! So ⅗-screen.
  235. (lines (ignore-errors (str:lines (…:linewrap-string text text-width)))))
  236. ;; (format *error-output* "VERT HEIGHT: ~A WIDTH ~A LINES: ~A~%" text-height text-width lines)
  237. ;; When the text can be printed with this layout…
  238. (when (and lines (>= text-height (length lines)))
  239. (let* ((y (…:at-least
  240. 0
  241. (if downp
  242. text-y-margin
  243. (- text-y-margin (length lines)))))
  244. (x (…:at-least
  245. 0
  246. (- (getf coords :x)
  247. (if (eq (length lines) 1)
  248. (floor (/ (length (car lines)) 2))
  249. (floor (/ text-width 2))))))
  250. (x-margin (if (> (+ x text-width) width)
  251. (- (+ x text-width) width)
  252. 0)))
  253. (list
  254. ;; Coords of text-box’es top-left corner
  255. (list :x (- x x-margin) :y y)
  256. text-width ;; Width of the text-box
  257. (length lines)))))) ;; Height of text-box
  258. (defun optimal-speech-layout (map dialogue &key (width 72) (height 20))
  259. "Given a line of DIALOGUE and MAP data, return the ideal “text-box” for the
  260. text. This tries to place the text on the screen without covering up anything
  261. important, if possible.
  262. The data returned is a list of the box’es top-left coordinate, max-column,
  263. and max-row; for use with RENDER-STRING. Like so:
  264. ((:x X :y Y) MAX-COLUMN MAX-ROW)"
  265. (let* ((speaker-id (dialogue-speaker dialogue))
  266. (playerp (eq speaker-id '✿:player))
  267. (leftp (not (🌍:getf-entity-data map speaker-id :facing-right)))
  268. (text (getf dialogue :text))
  269. (coords (🌍:world-coords->screen-coords (🌍:getf-entity-data map speaker-id :coords))))
  270. ;; Ideally, place text-box above/below (NPC/player); otherwise, place it behind speaker
  271. (or (optimal-text-placement-vertically text coords :width width :height height
  272. :downp playerp)
  273. (optimal-text-placement-horizontally text coords :width width :height height
  274. :rightp leftp)
  275. ;; … Worst-case scenario, just do whatever’ll fit :w:”
  276. (optimal-text-placement-horizontally text coords :width width :height height
  277. :rightp (not leftp))
  278. (optimal-text-placement-vertically text coords :width width :height height
  279. :downp (not playerp)))))
  280. (defun ensure-dialogue-layout (map dialogue-list)
  281. "Given a DIALOGUE-LIST, ensure that the FIRST line of dialogue has a :layout
  282. property — that is, a property detailing the optimal width and coordinates for
  283. its display."
  284. (when (and (getf (car dialogue-list) :text)
  285. (not (getf (car dialogue-list) :layout)))
  286. (setf (getf (car dialogue-list) :layout)
  287. (optimal-speech-layout map (car dialogue-list)))))
  288. (defun render-dialogue-block (matrix dialogue)
  289. "Render a bit of DIALOGUE to the MATRIX, in an intelligent fashion; that is,
  290. make it pretty, dang it! >O<
  291. ☆:.。.o(≧▽≦)o.。.:☆"
  292. (let* ((progress (getf dialogue :progress))
  293. (text (getf dialogue :text))
  294. (optimal-layout (getf dialogue :layout))
  295. (coords (car optimal-layout)))
  296. (when (and text optimal-layout)
  297. ;; (✎:render-fill-rectangle matrix #\space
  298. ;; (list :x (- (getf coords :x) 1)
  299. ;; :y (- (getf coords :y) 1)
  300. ;; (+ (second optimal-layout) 2) ;; Width
  301. ;; (+ (third optimal-layout) 1)) ;; Height
  302. (✎:render-string
  303. matrix text (first optimal-layout)
  304. :width (second optimal-layout)
  305. :char-count progress))))
  306. (defun dialogue-state-draw (matrix map dialogue-list)
  307. "Draw the dialogue where appropriate.
  308. Helper function for DIALOGUE-STATE."
  309. (when (getf (car dialogue-list) :text)
  310. (✎:show-cursor)
  311. (ensure-dialogue-layout map dialogue-list)
  312. (render-dialogue-block matrix (car dialogue-list))))
  313. ;;; ———————————————————————————————————
  314. ;;; Dialogue loop
  315. ;;; ———————————————————————————————————
  316. (defun dialogue-state (matrix &key dialogue map)
  317. "Render a bit of DIALOGUE to the screen, using :FLORA-SEARCH-AURORA.OVERWORLD
  318. entities as the speakers. Dialogue should be in the format:
  319. ((:text \"Hello, papa!\"
  320. :speaker \"son\" ;; The entity’s ID (if applicable)
  321. :face \"owo\") ;; If you want their face to change
  322. (:face \"=w=\" :speaker 'son) ;; change their face back when done talking
  323. (:text \"My dearest son, it’s been so long~!\"
  324. :speaker \"papa\"
  325. ...))
  326. A state-function for use with STATE-LOOP."
  327. (sleep .05)
  328. (dialogue-state-draw matrix map dialogue)
  329. (dialogue-state-update map dialogue))
  330. (defun make-dialogue-function (map dialogue-list)
  331. "Return a state-function for a section of dialogue, for use with STATE-LOOP."
  332. (lambda (matrix &key (map map) (dialogue dialogue-list))
  333. (🌍:overworld-state-draw matrix map)
  334. (dialogue-state matrix :map map :dialogue dialogue)))
  335. (defun make-dialogue-state (map dialogue-list)
  336. "Return a state-plist for a section of dialogue, for use with STATE-LOOP."
  337. (list :function (make-dialogue-function map dialogue-list)))
  338. ;; Split a banana in two, bisection-fruit,
  339. ;; Yummy-yummy, toot-toot~ 🎵