When printing dialogue, show chars one-at-a-time

Y’know, RPG-style! ‘Cause we’re cool kids! c:<
… and this is a real RPG! This is legit!
This is legit! … this is legit… =,w,=
It _will_ be legit!
This commit is contained in:
Jaidyn Ann 2023-06-19 14:43:46 -05:00
parent 95442b39db
commit b8faba4c6a
5 changed files with 104 additions and 51 deletions

View File

@ -18,10 +18,11 @@
;;;; the primary gameplay, the RPG-ish-ish bits). ;;;; the primary gameplay, the RPG-ish-ish bits).
(defpackage :flora-search-aurora.dialogue (defpackage :flora-search-aurora.dialogue
(:nicknames :fsa.d :dialogue) (:nicknames :fsa.dia :dialogue :💬)
(:use :cl (:use :cl
:flora-search-aurora.overworld :flora-search-aurora.ui :flora-search-aurora.input) :flora-search-aurora.overworld :flora-search-aurora.ui :flora-search-aurora.input)
(:export #:dialogue-state #:say)) (:export #:dialogue-state
#:start-dialogue #:face #:say #:mumble))
(in-package :flora-search-aurora.dialogue) (in-package :flora-search-aurora.dialogue)
@ -30,38 +31,46 @@
;;; ——————————————————————————————————— ;;; ———————————————————————————————————
;;; Dialogue-generation DSL (sorta) ;;; Dialogue-generation DSL (sorta)
;;; ——————————————————————————————————— ;;; ———————————————————————————————————
(defun dialogue (&rest dialogue-tree) (defun start-dialogue (&rest dialogue-tree)
(reduce (lambda (a b) (append a b)) (reduce (lambda (a b) (append a b))
dialogue-tree)) dialogue-tree))
(defun say (speaker text)
(list
(list :speaker speaker :text text :face 'talking-face)
(car (face speaker 'normal-face))))
(defun mumble (speaker text)
(list
(list :speaker speaker :text text)))
(defun face (speaker face) (defun face (speaker face)
(list (list
(list :speaker speaker :face face))) (list :speaker speaker :face face)))
(defun say (speaker text)
(list
(list :speaker speaker :text text :face 'talking-face :progress 0)
(car (face speaker 'normal-face))))
(defun mumble (speaker text)
(list
(list :speaker speaker :text text :progress 0)))
;;; ——————————————————————————————————— ;;; ———————————————————————————————————
;;; Dialogue logic ;;; Dialogue logic
;;; ——————————————————————————————————— ;;; ———————————————————————————————————
(defun pressed-enter-p () (defun pressed-enter-p ()
"Whether or not the enter/return key has been pressed recently."
(and (listen) (and (listen)
(eq (getf (normalize-char-plist (read-char-plist)) :char) (eq (getf (normalize-char-plist (read-char-plist)) :char)
#\return))) #\return)))
(defun appropriate-face (map speaker face) (defun appropriate-face (map speaker face)
"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 theyve got one).
If FACE is NIL guess what that does. :^)"
(let ((talking-face (getf-entity-data map speaker :talking-face)) (let ((talking-face (getf-entity-data map speaker :talking-face))
(normal-face (getf-entity-data map speaker :normal-face))) (normal-face (getf-entity-data map speaker :normal-face)))
(cond ((and (eq face 'talking-face) (cond ((and (eq face 'talking-face)
@ -74,27 +83,43 @@
face)))) face))))
(defun dialogue-state-update (dialogue-list map) (defun update-speaking-face (map dialogue)
"The logic/input-processing helper function for DIALOGUE-STATE." "Given a line (plist) of dialogue, change speakers face to either their
(let* ((speaker (intern (string-upcase (getf (car dialogue-list) :speaker)))) talking-face or the face given by the dialogue."
(new-face (appropriate-face map speaker (let* ((speaker (intern (string-upcase (getf dialogue :speaker))))
(getf (car dialogue-list) :face)))) (new-face (appropriate-face map speaker (getf dialogue :face))))
;; Replace the face, when appropriate. ;; Replace the face, when appropriate.
(when new-face (when new-face
(setf (getf-entity-data map speaker :face) new-face))) (setf (getf-entity-data map speaker :face) new-face))))
;; Progress the dialogue as appropriate.
(defun progress-line-delivery (dialogue)
"Progress the delivery of a line (plist) of dialogue. That is, increment the
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)))
(incf (getf dialogue :progress)))))
(defun dialogue-state-update (map dialogue-list)
"The logic/input-processing helper function for DIALOGUE-STATE."
(update-speaking-face map (car dialogue-list))
(progress-line-delivery (car dialogue-list))
;; Progress to the next line of dialogue as appropriate.
(let ((text (getf (car dialogue-list) :text))) (let ((text (getf (car dialogue-list) :text)))
(cond ((or (pressed-enter-p) (cond ((or (pressed-enter-p)
(not text)) (not text))
(if (cdr dialogue-list) (if (cdr dialogue-list)
(list :dialogue (cdr dialogue-list) :map map) (list :dialogue (cdr dialogue-list) :map map)
(values nil (progn
(list :map map)))) (:hide-cursor)
(values nil
(list :map map)))))
((cdr dialogue-list) ((cdr dialogue-list)
(list :dialogue dialogue-list :map map)) (list :dialogue dialogue-list :map map)))))
('t
(values nil
(list :map map))))))
@ -104,9 +129,11 @@
(defun dialogue-state-draw (matrix dialogue-list) (defun dialogue-state-draw (matrix dialogue-list)
"Draw the dialogue where appropriate. "Draw the dialogue where appropriate.
Helper function for DIALOGUE-STATE." Helper function for DIALOGUE-STATE."
(let ((text (getf (car dialogue-list) :text))) (let ((text (getf (car dialogue-list) :text))
(progress (getf (car dialogue-list) :progress)))
(when text (when text
(render-line matrix text 0 0)))) (:show-cursor)
(render-string-partially matrix text 0 0 :char-count progress))))
@ -126,4 +153,4 @@ entities as the speakers. Dialogue should be in the format:
A state-function for use with STATE-LOOP." A state-function for use with STATE-LOOP."
(sleep .02) (sleep .02)
(dialogue-state-draw matrix dialogue) (dialogue-state-draw matrix dialogue)
(dialogue-state-update dialogue map)) (dialogue-state-update map dialogue))

View File

@ -17,9 +17,10 @@
;;;; All display-related curses go here. ;;;; All display-related curses go here.
(defpackage :flora-search-aurora.display (defpackage :flora-search-aurora.display
(:nicknames :fsa.d :display :)
(:use :cl) (:use :cl)
(:export #:make-screen-matrix #:print-screen-matrix #:matrix-delta (:export #:make-screen-matrix #:print-screen-matrix #:matrix-delta
#:clear-screen)) #:hide-cursor #:show-cursor #:clear-screen))
(in-package :flora-search-aurora.display) (in-package :flora-search-aurora.display)
@ -51,7 +52,7 @@ The body has access to 4 variables:
(defun matrix-delta (a b) (defun matrix-delta (a b)
"Given two 2D matrices, return a matrix containing only the cells "Given two 2D matrices, return a matrix containing only the cells
that change between ab (favouring those in b) all others are nil." that change between AB (favouring those in B) all others are nil."
(let ((delta (make-array (array-dimensions a)))) (let ((delta (make-array (array-dimensions a))))
(do-for-cell a (do-for-cell a
(when (not (eq cell (when (not (eq cell
@ -66,9 +67,7 @@ that change between a→b (favouring those in b) — all others are nil."
(do-for-cell matrix (do-for-cell matrix
(when (characterp cell) (when (characterp cell)
(move-cursor (+ i 1) (+ j 1)) (move-cursor (+ i 1) (+ j 1))
(write-char cell))) (write-char cell))))
(destructuring-bind (i j) (array-dimensions matrix)
(move-cursor i j)))
(defun make-screen-matrix () (defun make-screen-matrix ()
@ -81,6 +80,14 @@ that change between a→b (favouring those in b) — all others are nil."
;;; ——————————————————————————————————— ;;; ———————————————————————————————————
;;; Misc. utils ;;; Misc. utils
;;; ——————————————————————————————————— ;;; ———————————————————————————————————
(defun hide-cursor ()
(cl-charms/low-level:curs-set 0))
(defun show-cursor ()
(cl-charms/low-level:curs-set 1))
(defun move-cursor (row column &key (stream *standard-output*)) (defun move-cursor (row column &key (stream *standard-output*))
"Moves cursor to desired position. "Moves cursor to desired position.
Borrowed from https://github.com/gorozhin/chlorophyll/ Borrowed from https://github.com/gorozhin/chlorophyll/

View File

@ -28,6 +28,7 @@
(load "dialogue.lisp") (load "dialogue.lisp")
(defpackage :flora-search-aurora (defpackage :flora-search-aurora
(:nicknames :fsa :)
(:export #:main) (:export #:main)
(:use :cl (:use :cl
:flora-search-aurora.input :flora-search-aurora.display :flora-search-aurora.input :flora-search-aurora.display
@ -41,9 +42,9 @@
(defun literary-girl-dialogue (map) (defun literary-girl-dialogue (map)
(lambda (matrix &key (map map) (lambda (matrix &key (map map)
(dialogue (dialogue::dialogue (dialogue (💬:start-dialogue
(dialogue::say "literary-girl" "Oh, hello.") (💬:say "literary-girl" "Blah blah, testing. A multi-lined one. For real! jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj akls djlaks jdlaksj dlakjsd")
(dialogue::say "player" "What, no quip?")))) (💬:say "player" "ktp ktp jes jes?"))))
(overworld-state-draw matrix map) (overworld-state-draw matrix map)
(dialogue-state matrix :map map :dialogue dialogue))) (dialogue-state matrix :map map :dialogue dialogue)))
@ -56,7 +57,7 @@ Given a list of state-functions, STATES, it will execute the first function.
Each state-function must take at least a single parameter, a matrix of characters. A state-function Each state-function must take at least a single parameter, a matrix of characters. A state-function
should edit this matrix in-place, replacing its elements with characters that will later be printed should edit this matrix in-place, replacing its elements with characters that will later be printed
to the terminal. to the terminal.
What the state-function returns is pretty important: What the state-function returns is pretty important, having different repercussions:
* NIL The function is removed from STATES, and so the next function in STATES will start * NIL The function is removed from STATES, and so the next function in STATES will start
getting executed instead. getting executed instead.
* NIL; List — The function is popped off STATES and the list is used as the new parameters for * NIL; List — The function is popped off STATES and the list is used as the new parameters for
@ -124,10 +125,10 @@ with STATE-LOOP."
(defun main () (defun main ()
"A pathetic fascimile of a main loop. What does it do? WHAST DOES TI DODOO?" "A pathetic fascimile of a main loop. What does it do? WHAST DOES TI DODOO?"
(cl-charms:with-curses () (cl-charms:with-curses ()
(cl-charms/low-level:curs-set 0) ;; Hide the terminal cursor (cl-charms:enable-raw-input :interpret-control-characters 't)
(cl-charms:enable-raw-input :interpret-control-characters 't) (hide-cursor)
(clear-screen) (clear-screen)
(state-loop (list (make-main-menu-state))))) (state-loop (list (make-main-menu-state)))))
(main) ;; — Knock-knock (main) ;; — Knock-knock

View File

@ -98,18 +98,20 @@
<objectgroup id="7" name="Entities"> <objectgroup id="7" name="Entities">
<object id="2" name="Player" type="Entity" x="103" y="368"> <object id="2" name="Player" type="Entity" x="103" y="368">
<properties> <properties>
<property name="face" value="=w="/>
<property name="facing_right" type="bool" value="true"/> <property name="facing_right" type="bool" value="true"/>
<property name="id" value="player"/> <property name="id" value="player"/>
<property name="normal_face" value="^_^"/>
<property name="talking_face" value="^o^"/>
</properties> </properties>
<point/> <point/>
</object> </object>
<object id="4" name="Literary girl" type="Entity" x="317.604" y="366.606"> <object id="4" name="Literary girl" type="Entity" x="317.604" y="366.606">
<properties> <properties>
<property name="face" value="owo"/>
<property name="facing_right" type="bool" value="true"/> <property name="facing_right" type="bool" value="true"/>
<property name="id" value="literary-girl"/> <property name="id" value="literary-girl"/>
<property name="interact" value="literary-girl-dialogue"/> <property name="interact" value="literary-girl-dialogue"/>
<property name="normal_face" value="=_="/>
<property name="talking_face" value="=o="/>
</properties> </properties>
<point/> <point/>
</object> </object>

26
ui.lisp
View File

@ -18,9 +18,10 @@
;;;; Let's get to it, we're on a deadline! ;;;; Let's get to it, we're on a deadline!
(defpackage :flora-search-aurora.ui (defpackage :flora-search-aurora.ui
(:nicknames :fsa.u :ui)
(:use :cl :flora-search-aurora.display :flora-search-aurora.input :assoc-utils) (:use :cl :flora-search-aurora.display :flora-search-aurora.input :assoc-utils)
(:export #:menu-state (:export #:menu-state
#:render-line #:render-line #:render-string #:render-string-partially
:label :selection :selected)) :label :selection :selected))
(in-package :flora-search-aurora.ui) (in-package :flora-search-aurora.ui)
@ -127,16 +128,31 @@ The item list should be an alist of the following format:
"Render the given string to the matrix of characters, character-by-character. "Render the given string to the matrix of characters, character-by-character.
Will line-break or truncate as appropriate and necessary to not exceed the Will line-break or truncate as appropriate and necessary to not exceed the
positional arguments nor the dimensions of the matrix." positional arguments nor the dimensions of the matrix."
(render-string-partially matrix text x y :max-column max-column :max-row max-row
:char-count (length text)))
(defun render-string-partially (matrix text x y &key (char-count 0) (max-column 72) (max-row 20))
"Partially render the given string to a matrix of characters. Will render only
a portion of the string, dictated by the CHAR-COUNT.
See the similar RENDER-STRING function."
(let* ((dimensions (array-dimensions matrix)) (let* ((dimensions (array-dimensions matrix))
(max-column (at-most (cadr dimensions) max-column)) (max-column (at-most (cadr dimensions) max-column))
(max-row (at-most (car dimensions) max-row)) (max-write-row (at-most (at-most (car dimensions) max-row)
(floor (/ char-count max-column))))
(max-column-at-max-write-row (- char-count (* max-write-row max-column)))
(substrings (split-string-by-length text (- max-column x))) (substrings (split-string-by-length text (- max-column x)))
(row 0)) (row 0))
(loop while (and (<= (+ y row) max-row) (loop while (and (<= (+ y row) max-row)
substrings) substrings)
do do (cond ((< row max-write-row)
(render-line matrix (pop substrings) (render-line matrix (pop substrings)
x (+ y row)) x (+ y row)))
((eq row max-write-row)
(render-line matrix (subseq (pop substrings) 0 max-column-at-max-write-row)
x (+ y row)))
('t
(pop substrings)))
(incf row))) (incf row)))
matrix) matrix)