luv

Workshop wiki

gnome.lisp

luvcraft/agent/gnome.lisp

system luvcraft/agent · 140 definitions · on GitHub

in-package#:luvcraft.agent

The gnome: an agent with a body (#5JDNKF), not a block (#V9DHV5).

A gnome occupies a discrete world cell but is not a block. The cell is semantic state -- where this agent stands -- while its animated, camera-facing figure is an independently rendered scene object. Focusing it is how you talk to it: the view leans in the way it does for a wall, the keys become a prompt line at the bottom of the screen, and RET asks. The gnome then thinks, and its thinking shows above its head as a bubble: a matte rounded cassette standing in the world, drawn through McCLIM the way the Telegram panel is (a world widget, in the scene, with relief). Each tool call is the next cassette, and pushes the ones before it upward. When the gnome talks -- the SAY command, which is a tool like any other -- it is not a cassette but a title at the bottom of the view, the way an NPC line is subtitled.

defparameter*gnome-name*

gnome

The lengths below, like everything in the shader that draws him, are in figure units: the frame the gnome is laid out in, where his hat reaches GNOME-HAT-HEIGHT whatever his stature. gnome-stature is the one place the world's scale enters, so it multiplies all of them.

defparameter*gnome-body-centre*0.85

The height above his feet of the centre of the gnome's bounding sphere.

The shader's marcher agrees with this number; moving one moves both.

defparameter*gnome-body-margin*0.05

Slack added to the computed bounding radius, for the rim light and for the fillets that push a weld a little outside the parts it joins.

defparameter*gnome-face-height*0.94

The height of the gnome's eyes above his feet.

What looks at a gnome is aimed here. It is not the top of him: aiming a conversation at the top of a gnome frames his hat.

defungnome-stature

How many world cells one figure unit is, from the gnome-stature knob.

defungnome-figure-radius

The radius, in figure units, of a sphere about *GNOME-BODY-CENTRE* that holds every part of the gnome the knobs are currently asking for.

The billboard is a conservative proxy and nothing outside it is drawn, so this has to be recomputed rather than assumed: a hat raised past a fixed radius does not overflow, it comes out sliced flat across the top. Each term is the extreme of one part -- how far out it reaches and how high it sits -- and the sphere is the largest of them.

macrolet
knob
name
`
float
symbol-value',name
1.0
let
flet
reach
radialheight
let
sqrt
+
*radialradial
*riserise
+*gnome-body-margin*
max
reach
abshat-lean
hat-height

the tip, wherever it leans

reachhat-flare1.04

the cone's skirt

reachbrim1.06

the brim's edge

reach
+
*head0.90
nose
0.855

the end of the nose

reachhead0.90

the back of the head

reachbeard-width0.605

the whiskers

reach0.115
-beard-length0.095

the beard's point

reachbelly0.30

the hem at its widest

reach0.0
-0.30belly

the hem at the ground

reachshoulder0.80

the shoulders

reach
+mitten0.09
0.50

the mittens

reach0.350.0

the toes

defparameter+clear-ink+
compose-in
make-rgb-color000
make-opacity0.0

A pane background that paints nothing: colour with zero opacity, which the GPU medium spells as a fully transparent fill.

defclassembodied-agent
session:initarg:session:readergnome-session

The cell is the discrete public place; position and VELOCITY are the physical realization moving between such places. Keeping both makes the intentional and simulation authorities explicit (#YPGXKI).

x:initarg:x:accessorgnome-x
y:initarg:y:accessorgnome-y
z:initarg:z:accessorgnome-z
position:initarg:position:accessorembodied-agent-position
velocity:initform:accessorembodied-agent-velocity
facing-yaw:initarg:facing-yaw:initform0d0:accessorembodied-agent-facing-yaw:documentation"Retained body heading in radians, with +z as north."
grounded-p:initformt:accessorembodied-agent-grounded-p
movement-action:initformnil:accessorembodied-agent-movement-action
movement-accumulator:initform0d0:accessorembodied-agent-movement-accumulator
pending-approval:initformnil:accessorembodied-agent-pending-approval:documentation"The one possible-world tool effect awaiting the player."
agent:initformnil:accessorgnome-agent
agent-lock:initform
sb-thread:make-mutex:name"embodied agent provider"
:readerembodied-agent-agent-lock
agent-ready:initform
sb-thread:make-waitqueue:name"embodied agent provider ready"
:readerembodied-agent-agent-ready
agent-opening-owner:initformnil:accessorembodied-agent-agent-opening-owner
agent-opening-thread:initformnil:accessorembodied-agent-agent-opening-thread
pending-agent-prompts:initform'
:accessorembodied-agent-pending-agent-prompts
agent-released-p:initformnil:accessorembodied-agent-agent-released-p
body:initformnil:accessorgnome-body
bubbles:initform'
:accessorgnome-bubbles:documentation"Bubble overlays, newest first."
dialogue:initformnil:accessorgnome-dialogue:documentation"The HUD line: the prompt being typed, or what was said."
draft:initform"":accessorgnome-draft
said:initformnil:accessorgnome-said:documentation"The gnome's last line, or NIL."
said-at:initform0:accessorgnome-said-at
observer:initformnil:accessorgnome-observer
notes:initform
sb-concurrency:make-mailbox:name"gnome notes"
:readergnome-notes:documentation"Transcript events waiting for the canvas thread."
turn-finished-at:initformnil:accessorgnome-turn-finished-at
:documentation

One embodied agent at a discrete cell: its thread, figure, and voice.

defclassgnome
:documentation

A small SDF-rendered garden gnome carrying a world-agent.

defgenericembodied-agent-body-height
agent
:documentation

agent's visible height above its occupied cell, in blocks.

defgenericembodied-agent-head-position
agent&optionallift
:documentation

The world point above agent, optionally lift blocks higher.

defgenericembodied-agent-face-position
agent&optionallift
:documentation

The world point agent looks from, optionally lift blocks higher.

defgenericembodied-agent-ray-distance
agentorigindirectionmax-distance
:documentation

Where a ray enters agent's conservative body, or NIL.

defgenericembodied-agent-audience-distance
agent
:documentation

How far away the focused camera stands from agent.

defgenericensure-embodied-agent-agent
agent
:documentation

Return agent's world-agent, opening it synchronously when necessary.

This setup operation may perform network I/O and must not run on the canvas thread. Gnome interaction uses Gnome-Ask's asynchronous first-contact path.

defgenericopen-embodied-agent-agent
agent
:documentation

Perform the blocking provider setup for agent and return a fresh world-agent.

Methods construct candidates only. The common opening protocol installs one winner, attaches its observer, and releases candidates that lose to teardown.

defgenericensure-embodied-agent-body
agent
:documentation

Return agent's render body, creating it when necessary.

defgenericembodied-agent-body-sphere
agent
:documentation

Return agent's conservative SDF proxy as world-space X, Y, Z, RADIUS.

The visible conversational figures are full block-world bodies. Their behavior is destinational, but contact is the same open body protocol the player and critters use.

defparameter*embodied-agent-walk-speed*2.35d0
defparameter*embodied-agent-gravity*24d0
defmethodluvcraft:body-position
embodied-agent-positionagent
defmethodluvcraft:body-velocity
embodied-agent-velocityagent
defmethodcommand-subject-bearing
session
declare
ignoresession
values
embodied-agent-facing-yawagent
0d0
defmethodluvcraft:body-half-width
declare
ignoreagent
0.28d0
defmethodluvcraft:body-grounded-p
embodied-agent-grounded-pagent
defmethod
grounded-p
setf
embodied-agent-grounded-pagent
grounded-p
defmethodluvcraft:body-movement-action
embodied-agent-movement-actionagent
defmethod
setf
embodied-agent-movement-actionagent
action
defmethodprint-object
print-unreadable-object
gnomestream:typet
formatstream"~D ~D ~D"
gnome-xgnome
gnome-ygnome
gnome-zgnome
defvar*agents*'

The embodied agents in live sessions.

These are semantic individuals, not members of the world's critter population and not readings of its block field. Releasing their session overlays removes them from this registry.

defunagents-in-session
session

Return session's embodied agents, newest first.

remove-if-not
lambda
agent
eqsession
gnome-sessionagent
*agents*
defunagent-at
sessionxyz

The embodied agent occupying X,Y,Z in session, or NIL.

find-if
lambda
agent
and
eqsession
gnome-sessionagent
=x
gnome-xagent
=y
gnome-yagent
=z
gnome-zagent
*agents*
defunattach-embodied-agent
classsessionxyz&key
facing-yaw0d0

Make one class at X,Y,Z and attach its semantic and render overlays.

let
agent
make-instanceclass:sessionsession:xx:yy:zz:facing-yaw
coercefacing-yaw'double-float
:position
luvcraft::make-vec3
+x0.5d0
coercey'double-float
+z0.5d0
pushagent*agents*
agent
defunfind-agent
sessionxyz&optional
make-pt

Find the embodied agent occupying integer cell X,Y,Z in session.

When make-p is true, create and attach one if the cell is unoccupied.

or
agent-atsessionxyz
defungnome-body-height

The height of the hat's tip above his feet, in figure units.

What is above a gnome -- his speech bubbles -- is measured from here, so a taller hat pushes them up rather than growing through them.

--------------------------------------------------------------------- The embodied agent as an overlay and a focus

defmethodluvcraft:luvcraft-focus-score

Targetable by TAB when the view ray enters the agent before terrain.

let
camera
luvcraft:luvcraft-session-camerasession
multiple-value-bind
rightupforward
declare
ignorerightup
let
whendistance
let
unless
andhit
<
luvcraft::block-ray-hit-distancehit
distance
distance
defmethodembodied-agent-ray-distance
origindirectionmax-distance

Return where a ray enters gnome's upright body, or NIL when it misses.

let*
near0d0
far
coercemax-distance'double-float
stature
coerce'double-float
half-width
*0.36d0stature
flet
slab
origindirectionminimummaximum
let
origin
coerceorigin'double-float
direction
coercedirection'double-float
if
zeropdirection
<=minimumoriginmaximum
let
entering
/
-minimumorigin
direction
leaving
/
-maximumorigin
direction
when
>enteringleaving
rotatefenteringleaving
setfnear
maxnearentering
far
minfarleaving
<=nearfar
let
and
slab
luvcraft::vec3-xorigin
luvcraft::vec3-xdirection
-center-xhalf-width
+center-xhalf-width
slab
luvcraft::vec3-yorigin
luvcraft::vec3-ydirection
slab
luvcraft::vec3-zorigin
luvcraft::vec3-zdirection
-center-zhalf-width
+center-zhalf-width
near
defungnome-ray-distance
gnomeorigindirectionmax-distance

Compatibility name for embodied-agent-ray-distance.

embodied-agent-ray-distancegnomeorigindirectionmax-distance
defparameter*gnome-audience-distance*3.2"How far from the gnome the focused camera stands, in blocks."
defmethodluvcraft:luvcraft-focus-camera-pose

Step back to a conversational distance, look the gnome in the face, and leave room above its hat for the bubbles.

or
alexandria:when-let
approval
embodied-agent-pending-approvalgnome
let*
camera
luvcraft:luvcraft-session-camerasession
eye
luvcraft:camera-positioncamera
dx
-
luvcraft::vec3-xface
luvcraft::vec3-xeye
dz
-
luvcraft::vec3-zface
luvcraft::vec3-zeye
flat
max
sqrt
+
*dxdx
*dzdz
0.001
ux
/dxflat
uz
/dzflat
position
luvcraft::make-vec3
-
luvcraft::vec3-xface
+
luvcraft::vec3-yface
0.10
-
luvcraft::vec3-zface
tdx
-
luvcraft::vec3-xtarget
luvcraft::vec3-xposition
tdy
-
luvcraft::vec3-ytarget
luvcraft::vec3-yposition
tdz
-
luvcraft::vec3-ztarget
luvcraft::vec3-zposition
tflat
max
sqrt
+
*tdxtdx
*tdztdz
0.001
luvcraft::make-camera-poseposition
atantdxtdz
atantdytflat
luvcraft::+luvcraft-camera-focused-vertical-field-of-view+
defungnome-ask
gnometext

Start text now, or queue it behind one asynchronous provider opening.

In particular, first contact never performs DNS, TLS, or a WebSocket handshake on the canvas thread. It returns gnome while opening, and a turn when the provider was already installed.

check-typetextstring
multiple-value-bind
agentopening-owner
sb-thread:with-mutex
embodied-agent-agent-lockgnome
when
embodied-agent-agent-released-pgnome
error"~A has left the world."
cond
gnome-agentgnome
values
gnome-agentgnome
nil
t
pushtext
embodied-agent-pending-agent-promptsgnome
if
embodied-agent-agent-opening-ownergnome
valuesnilnil
let
owner
list:openinggnome
setf
embodied-agent-agent-opening-ownergnome
owner
valuesnilowner
cond
agent
asktext:agentagent
opening-ownergnome
defmethodluvcraft:handle-luvcraft-focus-event
let
key
luv:canvas-key-event-key-nameevent
character
luv:canvas-key-event-characterevent
modifiers
luv:canvas-key-event-modifiersevent
approval
embodied-agent-pending-approvalgnome
paste-p
and
eq
luv:canvas-key-event-key-nameevent
:v
intersection'
:super:control
luv:canvas-key-event-modifiersevent
cond
andapproval
member:controlmodifiers
eqkey:y
andapproval
member:controlmodifiers
eqkey:n
deny-tool-approvalapproval"The player declined this possible world."
t
casekey
:return:keypad-enter
let
prompt
string-trim" "
gnome-draftgnome
setf
gnome-draftgnome
""
cond
approval
unless
string=prompt""
steer-tool-approvalapprovalprompt
string=prompt""
t
handler-case
error
condition
gnome-saygnome
formatnil"(~A)"condition
:backspace
let
draft
gnome-draftgnome
when
plusp
lengthdraft
setf
gnome-draftgnome
subseqdraft0
1-
lengthdraft
t
cond
paste-p
alexandria:when-let
setf
gnome-draftgnome
concatenate'string
gnome-draftgnome
substitute#\Space#\Newlinetext
andcharacter
graphic-char-pcharacter
<
length
gnome-draftgnome
400
setf
gnome-draftgnome
concatenate'string
gnome-draftgnome
stringcharacter
t

--------------------------------------------------------------------- The agent behind the gnome

defparameter*gnome-instructions*

You are a gnome: a small, cheerful, slightly gruff embodied agent in luvcraft, a block world running inside a live Common Lisp image. A player is talking to you. Your tools are the game's own commands. Coordinates are integer block cells: x and z are horizontal, y is up; you stand at the cell given below. To talk to the player you MUST use the say tool -- only what you say is heard; your final message is not shown. Keep what you say short and in character. Results may mention #ABCD handles; pass one back to describe-handle to read more.

defparameter*gnome-tools*'
com-saycom-where-am-icom-view-surroundingscom-move-tocom-block-atcom-place-block-atcom-propose-block-boxcom-describe-handlecom-eval
defmethodopen-embodied-agent-agent
make-world-agent:session
gnome-sessiongnome
:commands*gnome-tools*:instructions
formatnil"~A~%~%You stand at x=~D y=~D z=~D."*gnome-instructions*
gnome-xgnome
gnome-ygnome
gnome-zgnome
defunmake-gnome-observer
lambda
agentkindobject
declare
ignoreagent

Observers run in the turn's thread; they only queue intentions the canvas thread realizes in refresh-luvcraft-overlay.

casekind
:turn-started
setf
gnome-turn-finished-atgnome
nil
gnome-notegnome
list:turnobject
:thought
gnome-notegnome
list:thoughtobject
:call-started
unless
eq'com-say
command-tool-command
tool-call-toolobject
gnome-notegnome
list:callobject
:call-finished
gnome-notegnome
list:call-finishedobject
:turn-finished
setf
gnome-turn-finished-atgnome
get-internal-real-time
gnome-notegnome
list:turn-finishedobject
defungnome-note
gnomenote
sb-concurrency:send-message
gnome-notesgnome
note
defunnote-embodied-agent-failure
agentcondition&optional
context"Agent"

Publish condition for the next canvas refresh, unless agent was released.

let
publish-p
sb-thread:with-mutex
embodied-agent-agent-lockagent
not
embodied-agent-agent-released-pagent
whenpublish-p
gnome-noteagent
list:agent-failed
formatnil"~A: ~A"contextcondition
defunrelease-uninstalled-world-agent
presenceagentobserver

Release a provider candidate that was never published on presence.

whenobserver
ignore-errors
when
eq*agent*agent
setf*agent*
gnome-agentpresence
ignore-errors
nil
defunfinish-embodied-agent-opening-failure
presenceownercondition

Relinquish owner and queue one canvas-safe failure for its pending prompts.

let
publish-pnil
sb-thread:with-mutex
embodied-agent-agent-lockpresence
when
eqowner
embodied-agent-agent-opening-ownerpresence
setfpublish-p
and
embodied-agent-pending-agent-promptspresence
not
embodied-agent-agent-released-ppresence
embodied-agent-pending-agent-promptspresence
'
embodied-agent-agent-opening-ownerpresence
nil
embodied-agent-agent-opening-threadpresence
nil
sb-thread:condition-broadcast
embodied-agent-agent-readypresence
whenpublish-p
note-embodied-agent-failurepresencecondition"Could not open agent"
defunpublish-opened-embodied-agent
presenceownercandidate

Install candidate iff owner still owns presence's opening.

Return the installed provider and the pending prompts in arrival order. A candidate that loses to release or another publication is detached and closed.

let
observernil
installednil
prompts'
unwind-protect
progn

Keep validation inside the cleanup extent: a buggy opener can still have returned a live provider object of the wrong class.

check-typecandidateworld-agent
setf
world-agent-presencecandidate
presence
observer
add-agent-observercandidateobserver
sb-thread:with-mutex
embodied-agent-agent-lockpresence
when
and
eqowner
embodied-agent-agent-opening-ownerpresence
not
embodied-agent-agent-released-ppresence
cond
gnome-agentpresence
setfinstalled
gnome-agentpresence
t
setfinstalledcandidate
gnome-agentpresence
candidate
gnome-observerpresence
observer
setfprompts
nreverse
embodied-agent-pending-agent-promptspresence
embodied-agent-pending-agent-promptspresence
'
embodied-agent-agent-opening-ownerpresence
nil
embodied-agent-agent-opening-threadpresence
nil
sb-thread:condition-broadcast
embodied-agent-agent-readypresence
valuesinstalledprompts
unless
eqinstalledcandidate
release-uninstalled-world-agentpresencecandidateobserver
defunsubmit-embodied-agent-prompts
presenceproviderprompts

Submit prompts without letting one worker-creation failure lose the rest.

dolist
promptprompts
handler-case
askprompt:agentprovider
error
condition
note-embodied-agent-failurepresencecondition"Could not start agent turn"
provider
defunrun-embodied-agent-opening
presenceowner

Blocking body of the provider-opening worker owned by owner.

handler-case
let
candidate
unlesscandidate
error"The provider opener returned NIL."
multiple-value-bind
providerprompts
publish-opened-embodied-agentpresenceownercandidate
whenprovider
submit-embodied-agent-promptspresenceproviderprompts
error
condition
defunembodied-agent-opening-thread-name
presence
formatnil"Luvcraft ~:(~A~) agent opening"
defunstart-embodied-agent-opening
presenceowner

Start owner's named connection worker and return immediately.

handler-case
let
thread
sb-thread:make-thread
lambda
:name
sb-thread:with-mutex
embodied-agent-agent-lockpresence
when
eqowner
embodied-agent-agent-opening-ownerpresence
setf
embodied-agent-agent-opening-threadpresence
thread
thread
error
condition
nil
defmethodensure-embodied-agent-agent

This deliberately remains a synchronous setup primitive for SLY and explicit callers. Canvas interaction reaches only gnome-ask above.

loop
let
owner
list:synchronous-openingpresence
multiple-value-bind
provideropen-p
sb-thread:with-mutex
embodied-agent-agent-lockpresence
cond
embodied-agent-agent-released-ppresence
error"~A has left the world."
gnome-agentpresence
values
gnome-agentpresence
nil
embodied-agent-agent-opening-ownerpresence
sb-thread:condition-wait
embodied-agent-agent-readypresence
embodied-agent-agent-lockpresence
valuesnilnil
t
setf
embodied-agent-agent-opening-ownerpresence
owner
embodied-agent-agent-opening-threadpresence
sb-thread:*current-thread*
valuesnilt
whenprovider
returnprovider
whenopen-p
handler-case
let
candidate
unlesscandidate
error"The provider opener returned NIL."
multiple-value-bind
installedprompts
publish-opened-embodied-agentpresenceownercandidate
wheninstalled
submit-embodied-agent-promptspresenceinstalledprompts
returninstalled
error
condition
errorcondition
defunrelease-embodied-agent-harness
presence

Detach presence from its provider once, without waiting for an opener.

multiple-value-bind
providerobserverrelease-p
sb-thread:with-mutex
embodied-agent-agent-lockpresence
if
embodied-agent-agent-released-ppresence
valuesnilnilnil
let
provider
gnome-agentpresence
observer
gnome-observerpresence
setf
embodied-agent-agent-released-ppresence
t
gnome-agentpresence
nil
gnome-observerpresence
nil
embodied-agent-pending-agent-promptspresence
'
embodied-agent-agent-opening-ownerpresence
nil
embodied-agent-agent-opening-threadpresence
nil
sb-thread:condition-broadcast
embodied-agent-agent-readypresence
valuesproviderobservert
whenrelease-p
when
andproviderobserver
ignore-errors
remove-agent-observerproviderobserver
whenprovider
release-p

--------------------------------------------------------------------- Saying

defungnome-say
gnometext
setf
gnome-saidgnome
text
gnome-said-atgnome
get-internal-real-time
text
define-command
com-say:command-tableluvcraft-agent:name"Say"
text'string:prompt"text"
"Say TEXT aloud to the player. This is the only way the player hears you; keep it to a sentence or two."
let
ifgnome
formatnil"(nobody is here to hear: ~A)"text
"said"
defmethodcommand-ink
command
eql'com-say
make-rgb-color0.950.850.55

--------------------------------------------------------------------- The dialogue line: a HUD title at the bottom of the view.

defparameter*dialogue-width*1100
defparameter*dialogue-height*150
defparameter*dialogue-columns*70
defclassgnome-dialogue-pane
:documentation

A pane that paints nothing but its text: the view shows through.

define-application-framegnome-dialogue
gnome:initarg:gnome:readerdialogue-gnome
:menu-barnil

The pane is the whole frame: a layout pane around it would paint its own background, and there is nothing to lay out.

:panes
:layouts
defaultsheet
defundialogue-lines

What the line shows: (SPEAKER TEXT INK) or NIL.

let*
session
gnome-sessiongnome
focused-p
eqgnome
luvcraft:luvcraft-session-modal-focussession
draft
gnome-draftgnome
said
gnome-saidgnome
said-age
/
-
get-internal-real-time
gnome-said-atgnome
floatinternal-time-units-per-second1.0
cond
andfocused-p
embodied-agent-pending-approvalgnome
zerop
lengthdraft
list"proposal""possible world · Ctrl-Y approve · Ctrl-N deny · type feedback + Enter"
make-rgb-color0.450.951.0
andfocused-p
or
plusp
lengthdraft
nullsaid
>said-age20
list"you"
concatenate'stringdraft"_"
*hud-text-ink*
andsaid
orfocused-p
<said-age20
listsaid
make-rgb-color0.970.930.80
tnil
defmethodhandle-repaint
declare
ignoreregion
let*
frame
pane-framepane
gnome
dialogue-gnomeframe
whenline
destructuring-bind
speakertextink
line
let*
lines
if
>
lengthlines
4
lastlines4
lines
line-height30
height
+18
*line-height
lengthlines
14
left40
mcluv:draw-analytic-rounded-rectangle*panelefttopright*dialogue-height*:radius10:ink
make-rgb-color0.060.060.06
draw-text*panespeaker
+left24
+top22
:align-y:center:text-size15:text-face:bold:ink
if
string=speaker"you"
*hud-muted-ink*
command-ink'com-say
let
y
+top1814
dolist
text-linelines
draw-text*panetext-line
+left2470
+y2
:align-y:center:text-size22:inkink
incfyline-height
defclassgnome-dialogue-overlay
gnome:initarg:gnome:readerdialogue-overlay-gnome
visible-state:initformnil:accessordialogue-visible-state
defmethodluvcraft:luvcraft-overlay-stage
declare
ignoreoverlay
:hud
defmethodluvcraft:luvcraft-focus-score
declare
ignoreoverlaysession
nil
defundialogue-screen-state
overlay

Centred at the bottom of the viewport.

let*
viewport-size
multiple-value-list
luv:canvas-logical-size
luvcraft:luvcraft-session-canvas
mcluv:widget-overlay-sessionoverlay
source-width
firstsource-size
source-height
secondsource-size
viewport-width
firstviewport-size
viewport-height
secondviewport-size
scale
min1.0
/
-viewport-width24.0
source-width
half-width
/
*source-widthscale
viewport-width
half-height
/
*source-heightscale
viewport-height

NDC spans two units over the viewport, so this is half the pixels.

bottom-margin
/300.0viewport-height
center-y
-1.0bottom-marginhalf-height
make-array12:element-type'single-float:initial-contents
mapcar
lambda
v
coercev'single-float
list0.0center-y0.01.0half-width0.00.00.00.0half-height0.00.0
defmethodluvcraft:encode-luvcraft-overlay
sessionpasssurface-texture
declare
ignorepass
overlay
defunrepaint-gnome-dialogue
alexandria:when-let
let*
frame
mcluv:widget-overlay-frameoverlay
mirror
sheet-direct-mirror
frame-top-level-sheetframe
setf
dialogue-visible-stateoverlay
defmethodluvcraft:refresh-luvcraft-overlay
declare
ignoresession
unless
equal
dialogue-lines
dialogue-overlay-gnomeoverlay
dialogue-visible-stateoverlay
repaint-gnome-dialogue
dialogue-overlay-gnomeoverlay
overlay
defunmake-embedded-frame
sessionframe-class&restinitargs

Make an enabled McCLIM frame embedded in session's canvas; return it.

let*
port
find-port:server-path'
:luv-gpu
manager
or
first
climi::frame-managersport
make-instance'mcluv:luv-frame-manager:portport
let
mcluv:*embedded-mirror-target*
luvcraft:luvcraft-session-canvassession
mcluv:*embedded-mirror-context*
luvcraft:luvcraft-session-contextsession
mcluv:*embedded-mirror-device*
luvcraft:luvcraft-session-devicesession
let
frame
apply#'make-application-frameframe-class:frame-managermanager:enabletinitargs

Nothing behind the pane either: the top-level sheet would otherwise fill the frame's rectangle with its own white.

frame

--------------------------------------------------------------------- The visible body: a sphere-traced scene object, not terrain.

defclasssdf-agent-body-overlay
agent:initarg:agent:initarg:gnome:readerbody-overlay-gnome
pipeline:initarg:pipeline:accessorgnome-body-pipeline
vertex-buffer:initarg:vertex-buffer:accessorgnome-body-vertex-buffer
instance-buffer:initarg:instance-buffer:accessorgnome-body-instance-buffer
instance-data:initarg:instance-data:readergnome-body-instance-data
:documentation

The shared conservative billboard and GPU resources for one SDF agent.

defclassgnome-body-overlay
:documentation

The gnome's SDF body overlay.

defmethodluvcraft:luvcraft-focus-score
declare
ignoreoverlaysession
nil
defmethodembodied-agent-body-sphere

The gnome's current conservative proxy, including his visual bob.

let*
phase
/
get-internal-real-time
floatinternal-time-units-per-second1.0
bob
*0.025
sin
*phase2.2
defunplace-gnome-body
overlay

Publish overlay's conservative sphere and retained forward direction.

let*
agent
body-overlay-gnomeoverlay
yaw
embodied-agent-facing-yawagent
data
gnome-body-instance-dataoverlay
multiple-value-bind
xyzradius
setf
arefdata0
coercex'single-float
arefdata1
coercey'single-float
arefdata2
coercez'single-float
arefdata3
coerceradius'single-float
arefdata4
coerce
sinyaw
'single-float
arefdata5
0.0
arefdata6
coerce
cosyaw
'single-float
arefdata7
0.0
luv:write-buffer
gnome-body-instance-bufferoverlay
data
overlay
defmethodluvcraft:encode-luvcraft-overlay
sessionpasssurface-texture
let
luv:set-pipelinepass
luvcraft::live-shader-pipeline-native-pipeline
gnome-body-pipelineoverlay
luv:set-vertex-bufferpass0
gnome-body-vertex-bufferoverlay
luv:set-vertex-bufferpass1
gnome-body-instance-bufferoverlay
luv:set-bind-grouppass0
luvcraft::luvcraft-frame-scene-bind-groupframe
luv:drawpass61
overlay
defmethodluvcraft:release-luvcraft-overlay
when
gnome-body-pipelineoverlay
luvcraft::release-live-shader-pipeline
gnome-body-pipelineoverlay
setf
gnome-body-pipelineoverlay
nil
dolist
resource
list
gnome-body-instance-bufferoverlay
gnome-body-vertex-bufferoverlay
whenresource
luv:destroyresource
setf
gnome-body-instance-bufferoverlay
nil
gnome-body-vertex-bufferoverlay
nil
when
eqoverlay
gnome-body
body-overlay-gnomeoverlay
setf
gnome-body
body-overlay-gnomeoverlay
nil
values
defunmake-sdf-agent-body
agentrolelabel&key

Make and attach one role pipeline body for agent.

let*
session
gnome-sessionagent
device
luvcraft:luvcraft-session-devicesession
instance-data
make-array8:element-type'single-float
vertex-buffernil
instance-buffernil
pipelinenil
completed-pnil
unwind-protect
progn
setfvertex-buffer
luv:createdevice
luv:make-buffer-descriptor:label
formatnil"~A proxy"label
:size
*4
lengthvertex-data
:usage'
:vertex:copy-dst
instance-buffer
luv:createdevice
luv:make-buffer-descriptor:label
formatnil"~A instance"label
:size
*4
lengthinstance-data
:usage'
:vertex:copy-dst
pipeline
luvcraft::make-live-shader-pipeline:rolerole:vertex-rolerole:label
formatnil"~A pipeline"label
:devicedevice:layout
luvcraft::live-shader-pipeline-layout
:vertex-buffers'
:array-stride12:attributes
:shader-location0:offset0:format:float32x3
:array-stride32:step-mode:instance:attributes
:shader-location1:offset0:format:float32x4
:shader-location2:offset16:format:float32x4
:target-formatluvcraft::+luvcraft-scene-color-format+:target-blend:premultiplied-alpha:primitive'
:topology:triangle-list
:depth-stencil'
:format:depth32-float:depth-write-enablednil:depth-compare:less
luv:write-buffervertex-buffervertex-data
let
overlay
make-instanceclass:agentagent:pipelinepipeline:vertex-buffervertex-buffer:instance-bufferinstance-buffer:instance-datainstance-data
setf
gnome-bodyagent
overlay
completed-pt
overlay
unlesscompleted-p
whenpipeline
wheninstance-buffer
ignore-errors
luv:destroyinstance-buffer
whenvertex-buffer
ignore-errors
luv:destroyvertex-buffer
defunensure-gnome-dialogue
or
let*
session
gnome-sessiongnome
mirror
sheet-direct-mirror
frame-top-level-sheetframe
overlay
make-instance'gnome-dialogue-overlay:sessionsession:frameframe:mirrormirror:gnomegnome
setf
frame-pretty-nameframe
"gnome dialogue"
mcluv:mirror-compositormirror
overlay

ADD consumes a rejected overlay. Publish the semantic handle only after the session accepted it, never while it names released state.

overlay

--------------------------------------------------------------------- Bubbles: cassettes in the world above the gnome's head.

defparameter*bubble-width*520
defparameter*bubble-height*150
defparameter*bubble-world-width*1.7"How wide a bubble is in blocks."
defparameter*bubble-lift*0.55"How far above the hat the newest bubble's centre floats, in blocks."
defparameter*bubble-spacing*0.52"The vertical distance between stacked bubbles, in blocks."
defparameter*bubble-count*4"How many bubbles stay up before the oldest is dropped."
defparameter*bubble-linger-seconds*14"How long after a turn ends its bubbles stay."
define-application-framegnome-bubble
gnome:initarg:gnome:readerbubble-gnome
item:initarg:item:accessorbubble-item:documentation"A TURN (its thought) or a TOOL-CALL."
:menu-barnil
:panes
sheet
:layouts
defaultsheet
defunbubble-state

What the bubble shows, as a key: repaint when it changes.

let
item
bubble-itemframe
etypecaseitem
turn
list:thought
length
turn-thoughtitem
tool-call
list
tool-call-statusitem
length
tool-call-outputitem
defparameter*bubble-margin*20
defparameter*bubble-columns*48
defunbubble-thought-lines
let
lines
wrap-words
remove#\*
turn-thoughtturn
*bubble-columns*
or
if
>
lengthlines
4
lastlines4
lines
list"..."
defmethodhandle-repaint
declare
ignoreregion
let*

The cassette: a matte rounded slab standing off the world, as tall as its contents and sitting at the bottom of the pane, nearest the head.

setf
stream-cursor-positionpane
etypecaseitem
tool-call
funcall-presentation-generic-functionpresentitem'tool-callpane+cassette-view+
defclassgnome-bubble-overlay
gnome:initarg:gnome:readerbubble-overlay-gnome
lift:initform0.0:accessorbubble-lift:documentation"Where the bubble floats now, in blocks above the hat."
target-lift:initform0.0:accessorbubble-target-lift
visible-state:initformnil:accessorbubble-visible-state
defmethodluvcraft:luvcraft-focus-score
session
declare
ignoreoverlaysession
nil
defunplace-bubble
overlaysession

Face the camera from above the gnome, at the bubble's current lift.

let*
gnome
bubble-overlay-gnomeoverlay
camera
luvcraft:luvcraft-session-camerasession
center
half-height
/half-widthaspect
multiple-value-bind
rightupforward
declare
ignoreup
let
flat-right
luvcraft::make-vec3
luvcraft::vec3-xright
0.0
luvcraft::vec3-zright
setf
mcluv::widget-overlay-centeroverlay
center
mcluv::widget-overlay-right-axisoverlay
luvcraft::vec3-scaleflat-righthalf-width
mcluv::widget-overlay-up-axisoverlay
luvcraft::make-vec30.0
-half-height
0.0
mcluv::widget-overlay-normal-axisoverlay
defmethodluvcraft:encode-luvcraft-overlay
sessionpasssurface-texture

Draw the bubble flat, in the scene, facing the player.

declare
ignorepass
place-bubbleoverlaysession
let
viewport-size
luv:canvas-extent
luvcraft:luvcraft-session-contextsession
mcluv:prepare-direct-widget-overlayoverlaysessionsurface-texture
mcluv::world-device-clip-stateoverlaysession
firstviewport-size
secondviewport-size
overlay
defunrepaint-bubble
overlay
let*
frame
mcluv:widget-overlay-frameoverlay
mirror
sheet-direct-mirror
frame-top-level-sheetframe
setf
bubble-visible-stateoverlay
defmethodluvcraft:refresh-luvcraft-overlay
session
declare
ignoresession

Ease toward the stack position the newer bubbles have pushed it to.

let
lift
bubble-liftoverlay
target
bubble-target-liftoverlay
setf
bubble-liftoverlay
+lift
*0.18
-targetlift
let
frame
mcluv:widget-overlay-frameoverlay
unless
equal
bubble-visible-stateoverlay
overlay
defunadd-gnome-bubble
gnomeitem

Put a new bubble for item above gnome's head, pushing the others up.

let*
session
gnome-sessiongnome
mirror
sheet-direct-mirror
frame-top-level-sheetframe
overlay
make-instance'gnome-bubble-overlay:sessionsession:frameframe:mirrormirror:gnomegnome:height-scale0.35
setf
frame-pretty-nameframe
"gnome bubble"
mcluv:mirror-compositormirror
overlay
bubble-liftoverlay
bubble-target-liftoverlay
*bubble-lift*

The gnome's semantic stack contains only application-owned overlays. A terminal rejection releases OVERLAY before this publication point.

pushoverlay
gnome-bubblesgnome
overlay
defunrestack-gnome-bubbles

Newest lowest; the ones past the count are taken down.

loopforoverlayin
gnome-bubblesgnome
forindexfrom0do
setf
bubble-target-liftoverlay
loopwhiledo
let
oldest
car
last
gnome-bubblesgnome
setf
gnome-bubblesgnome
butlast
gnome-bubblesgnome
defunclear-gnome-bubbles
dolist
overlay
gnome-bubblesgnome
setf
gnome-bubblesgnome
'
defunbubble-for-item
gnomeitem
finditem
gnome-bubblesgnome
:key
lambda
overlay
bubble-item
mcluv:widget-overlay-frameoverlay

--------------------------------------------------------------------- Realizing the transcript, on the canvas thread, once a frame.

defunrealize-gnome-note
gnomenote
destructuring-bind
kindobject
note
ecasekind
:call-finishednil
:agent-failed
gnome-saygnome
formatnil"(~A)"object
:turn-finished
unless
string=
turn-textobject
""

A gnome that forgot to SAY still gets heard.

unless
and
gnome-saidgnome
>
gnome-said-atgnome
turn-startedobject
gnome-saygnome
turn-textobject
defmethodluvcraft:refresh-luvcraft-overlay
declare
ignoresession

The gnome itself draws nothing; its refresh is where the turn thread's notes become bubbles and lines, and where old bubbles are taken down.

loopfor
notereceived-p
=
multiple-value-list
sb-concurrency:receive-message-no-hang
gnome-notesgnome
whilereceived-pdo
alexandria:when-let
finished
gnome-turn-finished-atgnome
when
and
gnome-bubblesgnome
>
/
-
get-internal-real-time
finished
floatinternal-time-units-per-second1.0
*bubble-linger-seconds*
gnome
defmethodluvcraft:advance-luvcraft-overlay
sessionseconds

Run agent's Move To motor at the same fixed step as the player.

incf
embodied-agent-movement-accumulatoragent
seconds
loopwhile
>=
embodied-agent-movement-accumulatoragent
luvcraft::+player-physics-step+
do
when
luvcraft:advance-body-movementagent
luvcraft:luvcraft-session-worldsession
luvcraft::+player-physics-step+
let*
x
luvcraft::vec3-xvelocity
z
luvcraft::vec3-zvelocity
when
>
+
*xx
*zz
1d-4
setf
embodied-agent-facing-yawagent
atanxz
decf
embodied-agent-movement-accumulatoragent
luvcraft::+player-physics-step+

The public place follows the continuous base as it crosses cell bounds.

multiple-value-bind
xyz
setf
gnome-xagent
x
gnome-yagent
y
gnome-zagent
z
agent
defmethodluvcraft:release-luvcraft-overlay:after
alexandria:when-let
approval
embodied-agent-pending-approvalgnome
deny-tool-approvalapproval"The embodied agent left the world."
setf*agents*
deletegnome*agents*:test#'eq

--------------------------------------------------------------------- Spawning one

defunspawn-embodied-agent
class&keyxyz

Spawn an embodied agent of class at integer cell X,Y,Z and return it.

With no coordinate, choose an empty cell a few steps ahead of the player and stand the agent on the first supporting block. The cell names its discrete place while its position and velocity continuously realize travel between places.

let*
player
luvcraft:luvcraft-session-playersession
camera
luvcraft:luvcraft-session-camerasession
world
luvcraft:luvcraft-session-worldsession
unlessx
multiple-value-bind
rightupforward
declare
ignorerightup
setfx
floor
+
*3.5
luvcraft::vec3-xforward
z
floor
+
*3.5
luvcraft::vec3-zforward
y

Stand on the ground, not in it.

loopwhiledo
incfy
loopwhiledo
decfy
unless
and
integerpx
integerpy
integerpz
error"An agent needs an integer cell, got (~S ~S ~S)."xyz
when
error"Cell (~D ~D ~D) is occupied by terrain."xyz
let
existing
agent-atsessionxyz
cond
nullexisting
attach-embodied-agentclasssessionxyz:facing-yaw
typepexistingclass
existing
t
error"Cell (~D ~D ~D) is occupied by ~A."xyz
defunspawn-agent
&keyxyz

Spawn a visible gnome agent, choosing a supported cell when omitted.

spawn-embodied-agent'gnome:sessionsession:xx:yy:zz
define-command
com-spawn-agent:command-tableluvcraft.clim::luvcraft-world:name"Spawn Gnome"
"Spawn a visible gnome agent a few discrete cells ahead of the player."