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).
(defpackage :flora-search-aurora.dialogue
(:nicknames :fsa.d :dialogue)
(:nicknames :fsa.dia :dialogue :💬)
(:use :cl
: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)
@ -30,38 +31,46 @@
;;; ———————————————————————————————————
;;; Dialogue-generation DSL (sorta)
;;; ———————————————————————————————————
(defun dialogue (&rest dialogue-tree)
(defun start-dialogue (&rest dialogue-tree)
(reduce (lambda (a b) (append a b))
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)
(list
(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
;;; ———————————————————————————————————
(defun pressed-enter-p ()
"Whether or not the enter/return key has been pressed recently."
(and (listen)
(eq (getf (normalize-char-plist (read-char-plist)) :char)
#\return)))
(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))
(normal-face (getf-entity-data map speaker :normal-face)))
(cond ((and (eq face 'talking-face)
@ -74,27 +83,43 @@
face))))
(defun dialogue-state-update (dialogue-list map)
"The logic/input-processing helper function for DIALOGUE-STATE."
(let* ((speaker (intern (string-upcase (getf (car dialogue-list) :speaker))))
(new-face (appropriate-face map speaker
(getf (car dialogue-list) :face))))
(defun update-speaking-face (map dialogue)
"Given a line (plist) of dialogue, change speakers face to either their
talking-face or the face given by the dialogue."
(let* ((speaker (intern (string-upcase (getf dialogue :speaker))))
(new-face (appropriate-face map speaker (getf dialogue :face))))
;; Replace the face, when appropriate.
(when new-face
(setf (getf-entity-data map speaker :face) new-face)))
;; Progress the dialogue as appropriate.
(setf (getf-entity-data map speaker :face) new-face))))
(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)))
(cond ((or (pressed-enter-p)
(not text))
(if (cdr dialogue-list)
(list :dialogue (cdr dialogue-list) :map map)
(progn
(:hide-cursor)
(values nil
(list :map map))))
(list :map map)))))
((cdr dialogue-list)
(list :dialogue dialogue-list :map map))
('t
(values nil
(list :map map))))))
(list :dialogue dialogue-list :map map)))))
@ -104,9 +129,11 @@
(defun dialogue-state-draw (matrix dialogue-list)
"Draw the dialogue where appropriate.
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
(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."
(sleep .02)
(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.
(defpackage :flora-search-aurora.display
(:nicknames :fsa.d :display :)
(:use :cl)
(:export #:make-screen-matrix #:print-screen-matrix #:matrix-delta
#:clear-screen))
#:hide-cursor #:show-cursor #:clear-screen))
(in-package :flora-search-aurora.display)
@ -51,7 +52,7 @@ The body has access to 4 variables:
(defun matrix-delta (a b)
"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))))
(do-for-cell a
(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
(when (characterp cell)
(move-cursor (+ i 1) (+ j 1))
(write-char cell)))
(destructuring-bind (i j) (array-dimensions matrix)
(move-cursor i j)))
(write-char cell))))
(defun make-screen-matrix ()
@ -81,6 +80,14 @@ that change between a→b (favouring those in b) — all others are nil."
;;; ———————————————————————————————————
;;; 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*))
"Moves cursor to desired position.
Borrowed from https://github.com/gorozhin/chlorophyll/

View File

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

View File

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

24
ui.lisp
View File

@ -18,9 +18,10 @@
;;;; Let's get to it, we're on a deadline!
(defpackage :flora-search-aurora.ui
(:nicknames :fsa.u :ui)
(:use :cl :flora-search-aurora.display :flora-search-aurora.input :assoc-utils)
(:export #:menu-state
#:render-line
#:render-line #:render-string #:render-string-partially
:label :selection :selected))
(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.
Will line-break or truncate as appropriate and necessary to not exceed the
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))
(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)))
(row 0))
(loop while (and (<= (+ y row) max-row)
substrings)
do
do (cond ((< row max-write-row)
(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)))
matrix)