2023-06-19 08:47:45 -05:00
|
|
|
|
;;;; Copyright © 2023, Jaidyn Ann <jadedctrl@posteo.at>
|
|
|
|
|
;;;;
|
|
|
|
|
;;;; 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, either version 3 of
|
|
|
|
|
;;;; the License, or (at your option) any later version.
|
|
|
|
|
;;;;
|
|
|
|
|
;;;; 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.
|
|
|
|
|
;;;;
|
|
|
|
|
;;;; You should have received a copy of the GNU General Public License
|
|
|
|
|
;;;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
2023-06-28 09:56:25 -05:00
|
|
|
|
;;;; FLORA-SEARCH-AURORA.DIALOGUE
|
|
|
|
|
;;;; The dialogue-scripting part of the game. This handles all dialogue!
|
|
|
|
|
|
2023-06-19 08:47:45 -05:00
|
|
|
|
(in-package :flora-search-aurora.dialogue)
|
|
|
|
|
|
2023-06-21 20:31:29 -05:00
|
|
|
|
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
;;; Dialogue-generation helpers
|
2023-06-19 08:47:45 -05:00
|
|
|
|
;;; ———————————————————————————————————
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(defun start-dialogue (&rest dialogue-tree)
|
2023-07-01 12:50:24 -05:00
|
|
|
|
(reduce #'append dialogue-tree))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-24 19:49:18 -05:00
|
|
|
|
(defun face (speaker face &optional (talking-face nil))
|
|
|
|
|
(if talking-face
|
|
|
|
|
(list
|
|
|
|
|
(list :speaker speaker
|
|
|
|
|
:face face
|
|
|
|
|
:set :normal-face
|
|
|
|
|
:to face)
|
|
|
|
|
(list :speaker speaker
|
|
|
|
|
:set :talking-face
|
|
|
|
|
:to talking-face))
|
|
|
|
|
(list
|
|
|
|
|
(list :speaker speaker
|
|
|
|
|
:face face
|
|
|
|
|
:set :normal-face
|
|
|
|
|
:to face))))
|
2023-06-19 14:43:46 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-23 22:25:16 -05:00
|
|
|
|
(defun say (speaker &rest keys)
|
2023-06-19 08:47:45 -05:00
|
|
|
|
(list
|
2023-06-23 22:25:16 -05:00
|
|
|
|
(list :speaker speaker :progress 0
|
|
|
|
|
:face (or (getf keys :face) 'talking-face)
|
|
|
|
|
:text (…:getf-lang keys))
|
2023-06-24 19:49:18 -05:00
|
|
|
|
(list :speaker speaker :face 'normal-face)))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-23 22:25:16 -05:00
|
|
|
|
(defun mumble (speaker &rest keys)
|
2023-06-19 08:47:45 -05:00
|
|
|
|
(list
|
2023-06-23 11:28:34 -05:00
|
|
|
|
(list :speaker speaker :progress 0
|
2023-06-23 22:25:16 -05:00
|
|
|
|
:text (…:getf-lang keys)
|
|
|
|
|
:face (getf keys :face))))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
|
|
|
|
|
|
2023-07-10 22:42:22 -05:00
|
|
|
|
(defun move (speaker coords &key (delay .05))
|
|
|
|
|
(if (or (getf coords :Δx) (getf coords :Δy))
|
|
|
|
|
(list
|
|
|
|
|
(list :speaker speaker :relative-coords coords :delay delay))
|
|
|
|
|
(list
|
|
|
|
|
(list :speaker speaker :coords coords :delay delay))))
|
2023-06-22 19:08:14 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-20 20:04:13 -05:00
|
|
|
|
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
;;; Accessors
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
(defun dialogue-speaker (dialogue)
|
2023-06-21 20:31:29 -05:00
|
|
|
|
"Get the DIALOGUE-speaker’s corresponding identifying symbol.
|
|
|
|
|
Because they’re stored in strings. So we gotta, like, unstringify. Ya dig?"
|
2023-06-26 21:49:34 -05:00
|
|
|
|
(getf dialogue :speaker))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
;;; Dialogue logic
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
(defun appropriate-face (map speaker face)
|
2023-06-19 14:43:46 -05:00
|
|
|
|
"Return the face appropriate for the speaker.
|
|
|
|
|
If FACE is a string, used that.
|
|
|
|
|
If FACE is 'TALKING-FACE, then use their talking-face (if they have one).
|
|
|
|
|
If FACE is 'NORMAL-FACE, then use their normal-face (if they’ve got one).
|
|
|
|
|
If FACE is NIL… guess what that does. :^)"
|
2023-06-19 14:56:13 -05:00
|
|
|
|
(let ((talking-face (🌍:getf-entity-data map speaker :talking-face))
|
|
|
|
|
(normal-face (🌍:getf-entity-data map speaker :normal-face)))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
(cond ((and (eq face 'talking-face)
|
|
|
|
|
talking-face)
|
|
|
|
|
talking-face)
|
|
|
|
|
((and (eq face 'normal-face)
|
|
|
|
|
normal-face)
|
|
|
|
|
normal-face)
|
|
|
|
|
((stringp face)
|
|
|
|
|
face))))
|
|
|
|
|
|
|
|
|
|
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(defun update-speaking-face (map dialogue)
|
2023-06-22 19:08:14 -05:00
|
|
|
|
"Given a line (plist) of DIALOGUE, change speaker’s face to either their
|
|
|
|
|
talking-face or the face given by the DIALOGUE."
|
2023-06-26 21:49:34 -05:00
|
|
|
|
(let* ((speaker (getf dialogue :speaker))
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(new-face (appropriate-face map speaker (getf dialogue :face))))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
;; Replace the face, when appropriate.
|
|
|
|
|
(when new-face
|
2023-06-19 14:56:13 -05:00
|
|
|
|
(setf (🌍:getf-entity-data map speaker :face) new-face))))
|
2023-06-19 14:43:46 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-24 19:49:18 -05:00
|
|
|
|
(defun update-entity-data (map dialogue)
|
|
|
|
|
"Given a plist of DIALOGUE, update an arbitrary bit of data in the speaker's
|
|
|
|
|
data, using :SET and :TO of the DIALOGUE."
|
2023-06-26 21:49:34 -05:00
|
|
|
|
(let* ((speaker (getf dialogue :speaker))
|
2023-06-24 19:49:18 -05:00
|
|
|
|
(key (getf dialogue :set))
|
|
|
|
|
(data (getf dialogue :to)))
|
|
|
|
|
(when (and key data)
|
2023-06-27 11:54:52 -05:00
|
|
|
|
(setf (🌍:getf-entity-data map speaker key) data))))
|
|
|
|
|
;; (format *error-output* "[~A] ~A → ~A???~%" dialogue key data)
|
|
|
|
|
|
|
|
|
|
;; (format *error-output* "~A!!!!~%" (🌍:getf-entity-data map speaker :normal-face)))))
|
2023-06-24 19:49:18 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(defun progress-line-delivery (dialogue)
|
2023-06-22 19:08:14 -05:00
|
|
|
|
"Progress the delivery of a line (plist) of DIALOGUE. That is, increment the
|
2023-06-19 14:43:46 -05:00
|
|
|
|
“said character-count” :PROGRESS, which dictates the portion of the message that
|
|
|
|
|
should be printed on the screen at any given moment."
|
|
|
|
|
(let ((progress (getf dialogue :progress))
|
|
|
|
|
(text (getf dialogue :text)))
|
|
|
|
|
(when (and text
|
|
|
|
|
(< progress (length text)))
|
2023-06-20 20:04:13 -05:00
|
|
|
|
(incf (getf dialogue :progress) 1))))
|
2023-06-19 14:43:46 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-22 19:08:14 -05:00
|
|
|
|
(defun progress-movement (map dialogue)
|
|
|
|
|
"Move the entity by one tile in the targeted position — that is, the
|
|
|
|
|
coordinates listed in the DIALOGUE’s :COORDS property. … If applicable, ofc."
|
|
|
|
|
(let* ((speaker (dialogue-speaker dialogue))
|
|
|
|
|
(target-coords (getf dialogue :coords))
|
|
|
|
|
(speaker-coords (🌍:getf-entity-data map speaker :coords))
|
|
|
|
|
(finished-moving-p (if target-coords (…:plist= speaker-coords target-coords) 't)))
|
|
|
|
|
(when (not finished-moving-p)
|
|
|
|
|
(🌍:move-entity
|
|
|
|
|
map speaker
|
2023-06-27 19:30:01 -05:00
|
|
|
|
:Δx (cond ((< (getf target-coords :x) (getf speaker-coords :x)) -1)
|
|
|
|
|
((> (getf target-coords :x) (getf speaker-coords :x)) 1)
|
|
|
|
|
('t 0))
|
|
|
|
|
:Δy (cond ((< (getf target-coords :y) (getf speaker-coords :y)) -1)
|
|
|
|
|
((> (getf target-coords :y) (getf speaker-coords :y)) 1)
|
|
|
|
|
('t 0)))
|
2023-06-23 11:28:34 -05:00
|
|
|
|
(sleep (or (getf dialogue :delay) 0)))
|
2023-06-22 19:08:14 -05:00
|
|
|
|
finished-moving-p))
|
|
|
|
|
|
|
|
|
|
|
2023-07-10 22:42:22 -05:00
|
|
|
|
(defun ensure-dialogue-movement (map dialogue-list)
|
|
|
|
|
"Given a DIALOGUE-LIST, ensure that the first line of dialogue’s movement is
|
|
|
|
|
absolute rather than relative, if it contains any movement at all."
|
|
|
|
|
(let ((dialogue (car dialogue-list)))
|
|
|
|
|
(when (and (getf dialogue :relative-coords)
|
|
|
|
|
(not (getf dialogue :coords)))
|
|
|
|
|
(let ((speaker-coords (🌍:getf-entity-data map (dialogue-speaker dialogue) :coords))
|
|
|
|
|
(relative-coords (getf dialogue :relative-coords)))
|
|
|
|
|
(setf (getf (car dialogue-list) :coords)
|
|
|
|
|
(list :x (+ (getf speaker-coords :x) (or (getf relative-coords :Δx) 0))
|
|
|
|
|
:y (+ (getf speaker-coords :y) (or (getf relative-coords :Δy) 0))))))))
|
|
|
|
|
|
|
|
|
|
|
2023-06-22 19:08:14 -05:00
|
|
|
|
(defun finished-printing-p (dialogue)
|
|
|
|
|
"Whether or not a line of dialogue has been completely printed to the screen."
|
|
|
|
|
(or (not (getf dialogue :text))
|
|
|
|
|
(eq (length (getf dialogue :text))
|
|
|
|
|
(getf dialogue :progress))))
|
|
|
|
|
|
|
|
|
|
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(defun dialogue-state-update (map dialogue-list)
|
2023-06-21 20:31:29 -05:00
|
|
|
|
"The logic/input-processing helper function for DIALOGUE-STATE.
|
|
|
|
|
Progress through the lines of dialogue when the user hits ENTER, etc.
|
|
|
|
|
Returns the state for use with STATE-LOOP, pay attention!"
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(update-speaking-face map (car dialogue-list))
|
2023-06-24 19:49:18 -05:00
|
|
|
|
(update-entity-data map (car dialogue-list))
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(progress-line-delivery (car dialogue-list))
|
2023-07-10 22:42:22 -05:00
|
|
|
|
(ensure-dialogue-movement map dialogue-list)
|
2023-06-19 14:43:46 -05:00
|
|
|
|
;; Progress to the next line of dialogue as appropriate.
|
2023-06-22 19:08:14 -05:00
|
|
|
|
(let* ((dialogue (car dialogue-list))
|
|
|
|
|
(text (getf dialogue :text))
|
2023-07-13 02:37:24 -05:00
|
|
|
|
(did-press-enter-p (⌨:pressed-enter-p))
|
2023-06-22 19:08:14 -05:00
|
|
|
|
(did-finish-printing-p (finished-printing-p dialogue))
|
|
|
|
|
(did-finish-moving-p (progress-movement map dialogue)))
|
|
|
|
|
;; Only show the cursor when rendering text!
|
2023-06-23 22:25:16 -05:00
|
|
|
|
(if (or did-finish-moving-p (not did-finish-printing-p))
|
2023-06-22 19:08:14 -05:00
|
|
|
|
(✎:show-cursor)
|
|
|
|
|
(✎:hide-cursor))
|
|
|
|
|
(cond
|
|
|
|
|
;; When enter’s hit and most everything is done (rendering text, etc),
|
|
|
|
|
;; progress the dialogue.
|
|
|
|
|
((or (and did-press-enter-p did-finish-printing-p did-finish-moving-p)
|
|
|
|
|
(and (not text) did-finish-moving-p))
|
|
|
|
|
(if (cdr dialogue-list)
|
2023-07-11 22:14:02 -05:00
|
|
|
|
(list :parameters (list :dialogue (cdr dialogue-list) :map map))
|
2023-06-22 19:08:14 -05:00
|
|
|
|
(progn
|
|
|
|
|
(✎:hide-cursor)
|
2023-07-13 22:55:16 -05:00
|
|
|
|
(list :drop (1+ (or (getf dialogue :drop) 0))
|
2023-07-11 22:14:02 -05:00
|
|
|
|
:function (getf dialogue :function)
|
2023-07-13 22:55:16 -05:00
|
|
|
|
:parameters (if (member :parameters dialogue)
|
|
|
|
|
(getf dialogue :parameters)
|
2023-07-11 22:14:02 -05:00
|
|
|
|
(list :map map))))))
|
2023-06-22 19:08:14 -05:00
|
|
|
|
;; Allow interupting text-printing to end it!
|
|
|
|
|
((and did-press-enter-p (not did-finish-printing-p))
|
|
|
|
|
(setf (getf (car dialogue-list) :progress) (length text))
|
2023-07-11 22:14:02 -05:00
|
|
|
|
(list :parameters (list :dialogue dialogue-list :map map)))
|
2023-06-22 19:08:14 -05:00
|
|
|
|
;; If no input, keep steady!
|
2023-06-23 22:25:16 -05:00
|
|
|
|
('t
|
2023-07-11 22:14:02 -05:00
|
|
|
|
(list :parameters (list :dialogue dialogue-list :map map))))))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
;;; Dialogue drawing
|
|
|
|
|
;;; ———————————————————————————————————
|
2023-06-21 20:31:29 -05:00
|
|
|
|
(defun optimal-text-placement-horizontally (text coords &key (rightp nil) (width 72) (height 20))
|
|
|
|
|
"Given a horizontal direction (RIGHTP defined or nil) and a focal point COORDS,
|
|
|
|
|
return the parameters of a text-box that can optimally fit the given TEXT in the
|
|
|
|
|
direction specified relative to the focal point. If a legible position can’t be
|
|
|
|
|
found, just give up! Return nil.
|
2023-07-02 11:54:16 -05:00
|
|
|
|
Otherwise, return a list list with the coordinates, textbox width, and textbox
|
|
|
|
|
height — all parameters for use with RENDER-STRING & co."
|
2023-06-20 20:43:27 -05:00
|
|
|
|
(let* ((text-x-margin (if rightp
|
|
|
|
|
(+ (getf coords :x) 3)
|
|
|
|
|
0))
|
2023-06-20 22:00:36 -05:00
|
|
|
|
(text-width (…:at-most (floor (* width 3/5)) ;; Not _too_ wide!
|
|
|
|
|
(if rightp
|
|
|
|
|
(- width text-x-margin)
|
|
|
|
|
(- (getf coords :x) 3))))
|
2023-07-01 12:50:24 -05:00
|
|
|
|
(lines (ignore-errors (str:lines (…:linewrap-string text text-width))))
|
2023-06-20 20:43:27 -05:00
|
|
|
|
(text-height (length lines)))
|
2023-07-02 11:54:16 -05:00
|
|
|
|
(format *error-output* "HORIZ COORD: ~A HEIGHT: ~A WIDTH ~A LINES:~%~S~%" coords text-height text-width lines)
|
2023-06-20 22:00:36 -05:00
|
|
|
|
;; When this layout is valid…
|
|
|
|
|
(when (and lines
|
|
|
|
|
(>= height text-height) ;; If the text’ll fit on screen
|
|
|
|
|
(> text-width 10)) ;; If the text is wide enough to be legible
|
2023-07-02 11:54:16 -05:00
|
|
|
|
(let* ((y (…:at-least 0 (- (getf coords :y)
|
|
|
|
|
(if (eq text-height 1) ;; Align toward the speaker’s face
|
|
|
|
|
1 0)
|
|
|
|
|
(floor (/ text-height 2)))))
|
|
|
|
|
(x (if (and (not rightp)
|
|
|
|
|
(eq (length lines) 1))
|
|
|
|
|
(- text-width (length text))
|
|
|
|
|
text-x-margin))
|
|
|
|
|
(y-margin (if (> (+ y (length lines)) height) ;; How many lines are off-screen
|
|
|
|
|
(- (+ y (length lines)) height)
|
|
|
|
|
0)))
|
|
|
|
|
(list
|
|
|
|
|
;; Coords of text-box’es top-left corner
|
|
|
|
|
(list :x x :y (- y y-margin))
|
|
|
|
|
text-width ;; Width of text-box
|
|
|
|
|
text-height))))) ;; Height of text-box
|
2023-06-20 20:43:27 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-21 20:31:29 -05:00
|
|
|
|
(defun optimal-text-placement-vertically (text coords &key (downp nil) (width 72) (height 20))
|
|
|
|
|
"Given a vertical direction (DOWNP defined or nil) and a focal point COORDS,)
|
|
|
|
|
return the parameters of a text-box that can optimally fit the given TEXT in the
|
|
|
|
|
direction specified relative to the focal point. Return nil if no such placement
|
2023-07-02 11:54:16 -05:00
|
|
|
|
is found, otherwise return a list of the coordinates, textbox width, and textbox
|
|
|
|
|
height (for use as parameters with RENDER-STRING et al.)."
|
2023-06-20 20:43:27 -05:00
|
|
|
|
(let* ((text-y-margin (if downp
|
2023-06-24 22:29:56 -05:00
|
|
|
|
(+ (getf coords :y) 2)
|
2023-06-20 20:43:27 -05:00
|
|
|
|
(- (getf coords :y) 2)))
|
|
|
|
|
(text-height (if downp
|
|
|
|
|
(- height text-y-margin)
|
|
|
|
|
(- text-y-margin 1)))
|
|
|
|
|
(text-width (floor (* width 3/5))) ;; Too wide’s illegible! So ⅗-screen.
|
2023-07-01 12:50:24 -05:00
|
|
|
|
(lines (ignore-errors (str:lines (…:linewrap-string text text-width)))))
|
2023-07-02 11:54:16 -05:00
|
|
|
|
(format *error-output* "VERT HEIGHT: ~A WIDTH ~A LINES: ~A~%" text-height text-width lines)
|
2023-06-20 22:00:36 -05:00
|
|
|
|
;; When the text can be printed with this layout…
|
2023-06-20 20:43:27 -05:00
|
|
|
|
(when (and lines (>= text-height (length lines)))
|
2023-07-02 11:54:16 -05:00
|
|
|
|
(let* ((y (…:at-least
|
|
|
|
|
0
|
|
|
|
|
(if downp
|
|
|
|
|
text-y-margin
|
|
|
|
|
(- text-y-margin (length lines)))))
|
|
|
|
|
(x (…:at-least
|
|
|
|
|
0
|
|
|
|
|
(- (getf coords :x)
|
|
|
|
|
(if (eq (length lines) 1)
|
|
|
|
|
(floor (/ (length (car lines)) 2))
|
|
|
|
|
(floor (/ text-width 2))))))
|
|
|
|
|
(x-margin (if (> (+ x text-width) width)
|
|
|
|
|
(- (+ x text-width) width)
|
|
|
|
|
0)))
|
|
|
|
|
(list
|
|
|
|
|
;; Coords of text-box’es top-left corner
|
|
|
|
|
(list :x (- x x-margin) :y y)
|
|
|
|
|
text-width ;; Width of the text-box
|
|
|
|
|
(length lines)))))) ;; Height of text-box
|
2023-06-20 20:04:13 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(defun optimal-speech-layout (map dialogue &key (width 72) (height 20))
|
2023-06-21 20:31:29 -05:00
|
|
|
|
"Given a line of DIALOGUE and MAP data, return the ideal “text-box” for the
|
|
|
|
|
text. This tries to place the text on the screen without covering up anything
|
|
|
|
|
important, if possible.
|
|
|
|
|
The data returned is a list of the box’es top-left coordinate, max-column,
|
|
|
|
|
and max-row; for use with RENDER-STRING. Like so:
|
|
|
|
|
((:x X :y Y) MAX-COLUMN MAX-ROW)"
|
2023-06-20 20:04:13 -05:00
|
|
|
|
(let* ((speaker-id (dialogue-speaker dialogue))
|
2023-06-28 09:56:25 -05:00
|
|
|
|
(playerp (eq speaker-id '✿:player))
|
2023-06-26 20:25:02 -05:00
|
|
|
|
(leftp (not (🌍:getf-entity-data map speaker-id :facing-right)))
|
2023-06-20 20:04:13 -05:00
|
|
|
|
(text (getf dialogue :text))
|
|
|
|
|
(coords (🌍:world-coords->screen-coords (🌍:getf-entity-data map speaker-id :coords))))
|
2023-06-24 16:12:05 -05:00
|
|
|
|
;; Ideally, place text-box above/below (NPC/player); otherwise, place it behind speaker
|
|
|
|
|
(or (optimal-text-placement-vertically text coords :width width :height height
|
|
|
|
|
:downp playerp)
|
|
|
|
|
(optimal-text-placement-horizontally text coords :width width :height height
|
2023-06-20 22:00:36 -05:00
|
|
|
|
:rightp leftp)
|
2023-06-21 20:31:29 -05:00
|
|
|
|
;; … Worst-case scenario, just do whatever’ll fit :w:”
|
2023-06-23 11:28:34 -05:00
|
|
|
|
(optimal-text-placement-horizontally text coords :width width :height height
|
2023-06-24 16:12:05 -05:00
|
|
|
|
:rightp (not leftp))
|
|
|
|
|
(optimal-text-placement-vertically text coords :width width :height height
|
|
|
|
|
:downp (not playerp)))))
|
2023-06-20 20:04:13 -05:00
|
|
|
|
|
2023-07-10 22:42:22 -05:00
|
|
|
|
|
2023-07-02 11:54:16 -05:00
|
|
|
|
(defun ensure-dialogue-layout (map dialogue-list)
|
|
|
|
|
"Given a DIALOGUE-LIST, ensure that the FIRST line of dialogue has a :layout
|
|
|
|
|
property — that is, a property detailing the optimal width and coordinates for
|
|
|
|
|
its display."
|
|
|
|
|
(when (and (getf (car dialogue-list) :text)
|
|
|
|
|
(not (getf (car dialogue-list) :layout)))
|
|
|
|
|
(setf (getf (car dialogue-list) :layout)
|
|
|
|
|
(optimal-speech-layout map (car dialogue-list)))))
|
2023-06-20 20:04:13 -05:00
|
|
|
|
|
2023-07-02 11:54:16 -05:00
|
|
|
|
|
|
|
|
|
(defun render-dialogue-block (matrix dialogue)
|
2023-06-21 20:31:29 -05:00
|
|
|
|
"Render a bit of DIALOGUE to the MATRIX, in an intelligent fashion; that is,
|
|
|
|
|
make it pretty, dang it! >O<
|
|
|
|
|
☆:.。.o(≧▽≦)o.。.:☆"
|
2023-06-20 20:04:13 -05:00
|
|
|
|
(let* ((progress (getf dialogue :progress))
|
|
|
|
|
(text (getf dialogue :text))
|
2023-07-02 11:54:16 -05:00
|
|
|
|
(optimal-layout (getf dialogue :layout))
|
2023-07-01 12:50:24 -05:00
|
|
|
|
(coords (car optimal-layout)))
|
2023-06-20 20:04:13 -05:00
|
|
|
|
(when (and text optimal-layout)
|
2023-07-07 19:31:18 -05:00
|
|
|
|
;; (✎:render-fill-rectangle matrix #\space
|
|
|
|
|
;; (list :x (- (getf coords :x) 1)
|
|
|
|
|
;; :y (- (getf coords :y) 1)
|
|
|
|
|
;; (+ (second optimal-layout) 2) ;; Width
|
|
|
|
|
;; (+ (third optimal-layout) 1)) ;; Height
|
2023-07-01 12:50:24 -05:00
|
|
|
|
(✎:render-string
|
2023-06-20 20:04:13 -05:00
|
|
|
|
matrix text (first optimal-layout)
|
2023-07-01 12:50:24 -05:00
|
|
|
|
:width (second optimal-layout)
|
2023-06-20 20:04:13 -05:00
|
|
|
|
:char-count progress))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(defun dialogue-state-draw (matrix map dialogue-list)
|
2023-06-19 08:47:45 -05:00
|
|
|
|
"Draw the dialogue where appropriate.
|
|
|
|
|
Helper function for DIALOGUE-STATE."
|
2023-06-20 20:04:13 -05:00
|
|
|
|
(when (getf (car dialogue-list) :text)
|
|
|
|
|
(✎:show-cursor)
|
2023-07-02 11:54:16 -05:00
|
|
|
|
(ensure-dialogue-layout map dialogue-list)
|
|
|
|
|
(render-dialogue-block matrix (car dialogue-list))))
|
2023-06-19 08:47:45 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
;;; Dialogue loop
|
|
|
|
|
;;; ———————————————————————————————————
|
|
|
|
|
(defun dialogue-state (matrix &key dialogue map)
|
2023-06-21 20:31:29 -05:00
|
|
|
|
"Render a bit of DIALOGUE to the screen, using :FLORA-SEARCH-AURORA.OVERWORLD
|
2023-06-19 08:47:45 -05:00
|
|
|
|
entities as the speakers. Dialogue should be in the format:
|
|
|
|
|
((:text \"Hello, papa!\"
|
|
|
|
|
:speaker \"son\" ;; The entity’s ID (if applicable)
|
|
|
|
|
:face \"owo\") ;; If you want their face to change
|
|
|
|
|
(:face \"=w=\" :speaker 'son) ;; change their face back when done talking
|
2023-06-21 20:31:29 -05:00
|
|
|
|
(:text \"My dearest son, it’s been so long~!\"
|
2023-06-19 08:47:45 -05:00
|
|
|
|
:speaker \"papa\"
|
|
|
|
|
...))
|
|
|
|
|
A state-function for use with STATE-LOOP."
|
2023-06-20 20:04:13 -05:00
|
|
|
|
(sleep .05)
|
|
|
|
|
(dialogue-state-draw matrix map dialogue)
|
2023-06-19 14:43:46 -05:00
|
|
|
|
(dialogue-state-update map dialogue))
|
2023-06-20 22:00:36 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-23 13:29:09 -05:00
|
|
|
|
(defun make-dialogue-state (map dialogue-list)
|
|
|
|
|
"Return a state-function for a section of dialogue, for use with STATE-LOOP."
|
2023-07-11 22:14:02 -05:00
|
|
|
|
(list :function
|
|
|
|
|
(lambda (matrix &key (map map) (dialogue dialogue-list))
|
|
|
|
|
(🌍:overworld-state-draw matrix map)
|
|
|
|
|
(dialogue-state matrix :map map :dialogue dialogue))))
|
2023-06-23 13:29:09 -05:00
|
|
|
|
|
|
|
|
|
|
2023-06-20 22:00:36 -05:00
|
|
|
|
;; Split a banana in two, bisection-fruit,
|
|
|
|
|
;; Yummy-yummy, toot-toot~ 🎵
|