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:
parent
95442b39db
commit
b8faba4c6a
|
@ -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 they’ve 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 speaker’s 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)
|
||||
(values nil
|
||||
(list :map map))))
|
||||
(list :dialogue (cdr dialogue-list) :map map)
|
||||
(progn
|
||||
(✎:hide-cursor)
|
||||
(values nil
|
||||
(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))
|
||||
|
|
17
display.lisp
17
display.lisp
|
@ -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 a→b (favouring those in b) — all others are nil."
|
||||
that change between A→B (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/
|
||||
|
|
|
@ -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,10 +125,10 @@ 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)
|
||||
(clear-screen)
|
||||
(state-loop (list (make-main-menu-state)))))
|
||||
(cl-charms:enable-raw-input :interpret-control-characters 't)
|
||||
(hide-cursor)
|
||||
(clear-screen)
|
||||
(state-loop (list (make-main-menu-state)))))
|
||||
|
||||
|
||||
(main) ;; — Knock-knock
|
||||
|
|
|
@ -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>
|
||||
|
|
26
ui.lisp
26
ui.lisp
|
@ -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
|
||||
(render-line matrix (pop substrings)
|
||||
x (+ y row))
|
||||
do (cond ((< row max-write-row)
|
||||
(render-line matrix (pop substrings)
|
||||
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)
|
||||
|
||||
|
|
Ŝarĝante…
Reference in New Issue