This guide covers the format of a world template and how to make your own.
Every entity is created from a template. Template values may not have any variation and always generate the same entity, or have a range of values for more procedural worlds.
Within an entity template, string fields are replaced with template strings and number fields are replaced with template numbers. These are rendered back into a string or number value when an entity is created from the template.
Each template has a set of base
values and list of modifiers, often adjectives like sharp or rusty. Modifiers each
have a chance of appearing, and can exclude one another, to prevent mutually exclusive modifiers from appearing
together.
Between the defaults and modifiers, templates have a few layers:
- the world
defaults
- the
base
template - select template
mods
When creating an entity from the template, the world defaults are rendered first, then passed to the base
template.
Some of the mods
are randomly selected, then rendered in order, with the result of the previous.
For example, when creating an actor:
- the actor defaults have a base name of
none
(defaults.actor.name.base
) - the actor template has a base name of
bat
(templates.actors.0.base.name.base
) - the actor template has a modifier with a base name of
vampire {{base}}
(templates.actors.0.mods.0.name.base
)
Each string will be rendered in order:
none
bat
(does not use the{{base}}
token and so replaces the string entirely)vampire bat
Entities are created from templates, and retain a copy of the template ID. While most strings in a template are
template strings, the ID is a literal string. It is not rendered, but will have a sequential numeric suffix appended,
such as actor-bat-0
and actor-bat-1
. Modifier metadata omits the ID entirely.
Field | Template | Modifier | Entity |
---|---|---|---|
desc |
template string | template string | literal string |
id |
literal string | not present | literal string |
name |
template string | template string | literal string |
The metadata is a convenient container for localization and searching, containing the entity's unique ID along with its short display name and longer description.
For example:
This is how metadata should appear in a base template:
meta:
desc:
base: bat
name:
base: Bat
id: actor-bat
This is how metadata should appear in a template modifier:
meta:
desc:
base: vampire {{base}}
name:
base: Vampire {{base}}
This is how metadata will appear in the saved game state:
meta:
desc: vampire bat
name: Vampire Bat
id: actor-bat-0
Every world entity has a flags
field for storing short strings. Flags are meant to help scripts maintain state on
the entity without changing the class, to communicate with other scripts or between invocations of the same script.
Since JS strings are immutable, flags can only be set and removed. For numeric data that needs to be changed, helper functions are provided to modify the entity stats.
Flags are stored on each entity and must be sent whenever the entity changes or moves into another room, so it is important to make sure they do not grow too large. If you expect 10 flags per entity, try to keep them under 24 characters per flag.
The flags field is a [string, string]
map, and flag values are template strings.
For example:
flags: !map
scene:
base: cutscene-room
Some common flags are defined in the engine:
- items with
key
can unlock portals whose ID matches the value - rooms with
scene
will move actors into a cutscene room if they do not have a flagscene-${room.meta.id}
- items with
replace
and a script forsignal.replace
can be replaced with other items- tearing a piece of paper into scraps
- tearing a loaf of bread into crumbs
- filling out a form
Scripts can add their own flags by setting them. They do not need to be defined in the engine.
Actors and items have stats
for storing numeric data, like actor health and weapon damage. Helper functions are
provided to get, increment, and decrement stats.
Some common stats are defined in the engine:
- actors with
damage
do additional damage when using weapons - actors with
health
can be killed - items with
damage
are weapons and do damage when an actor ishit
with them - items with
health
can heal actors
Scripts can add their own stats by setting them. They do not need to be defined in the engine.
Including a minimum and maximum value in each stat is a planned feature: #148
Each room has some portals, grouped by wall or direction, with a destination room template.
When the player enters a new room, including the starting room, the game generates destination rooms for each group and creates links in both directions, ensuring the player can backtrack.
When starting a new game, the world begins empty. One of the starting rooms is selected and created, then populated with actors, items, and portals. Additional rooms are added to those portals, until the world depth has been reached.
When a new player joins, one of the starting actors is selected and created, unless an actor already exists with that
player's ID. In single-player, the player always joins the new world after create
or load
commands.
If there is only one starting room or actor, it will always be used. At least one room or actor must be present.
YAML is a human-readable config format with support for comments and sensitive to indentation. The Red Hat Ansible documentation has a good description of the format, and the CircleCI documentation has some helpful illustrated examples.
Every textual-engine
data file starts with a dictionary:
config: {} # optional config object
state: [] # optional save state
worlds: [] # list of world templates
Please see the YAML 1.2 specification for the complete syntax.
The js-yaml library is used to parse YAML and offers an online demo and validator. js-yaml
supports the YAML 1.2 specification with custom types.
The textual-engine
YAML schema adds a few custom types:
!env
- loads an environment variable by name
- for configuring the server
!map
- loads a JS
Map
from a YAML dictionary
- loads a JS
!stream
- loads a JS
process
output stream - for configuring the log library
- loads a JS
For example:
config:
logger:
level: !env TEXTUAL_LOG_LEVEL
name: textual-engine
streams:
- level: error
stream: !stream stderr
worlds:
- # some fields omitted
templates:
actors:
- base:
flags: !map
key1: value1
key2: value2
If you prefer using JSON over YAML, or want to use tooling that only supports JSON, most of the data file format is supported with the notable exception of custom types.
The YAML syntax is a superset of JSON, and most of the value types can be written in JSON, including dictionaries and lists. JSON does not have syntax for custom types and so does not support maps, which prevents JSON worlds from using flags or stats, unless those fields are written with inline YAML:
"dict": {
"list": [
1,
2,
3
],
"map": !map {
"key1": "value1",
"key2": "value2"
}
}
This may be changed in a future release to support strict JSON.
The engine supports pluggable parsers for other file format, including binary formats. Only YAML
and limited JSON
support are included.
To load a world template from a local file, such as one you are editing, use the load
command with a file://
path:
> load file://data/samples/alice.yml
no world states loaded from file://data/samples/alice.yml
Improving this output to indicate whether world templates were loaded is a planned feature: #153
To load a world template from Github, use the raw file link from the Gist or pull request, with the https://
protocol:
> load https://raw.githubusercontent.com/ssube/textual-engine/master/data/demo.yml
no world states loaded from https://raw.githubusercontent.com/ssube/textual-engine/master/data/demo.yml
This allows you to test new worlds from a branch or PR without checking it out locally.
To start a new game and create an instance of your world template, make sure it is loaded by listing the worlds
:
> worlds
test world (test)
Alice in Wonderland (sample-alice)
Then create
a new world using the template ID, with the world seed and number of rooms to generate before loading:
> create a sample-alice with test seed and 4
created new world Alice in Wonderland (sample-alice-1) from sample-alice with seed of test seed and room depth of 4
> look
Alice will look the next turn.
You are a Alice: Alice in Wonderland (player-0).
You have 20 health.
You are in a Introduction: a garden with a rose tree (room-intro-21).
You see a Rose: red rose with sharp thorns (item-rose-12).
You see a Painted Rose: painted white rose with sharp thorns (item-rose-13).
You see a Rose: red rose with sharp thorns (item-rose-14).
The engine can print a graph of the generated rooms and portal connecting them in the DOT language,
which can be drawn by the Graphviz tools. make
targets are provided to run the game and
render the graph, and the graph can be used to debug custom worlds.
To load the sample Dracula world template, create a new game, print the graph, and render it:
> RUN_ARGS='--config data/config.yml --data file://data/samples/dracula.yml --input "create a sample-dracula with test and with 4" --input "graph file://out/debug-graph" --input quit' make run graph
yarn
yarn install v1.22.4
[1/4] Resolving packages...
success Already up-to-date.
Done in 0.31s.
yarn tsc
yarn run v1.22.4
$ /home/ssube/code/ssube/text-adventure/node_modules/.bin/tsc
Done in 14.03s.
node --require esm out/src/index.js --config data/config.yml --data file://data/samples/dracula.yml --input "create a sample-dracula with test and with 4" --input "graph file://out/debug-graph" --input quit
Output: 3 lines > Actors
created new world Dracula (sample-dracula-0) from sample-dracula with seed of test and room depth of 4 Items
wrote 3 node graph to file://out/debug-graph Portals
quitting Verbs
Game Over
health: 10
main exited 0
cat out/debug-graph | dot -Tpng -oout/debug-graph.png && sensible-browser out/debug-graph.png
Opening in existing browser session.
A number of simple mechanics are built into the engine:
- looking at actors, items, and through portals
- moving between rooms
- using items
- for damage and health effects
- on yourself and other actors
- basic inventory
- taking and dropping items
- using items from inventory
- equipping items into character-specific slots
These are enabled by default, using the required fields in each template.
Some more complex features require the template to set additional flags or scripts.
TODO: explain how to use closed/locked stats
TODO: explain hidden rooms and timed/triggered movement
TODO: explain how to use scene
flag
TODO: explain removing entities from script
TODO: explain how to use replace verb/signal
Worlds have template metadata, with a literal id
and template strings for the rest.
id
- used in the saved state to refer back to the template world
- literal string, not templated
name
- short display name
- a template string
desc
- longer description
- a template string
TODO: describe entity defaults
Worlds may define multiple languages in their locale bundle. The key for each language should be its ISO 639-1, 639-2, or 639-3 code.
For example:
locale:
languages:
de: {}
en: {}
es: {}
Each language contains word lists for the recognized parts of speech, along with a nested dictionary for longer strings.
The recognized parts of speech are:
- articles
- ignored while parsing
- prepositions
- used to split multiple targets into phrases
- verbs
- decide which actor script to invoke
locale:
languages:
en:
articles: []
prepositions: []
strings: {}
verbs: []
Items in the word lists may use translation keys from strings
.
The strings
section of each language is a [string, string | nested]
dictionary. Values may be strings, or nested
dictionaries, whose keys may be strings, or further dictionaries.
Translation strings for similar messages (using an item, using an actor, and a missing use target) should be grouped under a common key.
For example:
languages:
en:
strings:
meta:
create: 'created new world {{state.name}} ({{state.id}}) from {{world}} with seed of {{seed}} and room depth of {{depth}}'
debug:
missing: 'no world state to debug'
graph:
missing: 'no world state to graph'
help: 'available verbs: {{verbs}}'
load:
missing: 'no world states loaded from {{-path}}'
state: 'loaded world state {{meta.id}} from {{-path}}'
quit: 'quitting'
save:
missing: 'no world state to save'
state: 'saved world state {{meta.id}} from {{-path}}'
step:
missing: 'please create a world before using any verbs'
world: '{{name}} ({{id}})'
A list of possible player actor templates. One of these will be selected and created in the start room for each player.
A list of possible start room templates. One of these will be selected and created, then other rooms created as the starting room's portals are populated. New player actors will be placed in the start room.
Template field types correspond to the entity field's type. That is, a string like name
or slot
will be
created from a template string, and a number like stats
from a template number.
Each template has metadata, missing the template
field that exists in entity metadata.
id
- literal string, not templated
name
- short display name
- a template string
desc
- longer description
- a template string
For example:
meta:
id: goblin
name:
base: Goblin
desc:
base: (slimy|smelly) goblin
Template numbers define a range [min, max)
and select a random integer within that.
min
- minimum value, inclusive
- number
max
- maximum value, exclusive
- number
step
- interval between values
- number
- optional, defaults to 1
For example:
stats: !map
health:
min: 10
max: 20
step: 5 # produces 10, 15, or 20
When templates need to include one another, they can refer to the id
of the other template.
The chance
of each template being created is a number in [0, 100]
, where 0 will never be created, and 100 will
always be created. The chance for each template is rolled individually, creating zero or more entities.
For example:
items:
- id: item-sword
chance: 25
When templates need to use a script, they can refer to the name
and pass some data. The script name
must be
recognized by the script service. The data
will be merged with existing data and passed on to the script.
data
- additional data to pass
- values may be template numbers or strings
- a
[string, number | string]
map
name
- a template string
For example:
scripts: !map
signal.get:
data: !map {}
name:
base: signal-actor-get
verbs.common.look:
data: !map {}
name:
base: verb-actor-look
Template strings use a series of nested lists, alternating between AND and OR operators, to produce the final string. The whole string starts with the AND operator to join words, so parenthesized groups start with OR, then AND, and so on. Items are split on whitespace and joined with spaces.
The template (gross|slimy) goblin
becomes [[gross OR slimy] AND goblin]
, which will resolve
to one of gross goblin
or slimy goblin
.
For example:
meta:
desc:
base: (gross|slimy) goblin
Each type of entity has a corresponding template, with fields replaced by numeric ranges, template strings, and nested references to other templates.
Actor templates have metadata and scripts, act as a container for items (inventory), and store some numeric stats.
meta
- template metadata
flags
- arbitrary data, short tags
- a
[string, string]
map
items
- a list of item template references
scripts
- signal and verb scripts
- a
[string, string]
map
slots
- equipment slots
- a
[string, string]
map
stats
- actor statistics (health, stamina, etc)
- a
[string, number]
map
Item templates have metadata and scripts, have custom verbs, and store some numeric stats.
meta
- template metadata
flags
- arbitrary data, short tags
- a
[string, string]
map
scripts
- event scripts with name and data
- a
[string, script]
map
slot
- filter for slots into which this item can be equipped
- a string template
stats
- item statistics (health, damage, etc)
- a
[string, number]
map
Room templates have metadata and scripts, have custom verbs, and act as a container for actors, items, and portals.
meta
- template metadata
actors
- list of actor template references
flags
- arbitrary data, short tags
- a
[string, string]
map
items
- list of item template references
portals
- list of portal template references
scripts
- event scripts with name and data
- a
[string, script]
map
Rooms are linked together through portals.
Portals have source and target groups, and the engine attempts to link them by name, within the appropriate groups.
meta
- template metadata
dest
- destination room ID
- a template string
- portals may be linked to existing rooms, which uses the
group
rather thandest
flags
- arbitrary data, short tags
- a
[string, string]
map
group
- how this portal will be linked to other rooms
- a complex type:
key
- the name of the group
- a template string
source
- the side of the room with this portal
- a template string
target
- the side of the room with the target portal
- a template string
scripts
- event scripts with name and data
- a
[string, script]
map
stats
- item statistics (closed, locked, etc)
- a
[string, number]
map
Two portals in the same room and source group will be linked to the same destination room, and portals of the same names, within the designated target group. If a matching portal cannot be found, one may be added to the room.
For example, with two rooms linked by two ports:
Two rooms, room-north
and room-south
, where each room has two portals, in the door
and window
groups:
portals:
- base:
meta:
id:
base: portal-door-north
dest:
base: room-north
group:
key:
base: door
source:
base: north
target:
base: south
- base:
meta:
id:
base: portal-window-north
dest:
base: room-north
group:
key:
base: window
source:
base: north
target:
base: south
- base:
meta:
id:
base: portal-door-south
dest:
base: room-south
group:
key:
base: door
source:
base: south
target:
base: north
- base:
meta:
id:
base: portal-window-south
dest:
base: room-south
group:
key:
base: window
source:
base: south
target:
base: north
rooms:
- base:
meta:
id:
base: room-south
portals:
- id: portal-door-north
- id: portal-window-north
- base:
meta:
id:
base: room-north
portals:
- id: portal-door-south
- id: portal-window-south
Note the dest
changes to the other room, and the sourceGroup
and targetGroup
are reversed.
This will produce a pair of rooms with two bidirectional links, like:
+----------+ +----------+
| | door | |
| +<-------->+ |
| room-0 | | room-1 |
| +<-------->+ |
| | window | |
+----------+ +----------+