mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 16:36:57 +08:00
docs: Description of jenny package (#2102)
Adding preliminary description for the jenny project
This commit is contained in:
2
.github/.cspell/flame_dictionary.txt
vendored
2
.github/.cspell/flame_dictionary.txt
vendored
@ -12,6 +12,7 @@ Klingsbo
|
||||
Lukas
|
||||
Nakama
|
||||
Patreon
|
||||
Prosser
|
||||
Skia
|
||||
Spritecow
|
||||
Tiled
|
||||
@ -28,6 +29,7 @@ tavian
|
||||
trex
|
||||
wolfenrain
|
||||
xaha
|
||||
yarnspinner
|
||||
рушниці
|
||||
рушниць
|
||||
рушниця
|
||||
|
||||
1
.github/.cspell/gamedev_dictionary.txt
vendored
1
.github/.cspell/gamedev_dictionary.txt
vendored
@ -130,6 +130,7 @@ subfolder
|
||||
subfolders
|
||||
sublist
|
||||
sublists
|
||||
subrange
|
||||
tappable
|
||||
tappables
|
||||
tileset
|
||||
|
||||
5
.github/.cspell/sphinx_dictionary.txt
vendored
5
.github/.cspell/sphinx_dictionary.txt
vendored
@ -1,4 +1,9 @@
|
||||
deflist
|
||||
dollarmath
|
||||
linkcheck
|
||||
markdownlint
|
||||
seealso
|
||||
smartquotes
|
||||
tasklist
|
||||
toctree
|
||||
toctrees
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
26
doc/_sphinx/extensions/yarn_lexer.css
Normal file
26
doc/_sphinx/extensions/yarn_lexer.css
Normal 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;
|
||||
}
|
||||
198
doc/_sphinx/extensions/yarn_lexer.py
Normal file
198
doc/_sphinx/extensions/yarn_lexer.py
Normal 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,
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
128
doc/other_modules/jenny/index.md
Normal file
128
doc/other_modules/jenny/index.md
Normal 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>
|
||||
```
|
||||
16
doc/other_modules/jenny/language/commands/commands.md
Normal file
16
doc/other_modules/jenny/language/commands/commands.md
Normal 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>
|
||||
```
|
||||
3
doc/other_modules/jenny/language/commands/declare.md
Normal file
3
doc/other_modules/jenny/language/commands/declare.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `<<declare>>`
|
||||
|
||||
TODO
|
||||
21
doc/other_modules/jenny/language/commands/if.md
Normal file
21
doc/other_modules/jenny/language/commands/if.md
Normal 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.
|
||||
16
doc/other_modules/jenny/language/commands/jump.md
Normal file
16
doc/other_modules/jenny/language/commands/jump.md
Normal 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"}>>
|
||||
```
|
||||
3
doc/other_modules/jenny/language/commands/set.md
Normal file
3
doc/other_modules/jenny/language/commands/set.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `<<set>>`
|
||||
|
||||
TODO
|
||||
4
doc/other_modules/jenny/language/commands/stop.md
Normal file
4
doc/other_modules/jenny/language/commands/stop.md
Normal 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.
|
||||
8
doc/other_modules/jenny/language/commands/wait.md
Normal file
8
doc/other_modules/jenny/language/commands/wait.md
Normal 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>>
|
||||
```
|
||||
3
doc/other_modules/jenny/language/expressions.md
Normal file
3
doc/other_modules/jenny/language/expressions.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Expressions
|
||||
|
||||
TODO
|
||||
82
doc/other_modules/jenny/language/language.md
Normal file
82
doc/other_modules/jenny/language/language.md
Normal 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>
|
||||
```
|
||||
186
doc/other_modules/jenny/language/lines.md
Normal file
186
doc/other_modules/jenny/language/lines.md
Normal 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.
|
||||
===
|
||||
```
|
||||
3
doc/other_modules/jenny/language/markup.md
Normal file
3
doc/other_modules/jenny/language/markup.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Markup
|
||||
|
||||
TODO
|
||||
89
doc/other_modules/jenny/language/nodes.md
Normal file
89
doc/other_modules/jenny/language/nodes.md
Normal 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
|
||||
62
doc/other_modules/jenny/language/options.md
Normal file
62
doc/other_modules/jenny/language/options.md
Normal 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
|
||||
16
doc/other_modules/jenny/runtime/dialogue_choice.md
Normal file
16
doc/other_modules/jenny/runtime/dialogue_choice.md
Normal 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
|
||||
26
doc/other_modules/jenny/runtime/dialogue_line.md
Normal file
26
doc/other_modules/jenny/runtime/dialogue_line.md
Normal 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
|
||||
29
doc/other_modules/jenny/runtime/dialogue_option.md
Normal file
29
doc/other_modules/jenny/runtime/dialogue_option.md
Normal 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
|
||||
100
doc/other_modules/jenny/runtime/dialogue_runner.md
Normal file
100
doc/other_modules/jenny/runtime/dialogue_runner.md
Normal 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
|
||||
132
doc/other_modules/jenny/runtime/dialogue_view.md
Normal file
132
doc/other_modules/jenny/runtime/dialogue_view.md
Normal 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
|
||||
14
doc/other_modules/jenny/runtime/index.md
Normal file
14
doc/other_modules/jenny/runtime/index.md
Normal 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>
|
||||
```
|
||||
43
doc/other_modules/jenny/runtime/markup_attribute.md
Normal file
43
doc/other_modules/jenny/runtime/markup_attribute.md
Normal 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
|
||||
21
doc/other_modules/jenny/runtime/node.md
Normal file
21
doc/other_modules/jenny/runtime/node.md
Normal 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
|
||||
77
doc/other_modules/jenny/runtime/yarn_project.md
Normal file
77
doc/other_modules/jenny/runtime/yarn_project.md
Normal 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
|
||||
@ -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>
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -601,6 +601,7 @@ class _Lexer {
|
||||
} else {
|
||||
final cu = currentCodeUnit;
|
||||
if (cu == $backslash ||
|
||||
cu == $colon ||
|
||||
cu == $slash ||
|
||||
cu == $hash ||
|
||||
cu == $minus ||
|
||||
|
||||
@ -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)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -77,8 +77,4 @@ class YarnProject {
|
||||
void parse(String text) {
|
||||
impl.parse(text, this);
|
||||
}
|
||||
|
||||
void setVariable(String name, dynamic value) {
|
||||
variables.setVariable(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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', () {
|
||||
|
||||
@ -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++;
|
||||
|
||||
Reference in New Issue
Block a user