Story Format
Overview
Basically, you write story inside javascript or typescript files, you get smart completions, typechecking, etc... But also you have to write a lot of boilerplate code. Let's compare the short story written in JS and in our language:
engine.script({
start: [
a.showBackground(room),
a.showCharacter("Sister", "curious"),
a.dialog("Sister", "Mum, he's still asleep!"),
a.choice(
"What would you do?",
["Wake up", [a.dialog("You", "Oh no!"), a.exit()]],
[
"Continue sleeping",
[a.dialog("You", "Let me get some sleep"), a.exit()],
]
),
a.condition((state) => state.sister.angry, {
true: [
a.dialog(
"Sister",
"Are you out of your mind? We're going to be late because of you!"
),
a.exit(),
],
false: [a.dialog("Sister", "Wake up!"), a.exit()],
}),
a.end(),
],
});
start
!showBackground %room
!showCharacter Sister "curious"
!dialog
\Sister
\Mum, he's still asleep!
!choice
\What would you do?
=
\Wake up
=
!dialog
\You
\Oh no!
!exit
=
\Continue sleeping
=
!dialog
\You
\Let me get some sleep
!exit
!condition
%(state) => state.sister.angry
*
true
!dialog
\Sister
\
Are you out of your mind?
We're going to be late because of you!
!exit
false
!dialog
\Sister
\Wake up!
!exit
!end
See it? No more parentheses, not so much quotes! Now let's look at it in more detail:
- It is based on identation
- Story keys does not need array literals
- Actions start with
!
- Strings are wrapped in
""
or takes a new line and starts with\
- JS Values are starts with
%
- Values from JS need to start with
$values1
, we'll see why later (however, it can be avoided) - Object is replaced with
*
and nested keys - Arrays replaced with
=
Why
The reason why that format is needed, at least to me, is that is does not have so much boilerplate as well as visually repetitive designs like parentheses, brackets, and colons.
Like
export default {
start: [a.showCharacter("Ciri", "smiley")],
};
And
start
!showCharacter Ciri smiley
Type less!
Details
When you import .novely
file, the plugin starts it's work, then the parser parses the source, and transformer transforms it to JavaScript.
Internal story format is basically what do you see. When you use the engine.action.some(prop1, prop2)
syntax, it just changes to ['some', prop1, prop2]
.
This custom format cannot be purely changed to ['some', prop]
, because prop
is not accessible here. Instead, it returns a function. Take a look:
export default ($values1) => ({
start: [["some", $values1.prop]],
});
That way variables can be passed to the story. But not everything should be passed from some object. There are set of things that prevent use of $values1
: When values starts from undefined
, null
, window
, globalThis
, or ()
, the raw value is used.
So,
start
!function
%() => $values1.state({ some: 'value' })
!function
%someFunction
!function
%setRelationshipPreference('None')
Will be transformed into
export default ($values1) => ({
start: [
["function", () => $values1.state({ some: "value" })],
["function", $values1.someFunction],
["function", $values1.setRelationshipPreference("None")],
],
});
And when you will run that, you will need to pass state
and someFunction
:
import setupStory from "./story.novely";
const story = setupStory({
state: engine.state,
someFunction: () => {
console.log("Do something here");
},
setRelationshipPreference: (target) => {
/**
* Make a closure that will update something
*/
return () => {
/**
* Update relationships
*/
state({ relationships: { target } });
};
},
});
engine.script(story);