diff --git a/.github/.cspell/flame_dictionary.txt b/.github/.cspell/flame_dictionary.txt index b6060b7c0..0a94bf8ec 100644 --- a/.github/.cspell/flame_dictionary.txt +++ b/.github/.cspell/flame_dictionary.txt @@ -12,6 +12,7 @@ Klingsbo Lukas Nakama Patreon +Prosser Skia Spritecow Tiled @@ -28,6 +29,7 @@ tavian trex wolfenrain xaha +yarnspinner рушниці рушниць рушниця diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt index 653cb16e3..2d399d915 100644 --- a/.github/.cspell/gamedev_dictionary.txt +++ b/.github/.cspell/gamedev_dictionary.txt @@ -130,6 +130,7 @@ subfolder subfolders sublist sublists +subrange tappable tappables tileset diff --git a/.github/.cspell/sphinx_dictionary.txt b/.github/.cspell/sphinx_dictionary.txt index 6461ba387..ff185ea1b 100644 --- a/.github/.cspell/sphinx_dictionary.txt +++ b/.github/.cspell/sphinx_dictionary.txt @@ -1,4 +1,9 @@ +deflist +dollarmath linkcheck markdownlint +seealso +smartquotes +tasklist toctree toctrees diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9874cbe31..4ab6de088 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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:/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 diff --git a/doc/_sphinx/conf.py b/doc/_sphinx/conf.py index ad77bc5f8..deab86f62 100644 --- a/doc/_sphinx/conf.py +++ b/doc/_sphinx/conf.py @@ -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

") del titles[0] # remove the

title - h1_seen = False - ul_level = 0 html_text = "
\n" html_text += "
Contents
\n" for title, node_id, level in titles: if level <= 1: return document.reporter.error("More than one

title on the page") - html_text += f" {title}\n" + html_text += f" " \ + f"{html.escape(title)}\n" html_text += "

\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): diff --git a/doc/_sphinx/extensions/yarn_lexer.css b/doc/_sphinx/extensions/yarn_lexer.css new file mode 100644 index 000000000..a6855b2e2 --- /dev/null +++ b/doc/_sphinx/extensions/yarn_lexer.css @@ -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; +} diff --git a/doc/_sphinx/extensions/yarn_lexer.py b/doc/_sphinx/extensions/yarn_lexer.py new file mode 100644 index 000000000..c0cedb688 --- /dev/null +++ b/doc/_sphinx/extensions/yarn_lexer.py @@ -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(''), + include(''), + (r'#[^\n]*\n', Comment.Hashbang), + (r'---+\n', Punctuation, 'node_header'), + default('node_header'), + ], + + '': [ + (r'\s+', Whitespace), + (r'//.*\n', Comment), + ], + + 'node_header': [ + include(''), + (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(''), + 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(''), + include(''), + (r'#[^\s]+', Comment.Hashbang), + (r'[<>/]', Text), + (r'[^\n\\\[\]{}<>/#]+', Text), + (r'.', Text), # just in case + ], + + '': [ + (r'<<', Punctuation, 'command_name'), + ], + 'command_name': [ + (words(BUILTIN_COMMANDS, suffix=r'\b'), Keyword, 'command_body'), + (r'\w+', Name.Class, 'command_body'), + ], + 'command_body': [ + include(''), + (r'\{', Punctuation, 'curly_expression'), + (r'>>', Punctuation, '#pop:2'), + (r'>', Text), + default('command_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(''), + ], + 'curly_expression': [ + (r'\}', Punctuation, '#pop'), + include(''), + ], + 'string_expression': [ + (r'\}', String.Interpol, '#pop'), + include(''), + ], + + '': [ + (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, + } diff --git a/doc/_sphinx/requirements.txt b/doc/_sphinx/requirements.txt index 6385c614f..75d9f7f3a 100644 --- a/doc/_sphinx/requirements.txt +++ b/doc/_sphinx/requirements.txt @@ -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 diff --git a/doc/_sphinx/theme/flames.css b/doc/_sphinx/theme/flames.css index d2a9cec93..3888001a0 100644 --- a/doc/_sphinx/theme/flames.css +++ b/doc/_sphinx/theme/flames.css @@ -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 { diff --git a/doc/bridge_packages/bridge_packages.md b/doc/bridge_packages/bridge_packages.md index 5101adcd3..df03395d5 100644 --- a/doc/bridge_packages/bridge_packages.md +++ b/doc/bridge_packages/bridge_packages.md @@ -67,6 +67,7 @@ flame_bloc flame_fire_atlas flame_forge2d flame_isolate +flame_lottie flame_oxygen flame_rive flame_splash_screen diff --git a/doc/bridge_packages/flame_bloc/bloc.md b/doc/bridge_packages/flame_bloc/bloc.md index d9a0a246c..da1075df9 100644 --- a/doc/bridge_packages/flame_bloc/bloc.md +++ b/doc/bridge_packages/flame_bloc/bloc.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 diff --git a/doc/bridge_packages/flame_bloc/bloc_components.md b/doc/bridge_packages/flame_bloc/bloc_components.md index f54d694e2..46f685bbc 100644 --- a/doc/bridge_packages/flame_bloc/bloc_components.md +++ b/doc/bridge_packages/flame_bloc/bloc_components.md @@ -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 diff --git a/doc/bridge_packages/flame_forge2d/forge2d.md b/doc/bridge_packages/flame_forge2d/forge2d.md index 15d6a396a..3111ecf7d 100644 --- a/doc/bridge_packages/flame_forge2d/forge2d.md +++ b/doc/bridge_packages/flame_forge2d/forge2d.md @@ -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`. diff --git a/doc/bridge_packages/flame_isolate/isolate.md b/doc/bridge_packages/flame_isolate/isolate.md index 5d8d4268a..2d45f9444 100644 --- a/doc/bridge_packages/flame_isolate/isolate.md +++ b/doc/bridge_packages/flame_isolate/isolate.md @@ -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 diff --git a/doc/development/style_guide.md b/doc/development/style_guide.md index b981a6ef6..447909057 100644 --- a/doc/development/style_guide.md +++ b/doc/development/style_guide.md @@ -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 diff --git a/doc/flame/collision_detection.md b/doc/flame/collision_detection.md index f79905584..9a67030f5 100644 --- a/doc/flame/collision_detection.md +++ b/doc/flame/collision_detection.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 diff --git a/doc/flame/components.md b/doc/flame/components.md index 81c272b04..5da3d89ca 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -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 diff --git a/doc/flame/other/debug.md b/doc/flame/other/debug.md index 10538b0bc..0ba3cb3eb 100644 --- a/doc/flame/other/debug.md +++ b/doc/flame/other/debug.md @@ -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 diff --git a/doc/other_modules/jenny/index.md b/doc/other_modules/jenny/index.md new file mode 100644 index 000000000..3faa9cc96 --- /dev/null +++ b/doc/other_modules/jenny/index.md @@ -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 +--- +<> + Slughorn: Sorry, Tom, I don't have time right now. + <> +<> + +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. + <> + Tom: Thank you, Professor, this is very munificent of you. +-> I wanted to ask... about Horcruxes <> + <> +-> 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... + <> + -> 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... + <> +-> Tom: I overheard it... And the word felt sharp and frigid, like it was the \ + embodiment of Dark Art <= 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 ... + ... + <> +=== +``` + +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 `<>` or `<>`; +- 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 (`<>`, `<>`). + +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 +Jenny API +``` diff --git a/doc/other_modules/jenny/language/commands/commands.md b/doc/other_modules/jenny/language/commands/commands.md new file mode 100644 index 000000000..e51f3b30f --- /dev/null +++ b/doc/other_modules/jenny/language/commands/commands.md @@ -0,0 +1,16 @@ +# Commands + +The **commands** are special instructions surrounded with double angle-brackets: `<>`. There +are both built-in and user-defined commands. + + +```{toctree} +:hidden: + +<> +<> +<> +<> +<> +<> +``` diff --git a/doc/other_modules/jenny/language/commands/declare.md b/doc/other_modules/jenny/language/commands/declare.md new file mode 100644 index 000000000..152b93928 --- /dev/null +++ b/doc/other_modules/jenny/language/commands/declare.md @@ -0,0 +1,3 @@ +# `<>` + +TODO diff --git a/doc/other_modules/jenny/language/commands/if.md b/doc/other_modules/jenny/language/commands/if.md new file mode 100644 index 000000000..e5f9c10fe --- /dev/null +++ b/doc/other_modules/jenny/language/commands/if.md @@ -0,0 +1,21 @@ +# `<>` + +The **\<\\>** 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 +<> + statements1... +<> + statements2... +<> + statementsN... +<> +``` + +The `<>`s and the `<>` are optional, whereas the final `<>` is mandatory. Also, +notice that the statements within each block are indented. + +During the runtime, the conditions within each `<>` and `<>` 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. diff --git a/doc/other_modules/jenny/language/commands/jump.md b/doc/other_modules/jenny/language/commands/jump.md new file mode 100644 index 000000000..0f774ccda --- /dev/null +++ b/doc/other_modules/jenny/language/commands/jump.md @@ -0,0 +1,16 @@ +# `<>` + +The **\<\\>** command unconditionally moves the execution pointer to the start of the target +node. The target node will now become "current": + +```yarn +<> +``` + +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 +<> +``` diff --git a/doc/other_modules/jenny/language/commands/set.md b/doc/other_modules/jenny/language/commands/set.md new file mode 100644 index 000000000..117788d51 --- /dev/null +++ b/doc/other_modules/jenny/language/commands/set.md @@ -0,0 +1,3 @@ +# `<>` + +TODO diff --git a/doc/other_modules/jenny/language/commands/stop.md b/doc/other_modules/jenny/language/commands/stop.md new file mode 100644 index 000000000..42b794bcb --- /dev/null +++ b/doc/other_modules/jenny/language/commands/stop.md @@ -0,0 +1,4 @@ +# `<>` + +The **\<\\>** 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. diff --git a/doc/other_modules/jenny/language/commands/wait.md b/doc/other_modules/jenny/language/commands/wait.md new file mode 100644 index 000000000..74c80050a --- /dev/null +++ b/doc/other_modules/jenny/language/commands/wait.md @@ -0,0 +1,8 @@ +# `<>` + +The **\<\\>** command forces the engine to wait the provided number of seconds before +proceeding with the dialogue. + +```yarn +<> +``` diff --git a/doc/other_modules/jenny/language/expressions.md b/doc/other_modules/jenny/language/expressions.md new file mode 100644 index 000000000..17b98dd5d --- /dev/null +++ b/doc/other_modules/jenny/language/expressions.md @@ -0,0 +1,3 @@ +# Expressions + +TODO diff --git a/doc/other_modules/jenny/language/language.md b/doc/other_modules/jenny/language/language.md new file mode 100644 index 000000000..32f5a90d6 --- /dev/null +++ b/doc/other_modules/jenny/language/language.md @@ -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 + +<> +<> // 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: + +- `<>` +- `<>` + +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 +Lines +Options +Commands +Expressions +``` diff --git a/doc/other_modules/jenny/language/lines.md b/doc/other_modules/jenny/language/lines.md new file mode 100644 index 000000000..f2bcd9325 --- /dev/null +++ b/doc/other_modules/jenny/language/lines.md @@ -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 +--- +<> + Professor: Welcome to the exam! + <> +<> + Professor: You have tried {plural($n_attempts, "% time")} already, but I \ + can give you another try. + <> +<> + Professor: You've failed 5 times in a row! How is this even possible? +<> +=== +``` + +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 \<> +\{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. +=== +``` diff --git a/doc/other_modules/jenny/language/markup.md b/doc/other_modules/jenny/language/markup.md new file mode 100644 index 000000000..435c201aa --- /dev/null +++ b/doc/other_modules/jenny/language/markup.md @@ -0,0 +1,3 @@ +# Markup + +TODO diff --git a/doc/other_modules/jenny/language/nodes.md b/doc/other_modules/jenny/language/nodes.md new file mode 100644 index 000000000..affecd806 --- /dev/null +++ b/doc/other_modules/jenny/language/nodes.md @@ -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 diff --git a/doc/other_modules/jenny/language/options.md b/doc/other_modules/jenny/language/options.md new file mode 100644 index 000000000..1c28fa3cd --- /dev/null +++ b/doc/other_modules/jenny/language/options.md @@ -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 `<>` +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 <= 50>> + <> + <> +-> I have so much money, here, take a 100 <= 10000>> + <> + <> + 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? + <> + <> + <> + You make a very reasonable point, sir, my apologies. + <> + <> +=== +``` + +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 diff --git a/doc/other_modules/jenny/runtime/dialogue_choice.md b/doc/other_modules/jenny/runtime/dialogue_choice.md new file mode 100644 index 000000000..33dd515f0 --- /dev/null +++ b/doc/other_modules/jenny/runtime/dialogue_choice.md @@ -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` +: The list of [DialogueOption]s comprising this choice set. + + +[Option]: ../language/options.md +[DialogueOption]: dialogue_option.md diff --git a/doc/other_modules/jenny/runtime/dialogue_line.md b/doc/other_modules/jenny/runtime/dialogue_line.md new file mode 100644 index 000000000..b71b3c152 --- /dev/null +++ b/doc/other_modules/jenny/runtime/dialogue_line.md @@ -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` +: 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` +: 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 diff --git a/doc/other_modules/jenny/runtime/dialogue_option.md b/doc/other_modules/jenny/runtime/dialogue_option.md new file mode 100644 index 000000000..163f29e67 --- /dev/null +++ b/doc/other_modules/jenny/runtime/dialogue_option.md @@ -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` +: 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` +: 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 diff --git a/doc/other_modules/jenny/runtime/dialogue_runner.md b/doc/other_modules/jenny/runtime/dialogue_runner.md new file mode 100644 index 000000000..5754db2ca --- /dev/null +++ b/doc/other_modules/jenny/runtime/dialogue_runner.md @@ -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` +: 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 + <> +=== + +title: Away +--- +<> +=== +``` + +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 diff --git a/doc/other_modules/jenny/runtime/dialogue_view.md b/doc/other_modules/jenny/runtime/dialogue_view.md new file mode 100644 index 000000000..6ec7475ec --- /dev/null +++ b/doc/other_modules/jenny/runtime/dialogue_view.md @@ -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`, 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 diff --git a/doc/other_modules/jenny/runtime/index.md b/doc/other_modules/jenny/runtime/index.md new file mode 100644 index 000000000..2b34460b6 --- /dev/null +++ b/doc/other_modules/jenny/runtime/index.md @@ -0,0 +1,14 @@ +# Jenny Runtime + +```{toctree} +:hidden: + +DialogueChoice +DialogueLine +DialogueOption +DialogueRunner +DialogueView +MarkupAttribute +Node +YarnProject +``` diff --git a/doc/other_modules/jenny/runtime/markup_attribute.md b/doc/other_modules/jenny/runtime/markup_attribute.md new file mode 100644 index 000000000..f71c39ad3 --- /dev/null +++ b/doc/other_modules/jenny/runtime/markup_attribute.md @@ -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` +: 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 diff --git a/doc/other_modules/jenny/runtime/node.md b/doc/other_modules/jenny/runtime/node.md new file mode 100644 index 000000000..79f161b04 --- /dev/null +++ b/doc/other_modules/jenny/runtime/node.md @@ -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` +: 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` +: 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 diff --git a/doc/other_modules/jenny/runtime/yarn_project.md b/doc/other_modules/jenny/runtime/yarn_project.md new file mode 100644 index 000000000..fd23c2a9a --- /dev/null +++ b/doc/other_modules/jenny/runtime/yarn_project.md @@ -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` +: 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: + + + - 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. + + +**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 diff --git a/doc/other_modules/other_modules.md b/doc/other_modules/other_modules.md index 99c556673..1be29af31 100644 --- a/doc/other_modules/other_modules.md +++ b/doc/other_modules/other_modules.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 +jenny +oxygen ``` diff --git a/doc/tutorials/klondike/step4.md b/doc/tutorials/klondike/step4.md index 6cfc2b683..e56766c5f 100644 --- a/doc/tutorials/klondike/step4.md +++ b/doc/tutorials/klondike/step4.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 diff --git a/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart b/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart index 64d04352d..64b7b406a 100644 --- a/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart +++ b/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart @@ -37,9 +37,7 @@ class DialogueRunner { }) : project = yarnProject, _dialogueViews = dialogueViews, _currentNodes = [], - _iterators = [] { - dialogueViews.forEach((dv) => dv.dialogueRunner = this); - } + _iterators = []; final YarnProject project; final List _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 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( diff --git a/packages/flame_jenny/jenny/lib/src/dialogue_view.dart b/packages/flame_jenny/jenny/lib/src/dialogue_view.dart index 4559468c8..e944e7fae 100644 --- a/packages/flame_jenny/jenny/lib/src/dialogue_view.dart +++ b/packages/flame_jenny/jenny/lib/src/dialogue_view.dart @@ -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 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 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 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 onDialogueFinish() {} - /// A future that never completes. @internal static Future never = Completer().future; diff --git a/packages/flame_jenny/jenny/lib/src/parse/token.dart b/packages/flame_jenny/jenny/lib/src/parse/token.dart index 5d45101c1..a188d96ac 100644 --- a/packages/flame_jenny/jenny/lib/src/parse/token.dart +++ b/packages/flame_jenny/jenny/lib/src/parse/token.dart @@ -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, diff --git a/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart b/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart index 5f28bdadc..9e50e8e01 100644 --- a/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart +++ b/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart @@ -601,6 +601,7 @@ class _Lexer { } else { final cu = currentCodeUnit; if (cu == $backslash || + cu == $colon || cu == $slash || cu == $hash || cu == $minus || diff --git a/packages/flame_jenny/jenny/lib/src/structure/dialogue_option.dart b/packages/flame_jenny/jenny/lib/src/structure/dialogue_option.dart index 06b1a9230..bdff10d74 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/dialogue_option.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/dialogue_option.dart @@ -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)'; } } diff --git a/packages/flame_jenny/jenny/lib/src/structure/node.dart b/packages/flame_jenny/jenny/lib/src/structure/node.dart index fda9adf3f..c9e7dc221 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/node.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/node.dart @@ -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 { const Node({ required this.title, - required this.content, - this.tags, - this.variables, - }); + required Block content, + Map? tags, + VariableStorage? variables, + }) : _content = content, + _tags = tags, + _variables = variables; final String title; - final Map? tags; - final Block content; - final VariableStorage? variables; + final Map? _tags; + final Block _content; + final VariableStorage? _variables; - List get lines => content.lines; + /// The list of extra tags specified in the node header. + Map get tags => _tags ?? const {}; + + /// 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 get lines => _content.lines; @override String toString() => 'Node($title)'; - - @override - NodeIterator get iterator => NodeIterator(this); } class NodeIterator implements Iterator { NodeIterator(this.node) { - diveInto(node.content); + diveInto(node._content); } final Node node; diff --git a/packages/flame_jenny/jenny/lib/src/yarn_project.dart b/packages/flame_jenny/jenny/lib/src/yarn_project.dart index 1252e7e18..7f64c2c9d 100644 --- a/packages/flame_jenny/jenny/lib/src/yarn_project.dart +++ b/packages/flame_jenny/jenny/lib/src/yarn_project.dart @@ -77,8 +77,4 @@ class YarnProject { void parse(String text) { impl.parse(text, this); } - - void setVariable(String name, dynamic value) { - variables.setVariable(name, value); - } } diff --git a/packages/flame_jenny/jenny/test/dialogue_view_test.dart b/packages/flame_jenny/jenny/test/dialogue_view_test.dart index e31dc85aa..8b015d5cb 100644 --- a/packages/flame_jenny/jenny/test/dialogue_view_test.dart +++ b/packages/flame_jenny/jenny/test/dialogue_view_test.dart @@ -155,8 +155,8 @@ class _RecordingDialogueView extends DialogueView { class _InterruptingCow extends DialogueView { @override FutureOr onLineStart(DialogueLine line) async { - dialogueRunner.sendSignal("I'm a banana!"); - dialogueRunner.stopLine(); + dialogueRunner!.sendSignal("I'm a banana!"); + dialogueRunner!.stopLine(); return false; } } diff --git a/packages/flame_jenny/jenny/test/parse/parse_test.dart b/packages/flame_jenny/jenny/test/parse/parse_test.dart index ab9b2e760..a3cace0b8 100644 --- a/packages/flame_jenny/jenny/test/parse/parse_test.dart +++ b/packages/flame_jenny/jenny/test/parse/parse_test.dart @@ -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()); - final line = block.lines[i] as DialogueLine; + expect(lines[i], isA()); + 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]); } diff --git a/packages/flame_jenny/jenny/test/parse/tokenize_test.dart b/packages/flame_jenny/jenny/test/parse/tokenize_test.dart index b88232ec8..9d38b964a 100644 --- a/packages/flame_jenny/jenny/test/parse/tokenize_test.dart +++ b/packages/flame_jenny/jenny/test/parse/tokenize_test.dart @@ -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' diff --git a/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart b/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart index b8647ebe6..5079f9cac 100644 --- a/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart +++ b/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart @@ -9,7 +9,7 @@ void main() { group('JumpCommand', () { test('<> command', () { final yarn = YarnProject() - ..setVariable(r'$target', 'DOWN') + ..variables.setVariable(r'$target', 'DOWN') ..parse('title:A\n---\n' '<>\n' '<>\n' diff --git a/packages/flame_jenny/jenny/test/structure/node_test.dart b/packages/flame_jenny/jenny/test/structure/node_test.dart index 9f7a94432..bf598a544 100644 --- a/packages/flame_jenny/jenny/test/structure/node_test.dart +++ b/packages/flame_jenny/jenny/test/structure/node_test.dart @@ -17,7 +17,7 @@ void main() { expect(node.title, 'Introduction'); expect(node.lines, []); - 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', () { diff --git a/packages/flame_jenny/jenny/test/test_scenario.dart b/packages/flame_jenny/jenny/test/test_scenario.dart index 53d966b25..35f787ad6 100644 --- a/packages/flame_jenny/jenny/test/test_scenario.dart +++ b/packages/flame_jenny/jenny/test/test_scenario.dart @@ -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++;