docs: Description of jenny package (#2102)

Adding preliminary description for the jenny project
This commit is contained in:
Pasha Stetsenko
2022-12-15 22:57:06 -08:00
committed by GitHub
parent 5151170c73
commit a99c930381
56 changed files with 1502 additions and 130 deletions

View File

@ -12,6 +12,7 @@ Klingsbo
Lukas
Nakama
Patreon
Prosser
Skia
Spritecow
Tiled
@ -28,6 +29,7 @@ tavian
trex
wolfenrain
xaha
yarnspinner
рушниці
рушниць
рушниця

View File

@ -130,6 +130,7 @@ subfolder
subfolders
sublist
sublists
subrange
tappable
tappables
tileset

View File

@ -1,4 +1,9 @@
deflist
dollarmath
linkcheck
markdownlint
seealso
smartquotes
tasklist
toctree
toctrees

View File

@ -67,7 +67,7 @@ and it will be automatically reflected in the PR.
## Open an issue and fork the repository
- If it is a bigger change or a new feature, first of all
[file a bug or feature report][GitHub issues], so that we can discuss what direction to follow.
[file a bug or feature report][GitHub issue], so that we can discuss what direction to follow.
- [Fork the project][fork guide] on GitHub.
- Clone the forked repository to your local development machine
(e.g. `git clone git@github.com:<YOUR_GITHUB_USER>/flame.git`).
@ -196,11 +196,11 @@ There are a few things to think about when doing a release:
- Create a PR containing the updated changelog and `pubspec.yaml` files.
[GitHub issue]: https://github.com/flame-engine/flame/issues/new
[GitHub issues]: https://github.com/flame-engine/flame/issues/new
[GitHub issue]: https://github.com/flame-engine/flame/issues
[GitHub issues]: https://github.com/flame-engine/flame/issues
[PRs]: https://github.com/flame-engine/flame/pulls
[fork guide]: https://guides.github.com/activities/forking/#fork
[Discord]: https://discord.gg/pxrBmy4
[fork guide]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
[Discord]: https://discord.com/invite/pxrBmy4
[Melos]: https://github.com/invertase/melos
[pubspec doc]: https://dart.dev/tools/pub/pubspec
[conventional commit]: https://www.conventionalcommits.org

View File

@ -4,6 +4,7 @@
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import docutils
import html
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
@ -27,22 +28,24 @@ extensions = [
'sphinxcontrib.mermaid',
'extensions.flutter_app',
'extensions.package',
'extensions.yarn_lexer',
]
# Configuration options for MyST:
# https://myst-parser.readthedocs.io/en/latest/syntax/optional.html
myst_enable_extensions = [e.lower() for e in [
myst_enable_extensions = [
'attrs_image',
'colon_fence',
'dollarMath',
'deflist',
'dollarmath',
'html_admonition',
'html_image',
'linkify',
'replacements',
'smartQuotes',
'smartquotes',
'strikethrough',
'substitution',
'taskList',
]]
'tasklist',
]
# Auto-generate link anchors for headers at levels H1 and H2
myst_heading_anchors = 4
@ -52,6 +55,11 @@ myst_heading_anchors = 4
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder
linkcheck_ignore = [
r'https://examples.flame-engine.org/#/.*',
r'https://pub.dev/documentation/flame/--VERSION--/',
]
# -- Options for HTML output -------------------------------------------------
@ -71,6 +79,7 @@ pygments_style = 'monokai'
html_static_path = ['images', 'scripts', 'theme']
html_js_files = ['versions.js', 'menu-expand.js']
# -- Custom setup ------------------------------------------------------------
class TitleCollector(docutils.nodes.SparseNodeVisitor):
def __init__(self, document):
@ -105,19 +114,17 @@ def get_local_toc(document):
"First title on the page is not <h1/>")
del titles[0] # remove the <h1> title
h1_seen = False
ul_level = 0
html_text = "<div id='toc-local' class='list-group'>\n"
html_text += " <div class='header'><i class='fa fa-list'></i> Contents</div>\n"
for title, node_id, level in titles:
if level <= 1:
return document.reporter.error("More than one <h1> title on the page")
html_text += f" <a href='#{node_id}' class='list-group-item level-{level-1}'>{title}</a>\n"
html_text += f" <a href='#{node_id}' class='list-group-item level-{level-1}'>" \
f"{html.escape(title)}</a>\n"
html_text += "</div>\n"
return html_text
# Emitted when the HTML builder has created a context dictionary to render
# a template with this can be used to add custom elements to the context.
def on_html_page_context(app, pagename, templatename, context, doctree):

View File

@ -0,0 +1,26 @@
/* Single-line comment */
.highlight-yarn span.c {
color: #505050;
}
/* Character name at the start of a line */
.highlight-yarn span.nt {
color: #10d576;
}
/* Variable */
.highlight-yarn span.nv {
color: #80a4da;
}
/* Hashtag */
.highlight-yarn span.ch {
color: #1e6557;
font-style: italic;
}
/* Markup */
.highlight-yarn span.sb {
color: #816417;
}

View File

@ -0,0 +1,198 @@
import os
import shutil
from pygments.lexer import RegexLexer, bygroups, default, include, words
from pygments.token import *
class YarnLexer(RegexLexer):
name = 'YarnSpinner'
aliases = ['yarn', 'jenny']
filenames = ['*.yarn']
mimetypes = ['text/yarn']
CONSTANTS = [
'Bool',
'Numeric',
'String',
'false',
'true',
]
OPERATORS = [
'and',
'as',
'eq',
'ge',
'gt',
'gte',
'is',
'le',
'lt',
'lte',
'ne',
'neq',
'not',
'or',
'to',
'xor',
]
BUILTIN_COMMANDS = [
'character',
'declare',
'else',
'elseif',
'embed',
'endif',
'if',
'jump',
'local',
'stop',
'visit',
'wait',
]
BUILTIN_FUNCTIONS = [
'bool',
'ceil',
'dec',
'decimal',
'dice',
'floor',
'inc',
'int',
'number',
'plural',
'random',
'random_range',
'round',
'round_places',
'string',
'visit_count',
'visited',
]
tokens = {
'root': [
include('<whitespace>'),
include('<commands>'),
(r'#[^\n]*\n', Comment.Hashbang),
(r'---+\n', Punctuation, 'node_header'),
default('node_header'),
],
'<whitespace>': [
(r'\s+', Whitespace),
(r'//.*\n', Comment),
],
'node_header': [
include('<whitespace>'),
(r'---+\n', Punctuation, 'node_body'),
(r'(title)(:)(.*?\n)', bygroups(Name.Builtin, Punctuation, Name.Variable)),
(r'(\w+)(:)(.*?\n)', bygroups(Name, Punctuation, Text)),
default('node_body'),
],
'node_body': [
(r'===+\n', Punctuation, '#pop:2'),
include('<whitespace>'),
default('line_start'),
],
'line_start': [
(r'\s+', Whitespace),
(r'->', Punctuation),
(r'(\w+)(\s*:\s*)', bygroups(Name.Tag, Punctuation)),
default('line_continue'),
],
'line_continue': [
(r'\n', Whitespace, '#pop:2'),
(r'\s+', Whitespace),
(r'//[^\n]*', Comment),
(r'\\[\[\]\\{}<>/:#]', String.Escape),
(r'\\\n\s*', String.Escape),
(r'\\.', Error),
(r'\{', Punctuation, 'curly_expression'),
include('<commands>'),
include('<markup>'),
(r'#[^\s]+', Comment.Hashbang),
(r'[<>/]', Text),
(r'[^\n\\\[\]{}<>/#]+', Text),
(r'.', Text), # just in case
],
'<commands>': [
(r'<<', Punctuation, 'command_name'),
],
'command_name': [
(words(BUILTIN_COMMANDS, suffix=r'\b'), Keyword, 'command_body'),
(r'\w+', Name.Class, 'command_body'),
],
'command_body': [
include('<whitespace>'),
(r'\{', Punctuation, 'curly_expression'),
(r'>>', Punctuation, '#pop:2'),
(r'>', Text),
default('command_expression'),
],
'<expression>': [
(r'\n', Error),
(r'\s+', Whitespace),
(r'(//.*)(\n)', bygroups(Comment, Error)),
(words(BUILTIN_FUNCTIONS, suffix=r'\b'), Name.Builtin),
(words(CONSTANTS, suffix=r'\b'), Name.Builtin),
(words(OPERATORS, suffix=r'\b'), Operator),
(r'\$\w+', Name.Variable),
(r'([+\-*/%><=]=?)', Operator),
(r'\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?', Number),
(r'[(),]', Punctuation),
(r'"', String.Delimeter, 'string'),
(r'\w+', Name.Function),
(r'.', Error),
],
'command_expression': [
(r'>>', Punctuation, '#pop:3'),
include('<expression>'),
],
'curly_expression': [
(r'\}', Punctuation, '#pop'),
include('<expression>'),
],
'string_expression': [
(r'\}', String.Interpol, '#pop'),
include('<expression>'),
],
'<markup>': [
(r'\[', String.Backtick, 'markup_start'),
],
'markup_start': [
(r'/?\w+', String.Backtick, 'markup_body'),
(r'/?\]', String.Backtick, '#pop'),
],
'markup_body': [
(r'/?\]', String.Backtick, '#pop:2'),
(r'[^/\]]+', String.Backtick),
],
'string': [
(r'"', String.Delimeter, '#pop'),
(r'{', String.Interpol, 'string_expression'),
(r'[^"{\n\\]+', String),
(r'\\[nt"{}]', String.Escape),
(r'\n', Error),
],
}
def setup(app):
base_dir = os.path.dirname(__file__)
target_dir = os.path.abspath('../_build/html/_static/')
os.makedirs(target_dir, exist_ok=True)
shutil.copy(os.path.join(base_dir, 'yarn_lexer.css'), target_dir)
app.add_css_file('yarn_lexer.css')
app.add_lexer('yarn', YarnLexer)
return {
'parallel_read_safe': True,
'parallel_write_safe': True,
'env_version': 1,
}

View File

@ -1,5 +1,5 @@
linkify-it-py==2.0.0
myst-parser==0.18.0
myst-parser==0.18.1
Pygments==2.12.0
Sphinx==5.0.2
sphinxcontrib-mermaid==0.7.1

View File

@ -40,6 +40,10 @@ a:hover {
color: #ddbb99;
}
a:hover code {
color: white;
}
a.reference.external {
word-wrap: break-word;
}
@ -75,7 +79,7 @@ div.expander {
flex: 1;
}
p + ul {
p + ul, p + ol {
margin-top: -0.5em;
}
@ -768,7 +772,7 @@ div.admonition.warning {
--admonition-border-color: orange;
--admonition-icon: '\f071';
--admonition-icon-color: gold;
--admonition-title-background-color: #503e1a;
--admonition-title-background-color: #a16820;
}
div.admonition.error {

View File

@ -67,6 +67,7 @@ flame_bloc <flame_bloc/flame_bloc.md>
flame_fire_atlas <flame_fire_atlas/flame_fire_atlas.md>
flame_forge2d <flame_forge2d/flame_forge2d.md>
flame_isolate <flame_isolate/flame_isolate.md>
flame_lottie <flame_lottie/flame_lottie.md>
flame_oxygen <flame_oxygen/flame_oxygen.md>
flame_rive <flame_rive/flame_rive.md>
flame_splash_screen <flame_splash_screen/flame_splash_screen.md>

View File

@ -1,6 +1,6 @@
# flame_bloc
`flame_bloc` is a bridge library for using [Bloc](https://bloclibrary.dev/#/) in your Flame
`flame_bloc` is a bridge library for using [Bloc](https://bloclibrary.dev/) in your Flame
game. `flame_bloc` offers a simple and natural (as in similar to flutter_bloc) way to use blocs and
cubits inside a FlameGame. Bloc offers way to make game state changes predictable by regulating when
a game state change can occur and offers a single way to change game state throughout an entire

View File

@ -6,7 +6,7 @@
## FlameBlocProvider
FlameBlocProvider is a Component which creates and provides a bloc to its children.
The bloc will only live while this component is alive.It is used as a dependency injection (DI)
The bloc will only live while this component is alive. It is used as a dependency injection (DI)
widget so that a single instance of a bloc can be provided to multiple Components within a subtree.
FlameBlocProvider should be used to create new blocs which will be made available to the rest of the

View File

@ -122,5 +122,5 @@ example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/br
Just like with normal `PositionComponent`s you can make the `Forge2DCamera` follow `BodyComponent`s
by calling `camera.followBodyComponent(...)` which works the same as
[camera.followComponent](../flame/camera_and_viewport.md#camerafollowcomponent). When you want to
[camera.followComponent](../../flame/camera_and_viewport.md#camerafollowcomponent). When you want to
stop following a `BodyComponent` you should call `camera.unfollowBodyComponent`.

View File

@ -1,7 +1,7 @@
# FlameIsolate
The power of [integral_isolates](https://pub.dev/packages/integral_isolates) neatly packaged in
[flame_isolates](https://pub.dev/packages/flame_isolates) for your Flame game.
[flame_isolate](https://pub.dev/packages/flame_isolate) for your Flame game.
If you've ever used the [compute](https://api.flutter.dev/flutter/foundation/compute-constant.html)
function before, you will feel right at home. This mixin allows you to run CPU-intensive code in an

View File

@ -224,5 +224,5 @@ The following style rules generally apply when writing documentation:
[effective dart]: https://dart.dev/guides/language/effective-dart
[flutter documentation guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc
[flutter documentation guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#user-content-documentation-dartdocs-javadocs-etc
[documentation]: documentation.md

View File

@ -5,7 +5,7 @@ other. For example an arrow hitting an enemy or the player picking up a coin.
In most collision detection systems you use something called hitboxes to create more precise
bounding boxes of your components. In Flame the hitboxes are areas of the component that can react
to collisions (and make [gesture input](inputs/gesture-input.md#gesturehitboxes)) more accurate.
to collisions (and make [gesture input](inputs/gesture_input.md#gesturehitboxes)) more accurate.
The collision detection system supports three different types of shapes that you can build hitboxes
from, these shapes are Polygon, Rectangle and Circle. Multiple hitbox can be added to a component to
@ -129,7 +129,7 @@ and two `RectangleHitbox`s as its hat.
A hitbox can be used either for collision detection or for making gesture detection more accurate
on top of components, see more regarding the latter in the section about the
[GestureHitboxes](inputs/gesture-input.md#gesturehitboxes) mixin.
[GestureHitboxes](inputs/gesture_input.md#gesturehitboxes) mixin.
### CollisionType

View File

@ -1170,7 +1170,7 @@ class Hud extends PositionComponent with HasGameRef {
`FlameGame`, to help with that Flame provides a `ComponentsNotifierBuilder` widget.
To see an example of its use check the running example
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/components/components_notifier_example.dart);
[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/components_notifier_example.dart).
## ClipComponent

View File

@ -30,6 +30,8 @@ the [](#fpstextcomponent).
### FpsTextComponent
The `FpsTextComponent` is simply a [](../rendering/text.md#textcomponent) that wraps an
[](../rendering/text.md#textcomponent), since you most commonly want to show the current FPS
somewhere when you the [](#fpscomponent) is used.
The `FpsTextComponent` is simply a [TextComponent] that wraps an `FpsComponent`, since you most
commonly want to show the current FPS somewhere when the `FpsComponent` is used.
[TextComponent]: ../rendering/text_rendering.md#textcomponent

View File

@ -0,0 +1,128 @@
# Jenny
The **jenny** library is a toolset for adding *dialogue* into a game. The dialogue may be quite
complex, including user-controlled interactions, branching, dynamically-generated content, commands,
markup, state controlled either from Jenny or from the game, custom functions and commands, etc.
The `jenny` library is an unofficial port of the [Yarn Spinner] library for Unity. The name of the
library comes from [spinning jenny], a kind of yarn-spinning machine.
Adding dialogue into any game generally consists of two stages:
1. Writing the text of the dialogue;
2. Interactively displaying it within the game.
With `jenny`, these two tasks are completely separate, allowing the creation of game content and
development of the game engine to be independent.
[Yarn Spinner]: https://docs.yarnspinner.dev/
[spinning jenny]: https://en.wikipedia.org/wiki/Spinning_jenny
## Writing dialogue
In `jenny`, the dialogue is written in plain text and stored in `.yarn` files that are added
to the game as assets. The `.yarn` file format is developed by the authors of [Yarn Spinner], and
is specifically designed for writing dialogue.
The simplest form of the yarn dialogue looks like a play:
```yarn
title: Scene1_Gregory_and_Sampson
---
Sampson: Gregory, on my word, we'll not carry coals.
Gregory: No, for then we should be colliers.
Sampson: I mean, an we be in choler, we'll draw.
Gregory: Ay, while you live, draw your neck out of collar.
Sampson: I strike quickly being moved.
Gregory: But thou art not quickly moved to strike.
===
```
This simple exchange, when rendered within a game, will be shown as a sequence of phrases spoken
in turn by the two characters. The `DialogRunner` will allow you to control whether the dialogue
proceeds automatically or requires "clicking-through" by the user.
The `.yarn` format supports many more advanced features too, allowing the dialogue to proceed
non-linearly, supporting variables and conditional execution, giving the player an ability to
select their response, etc. Most importantly, the format is so intuitive that it can be generally
understood without having to learn it:
```yarn
title: Slughorn_encounter
---
<<if visited("Horcrux_question")>>
Slughorn: Sorry, Tom, I don't have time right now.
<<stop>>
<<endif>>
Slughorn: Oh hello, Tom, is there anything I can help you with?
Tom: Good {time_of_day()}, Professor.
-> I was curious about the 12 uses of the dragon blood.
Slughorn: Such an inquisitive mind! You can read about that in the "Most \
Potente Potions" in the Restricted Section of the library.
<<give restricted_library_pass>>
Tom: Thank you, Professor, this is very munificent of you.
-> I wanted to ask... about Horcruxes <<if $knows_about_horcruxes>>
<<jump Horcrux_question>>
-> I just wanted to say how much I always admire your lectures.
Slughorn: Thank you, Tom. I do enjoy flattery, even if it is well-deserved.
===
title: Horcrux_question
---
Slughorn: Where... did you hear that?
-> Tom: It was mentioned in an old book in the library...
Slughorn: I see that you have read more books from the Restricted Section \
than is wise.
Slughorn: I'm sorry, Tom, I should have seen you'd be tempted...
<<take restricted_library_pass>>
-> But Professor!..
Slughorn: This is for your good, Tom. Many of those books are dangerous!
Slughorn: Now off you go. And do your best to forget about what you \
asked...
<<stop>>
-> Tom: I overheard it... And the word felt sharp and frigid, like it was the \
embodiment of Dark Art <<if luck() >= 80>>
Slughorn: It is a very Dark Art indeed, it is not good for you to know \
about it...
Tom: But if I don't know about this Dark Art, how can I defend myself \
against it?
Slughorn: It is a Ritual, one of the darkest known to wizard-kind ...
...
<<achievement "The Darkest Secret">>
===
```
This fragment demonstrates many of the features of the `.yarn` language, including:
- ability to divide the text into smaller chunks called *nodes*;
- control the flow of the dialog via commands such as `<<if>>` or `<<jump>>`;
- different dialogue path depending on player's choices;
- disable certain menu choices dynamically;
- keep state information in variables;
- user-defined functions (`time_of_day`, `luck`) and commands (`<<give>>`, `<<take>>`).
For more information, see the [Yarn Language](language/language.md) section.
## Using the dialogue in a game
By itself, the `jenny` library does not integrate with any game engine. However, it provides a
runtime that can be used to build such an integration. This runtime consists of the following
components:
- [`YarnProject`](runtime/yarn_project.md) -- the central repository of information, which knows
about all your yarn scripts, variables, custom functions and commands, settings, etc.
- [`DialogueRunner`](runtime/dialogue_runner.md) -- an executor that can run a specific dialogue
node. This executor will send the dialogue lines into one or more `DialogueView`s.
- [`DialogueView`](runtime/dialogue_view.md) -- an abstract interface describing how the dialogue
will be presented to the end user. Implementing this interface is the primary way of integrating
`jenny` into a specific environment.
```{toctree}
:hidden:
YarnSpinner language <language/language.md>
Jenny API <runtime/index.md>
```

View File

@ -0,0 +1,16 @@
# Commands
The **commands** are special instructions surrounded with double angle-brackets: `<<stop>>`. There
are both built-in and user-defined commands.
```{toctree}
:hidden:
<<declare>> <declare.md>
<<if>> <if.md>
<<jump>> <jump.md>
<<set>> <set.md>
<<stop>> <stop.md>
<<wait>> <wait.md>
```

View File

@ -0,0 +1,3 @@
# `<<declare>>`
TODO

View File

@ -0,0 +1,21 @@
# `<<if>>`
The **\<\<if\>\>** command evaluates its condition, and based on that decides which statements to
execute next. This command may have multiple parts, which look as following:
```yarn
<<if condition1>>
statements1...
<<elseif condition2>>
statements2...
<<else>>
statementsN...
<<endif>>
```
The `<<elseif>>`s and the `<<else>>` are optional, whereas the final `<<endif>>` is mandatory. Also,
notice that the statements within each block are indented.
During the runtime, the conditions within each `<<if>>` and `<<elseif>>` blocks will be evaluated
in turn, and the first one which evaluates to `true` will have its statements executed next. These
conditions must be boolean.

View File

@ -0,0 +1,16 @@
# `<<jump>>`
The **\<\<jump\>\>** command unconditionally moves the execution pointer to the start of the target
node. The target node will now become "current":
```yarn
<<jump FarewellScene>>
```
The argument of this command is the id of the node to jump to. It can be given in plain text if the
name of the node is a valid ID. If not, you can use the text interpolation syntax to supply an
arbitrary node id (which includes dynamically defined node ids):
```yarn
<<jump {"Farewell Scene"}>>
```

View File

@ -0,0 +1,3 @@
# `<<set>>`
TODO

View File

@ -0,0 +1,4 @@
# `<<stop>>`
The **\<\<stop\>\>** command is very simple: it immediately stops evaluating the current node, as if
we reached the end of the dialogue. This command takes no arguments.

View File

@ -0,0 +1,8 @@
# `<<wait>>`
The **\<\<wait\>\>** command forces the engine to wait the provided number of seconds before
proceeding with the dialogue.
```yarn
<<wait 1.5>>
```

View File

@ -0,0 +1,3 @@
# Expressions
TODO

View File

@ -0,0 +1,82 @@
# YarnSpinner language
**YarnSpinner** is the language in which `.yarn` files are written. You can check out the
[official documentation] for the YarnSpinner language, however, here we will be describing the
**Jenny** implementation, which may not contain all the original features, but may also contain
some that were not implemented in the YarnSpinner yet.
[official documentation]: https://docs.yarnspinner.dev/getting-started/writing-in-yarn
## Yarn files
Any Yarn project will contain one or more `.yarn` files. These are plain text files in UTF-8
encoding. As such, they can be edited in any text editor or IDE.
Having multiple `.yarn` files helps you better organize your project, but Jenny doesn't impose any
requirements on the number of files or their relationship.
Each `.yarn` file may contain **comments**, **tags**, **[commands]**, and **[nodes]**.
For example:
```yarn
// This is a comment
// The line below, however, is a tag:
# Chapter 1d
<<declare $visited_graveyard = false>>
<<declare $money = 25>> // is this too much?
title: Start
---
// Node content
===
```
[commands]: commands/commands.md
[nodes]: nodes.md
### Comments
A comment starts with `//` and continues until the end of the line. All the text inside a comment
will be completely ignored by Jenny as if it wasn't there.
There are no multi-line comments in YarnSpinner.
### Tags
File-level tags start with a `#` and continue until the end of the line. A tag can be used to
include some per-file custom project metadata. These tags are not interpreted by Jenny in any way.
### Commands
The commands are explained in more details [later][commands], but at this point it is
worth pointing out that only a limited number of commands are allowed at the root level of a file
(that is, outside of nodes). Currently, these commands are:
- `<<declare>>`
- `<<character>>`
The commands outside of nodes are compile-time instructions, that is they are executed during the
compilation of a YarnProject.
### Nodes
Nodes represent the main bulk of content in a yarn file, and are explained in a dedicated
[section][nodes]. There could be multiple nodes in a single file, placed one after another.
No special separator is needed between nodes: as soon as one node ends, the next one can begin.
```{toctree}
:hidden:
Nodes <nodes.md>
Lines <lines.md>
Options <options.md>
Commands <commands/commands.md>
Expressions <expressions.md>
```

View File

@ -0,0 +1,186 @@
# Lines
A **line** is the most common element of the Yarn dialogue. It's just a single phrase that a
character in the game says. In a `.yarn` file, a **line** is represented by a single line of text
in a [node body]. A line may contain the following elements:
- A character ID;
- Normal text;
- Escaped text;
- Interpolated expressions;
- Markup;
- Hashtags;
- A comment at the end of the line;
- (a line, however, cannot contain commands).
A **line** is represented with the [DialogueLine] class in Jenny runtime.
[node body]: nodes.md#body
[DialogueLine]: ../runtime/dialogue_line.md
## Character ID
If a line starts with a single word followed by a `:`, then that word is presumed to be the name
of the character who is speaking that line. In the following example there are two characters
talking to each other: Prosser and Ford, and the last line has no character ID.
```yarn
title: Bulldozer_Conversation
---
Prosser: You want me to come and lie there...
Ford: Yes
Prosser: In front of the bulldozer?
Ford: Yes
Prosser: In the mud.
Ford: In, as you say, the mud.
(low rumbling noise...)
===
```
It is worth emphasizing that a character ID must be a valid ID -- that is, it cannot contain
spaces or other special characters. In the example below "Harry Potter" is not a valid character ID,
while all other alternatives are ok.
```yarn
title: Hello
---
Harry Potter: Hello, Hermione!
Harry_Potter: Hello, Hermione!
HarryPotter: Hello, Hermione!
Harry: Hello, Hermione!
===
```
If you want to have a line that starts with a `WORD + ':'`, but you don't want that word to be
interpreted as a character name, then the colon can be [escaped](#escaped-text):
```yarn
title: Warning
---
Attention\: The cake is NOT a lie
===
```
## Interpolated expressions
You can insert dynamic text into a line with the help of **interpolated expression**s. These
expressions are surrounded with curly braces `{}`, and everything inside the braces will be
evaluated, and then the result of the evaluation will be inserted into the text.
```yarn
title: Greeting
---
Trader: Hello, {$player_name}! Would you like to see my wares?
Player: I have only {plural($money, "% coin")}, do you have anything I can afford?
===
```
The expressions will be evaluated at runtime when the line is delivered, which means it can produce
different text during different runs of the line.
```yarn
title: Exam_Greeting
---
<<if $n_attempts == 0>>
Professor: Welcome to the exam!
<<jump Exam>>
<<elseif $n_attempts < 5>>
Professor: You have tried {plural($n_attempts, "% time")} already, but I \
can give you another try.
<<jump Exam>>
<<else>>
Professor: You've failed 5 times in a row! How is this even possible?
<<endif>>
===
```
After evaluation, the text of the expression will be inserted into the line as-is, without any
further processing. Which means that the text of the expression may contain special characters
(such as `[`, `]`, `{`, `}`, `\`, etc), and they don't need to be escaped. It also means that the
expression cannot contain markup, or produce a hashtag, etc.
Read more about expressions in the [Expressions](expressions.md) section.
## Markup
The **markup** is a mechanism for text annotation. It is somewhat similar to HTML tags, except that
it uses square brackets `[]` instead of angular ones:
```yarn
title: Markup
---
Wizard: No, no, no! [em]This is insanity![/em]
===
```
The markup tags do not alter the text of the line, they merely insert annotations in it. Thus, the
line above will be delivered in game as "No, no, no! This is insanity!", however there will be
additional information attached to the line that shows that the last 17 characters were marked with
the `em` tag.
Markup tags can be nested, or be zero-width, they can also include parameters whose values can be
dynamic. Read more about this in the [Markup](markup.md) document.
## Hashtags
Hashtags may appear at the end of the line, and take the following form: `#text`. That is, a hashtag
is a `#` symbol followed by any text that doesn't contain whitespace.
Hashtags are used to add line-level metadata. There can be no line content after a hashtag (though
comments are allowed). A line can have multiple hashtags associated with it.
```yarn
title: Hashtags
---
Harry: There is no justice in the laws of Nature, Headmaster, no term for \
fairness in the equations of motion. #sad // HPMOR.39
Harry: The universe is neither evil, nor good, it simply does not care.
Harry: The stars don't care, or the Sun, or the sky.
Harry: But they don't have to! We care! #elated #volume:+1
Harry: There is light in the world, and it is us! #volume:+2
===
```
In most cases the Jenny engine does not interpret the tags, but merely stores them as part of the
line information. It is up to the programmer to examine these tags at runtime.
## Escaped text
Whenever you have a line that needs to include a character that would normally be interpreted as
one of the special syntaxes mentioned above, then such a character can be **escaped** with a
backslash `\`.
The following escape sequences are recognized: `\\`, `\/`, `\#`, `\<`, `\>`, `\[`, `\]`, `\{`, `\}`,
`\:`, `\-`, `\n`. In addition, there is also `\⏎` (i.e. backslash followed immediately by a
newline).
```yarn
title: Escapes
---
\// This is not a comment // but this is
This is not a \#hashtag
This is not a \<<command>>
\{This line\} does not contain an expression
Not a \[markup\]
===
```
The `\⏎` escape can be used to split a single long line into multiple physical lines, that would
still be treated by Jenny as if it was a single line. This escape sequence consumes both the
newline symbol and all the whitespace at the start of the next line:
```yarn
title: One_long_line
---
This line is so long that it becomes uncomfortable to read in a text editor. \
Therefore, we use the backslash-newline escape sequence to split it into \
several physical lines. The indentation at the start of the continuation \
lines is for convenience only, and will be removed from the resulting \
text.
===
```

View File

@ -0,0 +1,3 @@
# Markup
TODO

View File

@ -0,0 +1,89 @@
# Nodes
A **node** is a small section of text, that represents a single conversation or interaction with
an NPC. Each node has a **title**, which can be used to *run* that node in a [DialogueRunner], or to
[jump] to that node from another node.
You can think of a node as if it was a function in a regular programming language. Running a node
is equivalent to calling a function, and it is not possible to start execution in the middle of a
node/function. When a function becomes too large, we will usually want to split it into multiple
smaller ones -- the same is true for nodes, when a node becomes too long it is a good idea to split
it into several smaller nodes.
Each node consists of a **header** and a **body**. The header is separated from the body with 3
(or more) dashes, and the body is terminated with 3 "=" signs:
```yarn
// NODE HEADER
---
// NODE BODY
===
```
In addition, you can use 3 (or more) dashes to separate the header from the previous content, which
means the following is also a valid node:
```yarn
---------------
// NODE HEADER
---------------
// NODE BODY
===
```
A **node** is represented with a [Node] class in Jenny runtime.
[Node]: ../runtime/node.md
## Header
The header of a node consists of one or more lines of the form `TAG: CONTENT`. One of these lines
must contain the node's **title**, which is the name of the node:
```yarn
title: NodeName
```
The title of a node must be a valid ID (that is, starts with a letter, followed by any number of
letters, digits, or underscores). All nodes within a single project must have unique titles.
Besides the title, you can add any number of extra tags into the node's header. Jenny will store
these tags with the node's metadata, but will not interpret them in any other way. You will then
be able to access these tags programmatically
```yarn
title: Alert
colorID: 0
modal: true
---
WARNING\: Entering Radioactive Zone!
===
```
## Body
The body of a node is where the dialogue itself is located. The body is just a sequence of
statements, where each statement is either a [Line], an [Option], or a [Command]. For example:
```yarn
title: Gloomy_Morning
camera_zoom: 2
---
You : Good morning!
Guard: You call this good? 'Tis as crappy as could be
You : Why, what happened?
Guard: Don't you see the fog? Chills me through to the bones
You : Sorry to hear that...
You : So, can I pass?
Guard: Can I get some exercise cutting you into pieces? Maybe that'll warm me up!
You : Ok, I think I'll be going. Hope you feel better soon!
===
```
[DialogueRunner]: ../runtime/dialogue_runner.md
[jump]: commands/jump.md
[Line]: lines.md
[Option]: options.md
[Command]: commands/commands.md

View File

@ -0,0 +1,62 @@
# Options
**Options** are special lines that display a menu of choices for the player, and the player must
select one of them in order to continue. The options are indicated with an arrow `->` at the start
of the line:
```yarn
title: Adventure
---
You arrive at the edge of the forest. The road dives in, but there is another \
one going around the edge.
-> Go straight ahead, on the beaten path (x)
-> Take the road along the forest's edge
-> Turn back
===
```
An option is typically followed by an indented list of statements (which may, again, be lines,
options, or commands). These statements indicate how the dialogue should proceed if the player
chooses that particular option. After the control flow finishes running through the block
corresponding to the selected option, the dialogue resumes after the option set.
Other than the arrow indicator, an option follows the same syntax as the [line]. Thus, it can have
a character name, the main text, interpolated expressions, markup, and hashtags. One additional
feature that an option can have is the **conditional**. A conditional is a short-form `<<if>>`
command after the text of an option (but before the hashtags):
```yarn
title: Bridge
---
Guard: 50 coins and you can cross the bridge.
-> Alright, take the money <<if $gold >= 50>>
<<take gold 50>>
<<grant bridge_pass>>
-> I have so much money, here, take a 100 <<if $gold >= 10000>>
<<take gold 100>>
<<grant bridge_pass>>
Guard: Wow, so generous!
Guard: But I wouldn't recommend going around telling everyone that you \
have "so much money"
-> That's too expensive!
Guard: Is it? My condolences
-> How about I [s]kick your butt[/s] instead?
<<if $power < 1000>>
<<fight>>
<<else>>
You make a very reasonable point, sir, my apologies.
<<grant bridge_pass>>
<<endif>>
===
```
When the conditional evaluates to `true`, the option is delivered to the game normally. If the
conditional turns out to be false, the option is still delivered, but is marked as `unavailable`.
It is up to the game whether to display such option as greyed out, or crossed, or not show it at
all; however, such option cannot be selected.
As you have noticed, options always come in groups: after all, the player must select among several
possible choices. Thus, any sequence of options that are adjacent to each other in the dialogue,
will always be delivered as a single bundle to the frontend. This is called the **choice set**.
[line]: lines.md

View File

@ -0,0 +1,16 @@
# DialogueChoice
The **DialogueChoice** class represents multiple [Option] lines in the `.yarn` script, which will be
presented to the user so that they can make a choice for how the dialogue should proceed. The
`DialogueChoice` objects will be delivered to your `DialogueView` with the method
`onChoiceStart()`.
## Properties
**options** `List<DialogueOption>`
: The list of [DialogueOption]s comprising this choice set.
[Option]: ../language/options.md
[DialogueOption]: dialogue_option.md

View File

@ -0,0 +1,26 @@
# DialogueLine
The **DialogueLine** class represents a single [Line] of text in the `.yarn` script. The
`DialogueLine` objects will be delivered to your `DialogueView` with the methods `onLineStart()`,
`onLineSignal()`, `onLineStop()`, and `onLineFinish()`.
## Properties
**character** `String?`
: The name of the character who is speaking the line, or `null` if the line has no speaker.
**text** `String`
: The computed text of the line, after evaluating the inline expressions, stripping the markup,
and processing the escape sequences.
**tags** `List<String>`
: The list of hashtags for this line. If there are no hashtags, the list will be empty. Each entry
in the list will be a simple string starting with `#`.
**attributes** `List<MarkupAttribute>`
: The list of markup spans associated with the line. Each [MarkupAttribute] corresponds to a
single span within the **text**, delineated with markup tags.
[Line]: ../language/lines.md
[MarkupAttribute]: markup_attribute.md

View File

@ -0,0 +1,29 @@
# DialogueOption
The **DialogueOption** class represents a single [Option] line in the `.yarn` script. Multiple
options will be grouped into [DialogueChoice] objects.
## Properties
**text** `String`
: The computed text of the option, after evaluating the inline expressions, stripping the markup,
and processing the escape sequences.
**tags** `List<String>`
: The list of hashtags for this option. If there are no hashtags, the list will be empty. Each entry
in the list will be a simple string starting with `#`.
**attributes** `List<MarkupAttribute>`
: The list of markup spans associated with the option. Each [MarkupAttribute] corresponds to a
single span within the **text**, delineated with markup tags.
**isAvailable** `bool`
: The result of evaluating the *conditional* of this option. If the option has no conditional, this
will return `true`.
**isDisabled** `bool`
: Same as `!isAvailable`.
[Option]: ../language/options.md
[DialogueChoice]: dialogue_choice.md

View File

@ -0,0 +1,100 @@
# DialogueRunner
The **DialogueRunner** class is used to execute the dialogue at runtime. If you think of a
[YarnProject] as a dialogue program consisting of multiple [Node]s as "functions", then a
`DialogueRunner` is the virtual machine that can run a single "function" within that "program".
A single DialogueRunner may only execute one dialogue `Node` at a time. It is an error to try to
run another Node before the first one concludes. However, it is possible to create multiple
DialogueRunners for the same YarnProject, and then they would be able to execute multiple dialogues
simultaneously (for example, in a crowded room there could be multiple dialogues occurring at once
with different groups of people).
The job of a DialogueRunner is to fetch the dialogue lines in the correct order and at the
appropriate pace, to execute the logic in dialogue scripts, and to branch according to user input
in [DialogueChoice]s. The output of a DialogueRunner, therefore, is a stream of dialogue statements
that need to be presented to the player. Such presentation, is handled by the [DialogueView]s.
## Construction
The constructor takes two required parameters:
**yarnProject** `YarnProject`
: The [YarnProject] which the dialogue runner will be executing.
**dialogueViews** `List<DialogueView>`
: The list of [DialogueView]s that will be presenting the dialogue within the game. Each of these
`DialogueView`s can only be assigned to a single `DialogueRunner` at a time.
## Properties
**project** `YarnProject`
: The [YarnProject] within which the dialogue runner is running.
## Methods
**runNode**(`String nodeName`)
: Executes the node with the given name, and returns a future that completes only when the dialogue
finishes running (which may be a while). A single `DialogueRunner` can only run one node at a
time.
**sendSignal**(`dynamic signal`)
: Delivers the given `signal` to all dialogue views, in the form of a `DialogueView` method
`onLineSignal(line, signal)`. This can be used, for example, as a means of communication between
the dialogue views.
The `signal` object here is completely arbitrary, and it is up to the implementations to decide
which signals to send and to receive. Implementations should ignore any signals they do not
understand.
**stopLine**()
: Requests (via `onLineStop()`) that the presentation of the current line be finished as quickly
as possible. The dialogue will then proceed normally to the next line.
## Execution model
The `DialogueRunner` uses futures as a main mechanism for controlling the timing of the dialogue
progression. For each event, the dialogue runner will invoke the corresponding callback on all its
[DialogueView]s, and each of those callbacks may return a future. The dialogue runner then awaits
on all of these futures (in parallel), before proceeding to the next event.
For a simple `.yarn` script like this
```yarn
title: main
---
Hello
-> Hi
-> Go away
<<jump Away>>
===
title: Away
---
<<OhNo>>
===
```
the sequence of emitted events will be as follows (assuming the second option is selected):
- `onDialogueStart()`
- `onNodeStart(Node("main"))`
- `onLineStart(Line("Hello"))`
- `onLineFinish(Line("Hello"))`
- `onChoiceStart(Choice(["Hi", "Go away"]))`
- `onChoiceFinish(Option("Go away"))`
- `onNodeFinish(Node("main"))`
- `onNodeStart(Node("Away"))`
- `onCommand(Command("OhNo"))`
- `onNodeFinish(Node("Away"))`
- `onDialogueFinish()`
[DialogueChoice]: dialogue_choice.md
[DialogueView]: dialogue_view.md
[Node]: node.md
[YarnProject]: yarn_project.md

View File

@ -0,0 +1,132 @@
# DialogueView
The **DialogueView** class is the main mechanism of integrating Jenny with any existing framework,
such as a game engine. This class describes how [line]s and [option]s are presented to the user.
This class is abstract, which means you must create a concrete implementation in order to use the
dialogue system. The concrete `DialogueView` objects will then be passed to a [DialogueRunner],
which will orchestrate the dialogue's progression.
## Properties
**dialogueRunner**: `DialogueRunner?`
: The owner of this DialogueView. This property is non-`null` when the dialogue view is actively
used by a `DialogueRunner`.
This property can be used in order to access the parent [YarnProject], or to send signals into the
sibling `DialogueView`s.
## Methods
These methods that the `DialogueView` can implement in order to respond to the corresponding events.
Each method is optional, with a default implementation that does nothing. This means that you can
only implement those methods that you care about.
Most of the methods return `FutureOr<void>`, which means that the implementations can be either
synchronous or asynchronous. In the latter case, the dialogue runner will wait for the future to
resolve before proceeding (however, the futures from several dialogue views are awaited
simultaneously).
**onDialogueStart**()
: Called at the start of a new dialogue.
This is a good place to prepare the game's UI, such as fade in or animate dialogue panels, or
load resources.
**onDialogueFinish**()
: Called when the dialogue is about to finish.
**onNodeStart**(`Node node`)
: Called when the dialogue runner starts executing the [Node]. This will be called at the start of
`DialogueView.runNode()` (but after the **onDialogueStart**), and then each time the dialogue
jumps to another node.
This method is a good place to perform node-specific initialization, for example by querying the
`node`'s properties or metadata.
**onNodeFinish**(`Node node`)
: TODO
**onLineStart**(`DialogueLine line`) `-> bool`
: Called when the next dialogue [line] should be presented to the user.
The DialogueView may decide to present the `line` in whatever way it wants, or to not present
the line at all. For example, the dialogue view may: augment the line object, render the line at
a certain place on the screen, render only the character's name, show the portrait of whoever is
speaking, show the text within a chat bubble, play a voice-over audio file, store the text into
the player's conversation log, move the camera to show the speaker, etc.
Some of these methods of delivery can be considered "primary", while others are "auxiliary".
A "primary" dialogue view should return `true`, while all others `false` (especially if the
dialogue view ignores the line completely). This is used as a robustness check: if none of the
dialogue views return `true`, then a `DialogueError` will be thrown because it would mean the
line was not shown to the user in a meaningful way.
It is common for non-trivial dialogue views to return a future. After all, if this method were
to return immediately, the dialogue runner would advance to the next line without any delay,
and the player wouldn't have time to read the line. A common scenario then is to reveal the line
gradually, and then wait some time before returning; or, alternatively, return a `Completer`-based
future that completes based on some user action such as clicking a button or pressing a
keyboard key.
Note that this method is supposed to only *show* the line to the player, it should not try to
hide it at the end -- for that, there is a dedicated method **onLineFinish**.
**onLineSignal**(`DialogueLine line, dynamic signal`)
: Called when the dialogue runner sends the `signal` to all dialogue views.
The signal will be sent to all views, regardless of whether they have finished running
their **onLineStart** or not. The interpretation of the signal and the appropriate response
is up to the dialogue view.
For example, one possible scenario would be to speed up a typewriter effect and reveal the text
immediately in response to a "RUSH" signal. Or pause presentation in response to a "PAUSE"
signal. Or give a warning if the player makes a hostile gesture such as drawing a weapon.
**onLineStop**(`DialogueLine line`)
: Invoked via the [DialogueRunner]'s `stopLine()` method. This is a request to finish presenting
the line as quickly as possible (though it doesn't have to be immediate).
Examples when calling this method could be appropriate: the player was hit while talking to
an NPC; or the user has pressed the "next" button in the dialogue UI.
This method is invoked on all dialogue views, regardless of whether they finished their
**onLineStart** call or not. If they haven't, the futures from `onLineStart` will be discarded
and will no longer be awaited. In addition, the **onLineFinish** will not be called either --
the line will be considered finished when the future from `onLineStop` completes.
**onLineFinish**(`DialogueLine line`)
: Called when the line has finished presenting in all dialogue views.
Some dialogue views may need to clear their display when this event happens, or make some other
preparations to receive the next dialogue line.
**onChoiceStart**(`DialogueChoice choice`) `-> int`
: TODO
**onChoiceFinish**(`DialogueOption option`)
: Called when the choice has been made, and the [option] has been selected.
The `option` will be the one returned from the **onChoiceStart** method by one of the dialogue
views.
**onCommand**(`UserDefinedCommand command`)
: Called when executing a user-defined [command].
This method is invoked immediately after the command itself is executed, but before the result of
the execution was awaited. Thus, if the command's effect is asynchronous, then it will be send to
dialogue views and executed "at the same time".
In cases when the command's effect occurs within the game itself, implementing this method may not
be necessary. However, if you want to have a command that affects the dialogue views themselves,
then this method provides a way of doing that.
[DialogueRunner]: dialogue_runner.md
[Node]: node.md
[YarnProject]: yarn_project.md
[command]: user_defined_command.md
[line]: dialogue_line.md
[option]: dialogue_option.md

View File

@ -0,0 +1,14 @@
# Jenny Runtime
```{toctree}
:hidden:
DialogueChoice <dialogue_choice.md>
DialogueLine <dialogue_line.md>
DialogueOption <dialogue_option.md>
DialogueRunner <dialogue_runner.md>
DialogueView <dialogue_view.md>
MarkupAttribute <markup_attribute.md>
Node <node.md>
YarnProject <yarn_project.md>
```

View File

@ -0,0 +1,43 @@
# MarkupAttribute
A **MarkupAttribute** is a descriptor of a subrange of text in a [line], demarcated with markup
tags. For example, in a `.yarn` line below there are two ranges of text surrounded by markup tags,
and therefore there will be two `MarkupAttribute`s associated with this line:
```yarn
[b]Jenny[/b] is a library based on \
[link url="docs.yarnspinner.dev"]YarnSpinner[/link] for Unity.
```
These `MarkupAttribute`s can be found in the `.attributes` property of a [DialogueLine][line].
## Properties
**name** `String`
: The name of the markup tag. In the example above, the name of the first attribute is `"b"`, and
the second is `"link"`.
**start**, **end** `int`
: The location of the marked-up span within the final text of the line. The first index is
inclusive, while the second is exclusive. The `start` may be equal to `end` for a zero-width
markup attribute.
**length** `int`
: The length of marked-up text. This is always equal to `end - start`.
**parameters** `Map<String, dynamic>`
: The set of parameters associated with this markup attribute. In the example above, the first
markup attribute has no parameters, so this map will be empty. The second markup attribute has a
single parameter, so this map will be equal to `{"url": "docs.yarnspinner.dev"}`.
The type of each parameter will be either `String`, `num`, or `bool`, depending on the type of
expression give in the `.yarn` script. The expressions for parameter values can be dynamic, that
is they can be evaluated at runtime. In the example below, the parameter `color` will be equal to
the value of the variable `$color`, which may change each time the line is run.
```yarn
My [i]favorite[/i] color is [bb color=$color]{$color}[/bb].
```
[line]: dialogue_line.md

View File

@ -0,0 +1,21 @@
# Node
The **Node** class represents a single [node] within the `.yarn` script. The objects of this class
will be delivered to your [DialogueView]s with the methods `onNodeStart()`, `onNodeFinish()`.
## Properties
**title** `String`
: The title (name) of the node.
**tags** `Map<String, String>`
: Additional tags specified in the header of the node. The map will be empty if there were no tags
besides the required `title` tag.
**iterator** `Iterator<DialogueEntry>`
: The content of the node, which is a sequence of `DialogueLine`s, `DialogueChoice`s, or
`Command`s.
[node]: ../language/nodes.md
[DialogueView]: dialogue_view.md

View File

@ -0,0 +1,77 @@
# Yarn Project
A **YarnProject** is the central hub for all yarn scripts and the accompanying information.
Generally, there would be a single `YarnProject` in a game, though it is also possible to make
several yarn projects if their content is completely independent.
The standard sequence of initializing a `YarnProject` is the following:
- link user-defined functions;
- link user-defined commands;
- set the locale (if different from `en`);
- parse a `.yarn` script containing declarations of global variables and characters;
- parse all other `.yarn` scripts;
- restore the variables from a save-game storage.
For example:
```dart
final yarn = YarnProject()
..functions.addFunction0('money', player.getMoney)
..commands.addCommand1('achievement', player.earnAchievement)
..parse(readFile('project.yarn'))
..parse(readFile('chapter1.yarn'))
..parse(readFile('chapter2.yarn'));
```
## Properties
**locale** `String`
: The language used in this `YarnProject` (the default is `'en'`). Selecting a different language
changes the builtin `plural()` function.
**random** `Random`
: The random number generator. This can be replaced with a different generator, if, for example,
you need to control the seed.
**nodes** `Map<String, Node>`
: All [Node]s loaded into the project, keyed by their titles.
**variables** `VariableStorage`
: The container for all global variables used in this yarn project. There could be several reasons
to access this storage:
<!-- markdownlint-disable MD006 MD007 -->
- to change the value of a yarn variable from the game. This enables you to pass the information
from the game into the dialogue. For example, your dialogue may have variable `$gold`, which
you may want to update whenever the player's amount of money changes within the game.
- to store the values of all yarn variables during the save game, and to restore them when
loading the game.
<!-- markdownlint-enable MD006 MD007 -->
**functions** `FunctionStorage`
: The container for all user-defined functions linked into the project. The main reason to access
this property is to register new custom function to be available at runtime.
Note that all custom functions must be added to the `YarnProject` before they can be used in a
dialogue script -- otherwise a compile error will occur when encountering an unknown function.
**commands** `CommandStorage`
: The container for all user-defined commands linked into the project. The main reason to access
this container is to register new custom commands.
All custom commands must be added before they can be used in the dialogue script.
## Methods
**parse**(`String text`)
: Parses and compiles the `text` of a yarn script. After this command, the nodes contained within
the script will be runnable.
This method can be executed multiple times, and each time the new nodes will be added to the
existing ones.
[Node]: node.md

View File

@ -1,5 +1,11 @@
# Other Modules
:::{package} jenny
This module lets you add interactive dialogue into your game. The module itself handles Yarn scripts
and the dialogue runtime; use bridge package `flame_jenny` in order to add it into a Flame game.
:::
:::{package} oxygen
Oxygen is a lightweight Entity Component System framework written in Dart, with a focus on
@ -11,5 +17,6 @@ Entity Component System.
```{toctree}
:hidden:
oxygen <oxygen/oxygen.md>
jenny <jenny/index.md>
oxygen <oxygen/oxygen.md>
```

View File

@ -193,9 +193,9 @@ Aces. This doesn't make a very exciting gameplay though, so add line
in the `KlondikeGame` class right after the list of cards is created.
```{seealso}
For more information about tap functionality, see [](../../flame/inputs/tap-events.md).
```
:::{seealso}
For more information about tap functionality, see [](../../flame/inputs/tap_events.md).
:::
### Stock pile -- visual representation

View File

@ -37,9 +37,7 @@ class DialogueRunner {
}) : project = yarnProject,
_dialogueViews = dialogueViews,
_currentNodes = [],
_iterators = [] {
dialogueViews.forEach((dv) => dv.dialogueRunner = this);
}
_iterators = [];
final YarnProject project;
final List<DialogueView> _dialogueViews;
@ -50,37 +48,42 @@ class DialogueRunner {
/// Executes the node with the given name, and returns a future that finishes
/// once the dialogue stops running.
Future<void> runNode(String nodeName) async {
if (_currentNodes.isNotEmpty) {
throw DialogueError(
'Cannot run node "$nodeName" because another node is '
'currently running: "${_currentNodes.last.title}"',
);
}
final newNode = project.nodes[nodeName];
if (newNode == null) {
throw NameError('Node "$nodeName" could not be found');
}
_currentNodes.add(newNode);
_iterators.add(newNode.iterator);
await _combineFutures(
[for (final view in _dialogueViews) view.onDialogueStart()],
);
await _combineFutures(
[for (final view in _dialogueViews) view.onNodeStart(newNode)],
);
while (_iterators.isNotEmpty) {
final iterator = _iterators.last;
if (iterator.moveNext()) {
final entry = iterator.current;
await entry.processInDialogueRunner(this);
} else {
_finishCurrentNode();
try {
if (_currentNodes.isNotEmpty) {
throw DialogueError(
'Cannot run node "$nodeName" because another node is '
'currently running: "${_currentNodes.last.title}"',
);
}
final newNode = project.nodes[nodeName];
if (newNode == null) {
throw NameError('Node "$nodeName" could not be found');
}
_dialogueViews.forEach((dv) => dv.dialogueRunner = this);
_currentNodes.add(newNode);
_iterators.add(newNode.iterator);
await _combineFutures(
[for (final view in _dialogueViews) view.onDialogueStart()],
);
await _combineFutures(
[for (final view in _dialogueViews) view.onNodeStart(newNode)],
);
while (_iterators.isNotEmpty) {
final iterator = _iterators.last;
if (iterator.moveNext()) {
final entry = iterator.current;
await entry.processInDialogueRunner(this);
} else {
_finishCurrentNode();
}
}
await _combineFutures(
[for (final view in _dialogueViews) view.onDialogueFinish()],
);
} finally {
_dialogueViews.forEach((dv) => dv.dialogueRunner = null);
}
await _combineFutures(
[for (final view in _dialogueViews) view.onDialogueFinish()],
);
}
void _finishCurrentNode() {
@ -129,7 +132,7 @@ class DialogueRunner {
_error('Invalid option index chosen in a dialogue: $chosenIndex');
}
final chosenOption = choice.options[chosenIndex];
if (!chosenOption.available) {
if (!chosenOption.isAvailable) {
_error('A dialogue view selected a disabled option: $chosenOption');
}
await _combineFutures(

View File

@ -12,11 +12,11 @@ import 'package:meta/meta.dart';
abstract class DialogueView {
DialogueView();
late DialogueRunner _dialogueRunner;
DialogueRunner? _dialogueRunner;
DialogueRunner get dialogueRunner => _dialogueRunner;
DialogueRunner? get dialogueRunner => _dialogueRunner;
@internal
set dialogueRunner(DialogueRunner value) => _dialogueRunner = value;
set dialogueRunner(DialogueRunner? value) => _dialogueRunner = value;
/// Called before the start of a new dialogue, i.e. before any lines, options,
/// or commands are delivered.
@ -27,6 +27,13 @@ abstract class DialogueView {
/// completes.
FutureOr<void> onDialogueStart() {}
/// Called when the dialogue has ended.
///
/// This method can be used to clean up any of the dialogue UI. The returned
/// future will be awaited before the dialogue runner considers its job
/// finished.
FutureOr<void> onDialogueFinish() {}
/// Called when the dialogue enters a new [node].
///
/// This will be called immediately after the [onDialogueStart], and then
@ -144,13 +151,6 @@ abstract class DialogueView {
/// future to complete before proceeding with the dialogue.
FutureOr<void> onCommand(UserDefinedCommand command) {}
/// Called when the dialogue has ended.
///
/// This method can be used to clean up any of the dialogue UI. The returned
/// future will be awaited before the dialogue runner considers its job
/// finished.
FutureOr<void> onDialogueFinish() {}
/// A future that never completes.
@internal
static Future<Never> never = Completer<Never>().future;

View File

@ -168,9 +168,9 @@ enum TokenType {
startIndent, // RegExp(r'^\s*')
startMarkupTag, // '['
startParenthesis, // '('
typeBool, // 'bool'
typeNumber, // 'number'
typeString, // 'string'
typeBool, // 'Bool'
typeNumber, // 'Number'
typeString, // 'String'
error,
eof,

View File

@ -601,6 +601,7 @@ class _Lexer {
} else {
final cu = currentCodeUnit;
if (cu == $backslash ||
cu == $colon ||
cu == $slash ||
cu == $hash ||
cu == $minus ||

View File

@ -7,24 +7,27 @@ class DialogueOption extends DialogueLine {
required super.content,
super.character,
super.tags,
this.condition,
BoolExpression? condition,
this.block = const Block([]),
});
}) : _condition = condition;
final BoolExpression? condition;
final BoolExpression? _condition;
final Block block;
bool available = true;
bool _available = true;
bool get isAvailable => _available;
bool get isDisabled => !_available;
@override
void evaluate() {
super.evaluate();
available = condition?.value ?? true;
_available = _condition?.value ?? true;
}
@override
String toString() {
final prefix = character == null ? '' : '$character: ';
final suffix = available ? '' : ' #disabled';
final suffix = _available ? '' : ' #disabled';
return 'Option($prefix$text$suffix)';
}
}

View File

@ -1,32 +1,44 @@
import 'package:jenny/jenny.dart';
import 'package:jenny/src/structure/block.dart';
import 'package:jenny/src/structure/dialogue_entry.dart';
import 'package:meta/meta.dart';
class Node extends Iterable<DialogueEntry> {
const Node({
required this.title,
required this.content,
this.tags,
this.variables,
});
required Block content,
Map<String, String>? tags,
VariableStorage? variables,
}) : _content = content,
_tags = tags,
_variables = variables;
final String title;
final Map<String, String>? tags;
final Block content;
final VariableStorage? variables;
final Map<String, String>? _tags;
final Block _content;
final VariableStorage? _variables;
List<DialogueEntry> get lines => content.lines;
/// The list of extra tags specified in the node header.
Map<String, String> get tags => _tags ?? const <String, String>{};
/// Local variable storage for this node. This can be `null` if the node does
/// not define any local variables.
VariableStorage? get variables => _variables;
/// The iterator over the content of the node.
@override
NodeIterator get iterator => NodeIterator(this);
@visibleForTesting
List<DialogueEntry> get lines => _content.lines;
@override
String toString() => 'Node($title)';
@override
NodeIterator get iterator => NodeIterator(this);
}
class NodeIterator implements Iterator<DialogueEntry> {
NodeIterator(this.node) {
diveInto(node.content);
diveInto(node._content);
}
final Node node;

View File

@ -77,8 +77,4 @@ class YarnProject {
void parse(String text) {
impl.parse(text, this);
}
void setVariable(String name, dynamic value) {
variables.setVariable(name, value);
}
}

View File

@ -155,8 +155,8 @@ class _RecordingDialogueView extends DialogueView {
class _InterruptingCow extends DialogueView {
@override
FutureOr<bool> onLineStart(DialogueLine line) async {
dialogueRunner.sendSignal("I'm a banana!");
dialogueRunner.stopLine();
dialogueRunner!.sendSignal("I'm a banana!");
dialogueRunner!.stopLine();
return false;
}
}

View File

@ -50,18 +50,18 @@ void main() {
group('parseNodeHeader', () {
test('node with tags', () {
final yarn = YarnProject();
yarn.parse('title: Romeo v Juliette\n'
yarn.parse('title: Romeo_v_Juliette\n'
'requires: Montagues and Capulets\n'
'\n'
'// comment\n'
'location: fair Verona\n'
'---\n===\n');
final node = yarn.nodes['Romeo v Juliette'];
final node = yarn.nodes['Romeo_v_Juliette'];
expect(node, isNotNull);
expect(node!.title, 'Romeo v Juliette');
expect(node!.title, 'Romeo_v_Juliette');
expect(node.tags, isNotNull);
expect(node.tags!['requires'], 'Montagues and Capulets');
expect(node.tags!['location'], 'fair Verona');
expect(node.tags['requires'], 'Montagues and Capulets');
expect(node.tags['location'], 'fair Verona');
});
test('multiple colons', () {
@ -143,18 +143,18 @@ void main() {
'''),
);
final node1 = yarn.nodes['EmptyTags']!;
expect(node1.tags, isNotNull);
expect(node1.tags!['tags'], '');
expect(node1.tags, isNotEmpty);
expect(node1.tags['tags'], '');
final node2 = yarn.nodes['Tags']!;
expect(node2.tags!['tags'], 'one two three');
expect(node2.tags['tags'], 'one two three');
final node3 = yarn.nodes['ArbitraryHeaderWithValue']!;
expect(node3.tags!['arbitraryHeader'], 'some-arbitrary-text');
expect(node3.tags['arbitraryHeader'], 'some-arbitrary-text');
final node4 = yarn.nodes['Comments']!;
expect(node4.tags!['tags'], 'one two three');
expect(node4.tags!['metadata'], '');
expect(node4.tags['tags'], 'one two three');
expect(node4.tags['metadata'], '');
});
test(
@ -190,11 +190,11 @@ void main() {
'Saturn\n\n'
'Uranus // LOL\n'
'===\n');
final block = yarn.nodes['test']!.content;
expect(block.lines.length, 3);
final lines = yarn.nodes['test']!.lines;
expect(lines.length, 3);
for (var i = 0; i < 3; i++) {
expect(block.lines[i], isA<DialogueLine>());
final line = block.lines[i] as DialogueLine;
expect(lines[i], isA<DialogueLine>());
final line = lines[i] as DialogueLine;
expect(line.character, isNull);
expect(line.tags, isEmpty);
expect(line.text, ['Jupyter', 'Saturn', 'Uranus'][i]);
@ -282,7 +282,7 @@ void main() {
final line = choiceSet.options[i];
expect(line.character, isNull);
expect(line.tags, isEmpty);
expect(line.condition, isNull);
expect(line.isAvailable, true);
expect(line.block, isEmpty);
expect(line.text, ['Alpha', 'Beta', 'Gamma'][i]);
}

View File

@ -564,6 +564,28 @@ void main() {
);
});
test('escaped colon', () {
expect(
tokenize('---\n---\n'
'One\\: two\n'
'One two three\\:\n'
'===\n'),
const [
Token.startHeader,
Token.endHeader,
Token.startBody,
Token.text('One'),
Token.text(':'),
Token.text(' two'),
Token.newline,
Token.text('One two three'),
Token.text(':'),
Token.newline,
Token.endBody,
],
);
});
test('">>" sequence', () {
expect(
tokenize('---\n---\n'

View File

@ -9,7 +9,7 @@ void main() {
group('JumpCommand', () {
test('<<jump>> command', () {
final yarn = YarnProject()
..setVariable(r'$target', 'DOWN')
..variables.setVariable(r'$target', 'DOWN')
..parse('title:A\n---\n'
'<<jump UP>>\n'
'<<jump {\$target}>>\n'

View File

@ -17,7 +17,7 @@ void main() {
expect(node.title, 'Introduction');
expect(node.lines, <DialogueEntry>[]);
expect(node.tags, isNull);
expect(node.tags, isEmpty);
expect('$node', 'Node(Introduction)');
});
@ -29,9 +29,9 @@ void main() {
);
expect(node.title, 'Conclusion');
expect(node.tags, isNotEmpty);
expect(node.tags!.length, 2);
expect(node.tags!['line'], 'af451');
expect(node.tags!['characters'], 'Alice, Bob');
expect(node.tags.length, 2);
expect(node.tags['line'], 'af451');
expect(node.tags['characters'], 'Alice, Bob');
});
group('iterators', () {

View File

@ -147,18 +147,18 @@ class _TestPlan extends DialogueView {
final text2 =
(option2.character == null ? '' : '${option2.character}: ') +
option2.text +
(option2.available ? '' : ' [disabled]');
(option2.isAvailable ? '' : ' [disabled]');
assert(
text1 == text2,
'\n'
'Expected (${i + 1}): $text1\n'
'Actual (${i + 1}): $text2\n',
'Expected ($i): $text1\n'
'Actual ($i): $text2\n',
);
assert(
option1.enabled == option2.available,
option1.enabled == option2.isAvailable,
'\n'
'Expected option(${i + 1}): $option1; available=${option1.enabled}\n'
'Actual option(${i + 1}): $option2; available=${option2.available}\n',
'Expected option($i): $option1; available=${option1.enabled}\n'
'Actual option($i): $option2; available=${option2.isAvailable}\n',
);
}
_currentIndex++;