Speech
you know what, screw uppercase. not like that many people are gonna be reading this anyway.
creating a speech
the basics
do this, basically.
- ts
import type { SpeechFunction } from "$lib/speech/Speech"; export const speech: SpeechFunction = async function* (s) { // horray };
congrats! you have a speech that does nothing.
the
s
argument is important. it's your speech goodies, which are your gateway to doing anything, really.and that's the basics. let's review:
- export default an async generator
- the s argument is important!!!!!!!!!!!!!!!
cleaning up after yourself
before i get onto the actual speech part, i should touch on cleaning up after yourself.
this is a website, so we kinda have to deal with the issue of that the user might just ditch us halfway through via "links".
luckily it isn't so bad due to the generator functions.
yielding
whenever you want to check if it's okay to keep on going, you yield.
yielding is done best whenever there's a gap in the code to wait on, like when waiting for a promise to resolve. so yield also acts like await.
- ts
yield delayedResolve(1000);
however, i couldn't type it nicely, so you'll just get an
any
back when dealing with a value. a workaround in cases like those would be to just use await
and put yield
on a line on its own afterwards:- ts
const response = await fetch("/"); yield;
or just type the variable:
- ts
const response: Response = yield fetch("/");
it's not the prettiest, but who cares. it's just me :)
Warningsome functions that return values, likecharacter.prompt()
, must be followed by a yield. this is because functions like these have a mechanism to resolve early withnull
incase the user clicks off.
Questionwhy notyield* await
?do you want to type that out every single time?
anyway, the
yield
ing lets you do some weird things while letting you clean up easily. in this example, the user can click off without having to respond to the dialog and the ticker won't linger.- ts
const ticker = new Ticker().start(); ticker.onTick.add(() => { // some code to do some animation, idk }); try { yield dabric.say("I'm animating!!!!"); } finally { ticker.stop(); }
methods to help cleaning up in the goodies
they make life easier for how you're likely to use them in speeches, and they also automatically clean up after themselves so you don't have to do anything.
if you want to make your own, the event to listen to is
s.onDestroy
. just like svelte.ticker
lets you start the ticker and subscribe to it with a single function.
the above example could be shortened down to:
- ts
const ticker = s.ticker( { /* options */ }, () => { // some code to do animation, idk }, ); yield dabric.say("I'm animating!!!"); ticker.stop();
note that the try finally isn't needed anymore as the ticker helper is cleaning up for us.
subscribeTo
lets you subscribe to something with a similar nature to svelte stores; the usual object with a
subscribe
function that returns another function that unsubscribes...- ts
s.subscribeTo(thing, () => { /* blah */ });
talking
now we can get to characters and making them talk.
first, you'll need to get a character. you can just import one and then add it with
s.addCharacter()
- ts
const dabric = s.addCharacter(DabricCharacter);
making them talk is pretty simple:
- ts
yield dabric.say("hello");
you can also prompt for something like so:
- ts
// 0 if "idk", 1 if "you tell me"... const result = await dabric.prompt(["idk", "you tell me"]); yield;
and combine say and prompt at the same time with
ask
:- ts
const result = await evilMan.ask("What's your favorite color >:)", [ "idk", "green", ]); yield;
you can also directly mutate some properties for full control:
- ts
// makes the dialog appear dabric.isActive.set(true); // changes the text dabric.text.set("Text"); // waits for click dabric.onInteract.wait();
character states
characters have states. like what's their facial expression? or whatever?
there's three properties that govern it:
defaultState
: the default state, which you should normally leave alone.state
: applied overdefaultState
, it's literally just the state.lineState
: applied overstate
, it resets once a line is spoken.
you can set a
lineState
using .say
like so:- ts
yield dabric.say("I'm winking!!!", { face: "wink" }); yield dabric.ask("example", ["blah", "blah"], { face: "straight" });
for setting the state, you can just directly set it:
- ts
dabric.state.face = "wink"; yield dabric.say("I'm winking!!!!");
when you directly set the state, it won't update. you have to manually update the state using
.updateState()
. .say
and friends automatically do this, so it's usually not needed, but you may need to do it when using something like animations.by default a character has no states. oops
s.batch
a helper function that removes a lot of the boilerplate by using template literals. it's meant to be similar to the old yaml speech system.
so why didn't we just stick with the yaml system?sure it's elegant to write characters saying stuff, but its weaknesses start to appear when you have them react based on what you say. that's best to do with code, thus this.
- typescript
const dabric = s.addCharacter(DabricCharacter); yield * s.batch` ${dabric} hello, i'm dabric saying words this is another dialog this is a dialog\nwith a line break! aren't template literal whatevers cool? ${OtherGuyCharacter} Other guy talkin here yes, you can just give it an unconstructed character. it'll add it on its own and also remember the instance it made. Great for being lazy ${dabric} now i'm talking again ${{ face: "wink" }} I'M WINKING USING LINESTATE!! that means i'm not winking anymore whitespace is trimmed and blank lines are ignored how nice. ${`you can also do this!! now you can use ${variables}!!!!!`} `; yield * s.batch` characters are preserved across calls, so i'm still dabric talking ${OtherGuyCharacter} same with me!!!!!!! `;
the stage
Unimplemented
characters talking is fun, but sometimes they are represented as something actually on the screen, and you need some way to go like focus between them. stages are a system that handles some of this for you.
you can access the stage through
s.stage
.thinking, would it be good to support multiple stages? like two characters in different rooms? how would that work? that would already be somewhat complex camera-wise lmao
the stage manages which character(s) have focus. by default, it'll give the current talking character focus and have it linger until another one talks. this is auto mode.
you can set focus with a character or the stage itself. this will override auto mode.
- ts
dabric.focus(); // or stage.focus(dabric);
you can also give multiple characters focus:
- ts
stage.focus(dabric, coolGuy);
to return to auto mode handling it, just set the focus to null.
- ts
stage.focus(null);
this is really all stage does. it gives some standardization between renderers in terms of where to point the camera at.
onFocus
lets you listen to when the focus changes.running another speech
sometimes you want to pass the torch onto another speech to run. to do this just use
runSpeech()
. it will inherit any context with it.- ts
// supports relative paths yield s.runSpeech("../hell");
starting a speech
- ts
const speech = new Speech(); await speech.loadSpeech("/path/to/speech"); // or await speech.loadSpeech(aSpeechYouSomehowHaveOnHand);
you can then run the loaded speech like so, setting up beforehand if needed:
- ts
speech.context[SomeSymbol] = someThing; // resolves once this specific speech stops running await speech.run();
alternatively, you can also provide the path/speech directly to
.run
:- ts
await speech.run("/path/to/speech");
context
when you make dabric wink, which
DynamicDabricFaceManager
will you use? that's what contexts help with.you can access it with
context
on the Speech itself, or on the SpeechGoodies. it's just an object.when setting it, you should use symbols to avoid conflicts.
here's an idea. a function like so:
- js
function getFaceContext(speech) { if (speech[FaceContextSymbol] == null) { speech[FaceContextSymbol] = new FaceWhatever(); } return speech[FaceContextSymbol]; }
making a character
- ts
class DabricCharacter extends Character { constructor(data: CharacterData) { super(data); this.name.$ = "Dabric"; this.icon.$ = "/url/to/icon.png"; this.color.$ = { foreground: "hsl(56, 100%, 50%)", background: "hsl(56, 100%, 10%)", }; this.onStateChange.add(() => { // do something with the state }); } override defaultState = { face: "default", }; override async *typeMessage(message: string) { let currentMessage = ""; for (let i = 0; i < message.length; i++) { currentMessage += message[i]; await timeoutPromise(100); yield currentMessage; } } }