diff --git a/packages/flame_jenny/jenny/lib/jenny.dart b/packages/flame_jenny/jenny/lib/jenny.dart index ce868ff82fc..7d26f0cb1e4 100644 --- a/packages/flame_jenny/jenny/lib/jenny.dart +++ b/packages/flame_jenny/jenny/lib/jenny.dart @@ -1,2 +1,10 @@ -export 'src/errors.dart' show SyntaxError, NameError, TypeError; +export 'src/dialogue_runner.dart' show DialogueRunner; +export 'src/dialogue_view.dart' show DialogueView; +export 'src/errors.dart' show SyntaxError, NameError, TypeError, DialogueError; +export 'src/structure/dialogue_choice.dart' show DialogueChoice; +export 'src/structure/dialogue_line.dart' show DialogueLine; +export 'src/structure/expressions/expression_type.dart' show ExpressionType; +export 'src/structure/node.dart' show Node; +export 'src/structure/option.dart' show Option; +export 'src/variable_storage.dart' show VariableStorage; export 'src/yarn_project.dart' show YarnProject; diff --git a/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart b/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart new file mode 100644 index 00000000000..24811fb3b62 --- /dev/null +++ b/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:jenny/src/dialogue_view.dart'; +import 'package:jenny/src/errors.dart'; +import 'package:jenny/src/structure/block.dart'; +import 'package:jenny/src/structure/commands/command.dart'; +import 'package:jenny/src/structure/dialogue_choice.dart'; +import 'package:jenny/src/structure/dialogue_line.dart'; +import 'package:jenny/src/structure/node.dart'; +import 'package:jenny/src/structure/statement.dart'; +import 'package:jenny/src/yarn_project.dart'; +import 'package:meta/meta.dart'; + +/// [DialogueRunner] is a an engine in flame_yarn that runs a single dialogue. +/// +/// If you think of [YarnProject] as a dialogue "program", consisting of +/// multiple Nodes as "functions", then [DialogueRunner] is like a VM, capable +/// of executing a single "function" in 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 [DialogueRunner]s for the same +/// [YarnProject], and then they would be able to execute multiple dialogues at +/// once (for example, in a crowded room there could be multiple dialogues +/// occurring at once within 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, however, is +/// handled by the [DialogueView]s, not by the [DialogueRunner]. +class DialogueRunner { + DialogueRunner({ + required YarnProject yarnProject, + required List dialogueViews, + }) : project = yarnProject, + _dialogueViews = dialogueViews, + _currentNodes = [], + _iterators = []; + + final YarnProject project; + final List _dialogueViews; + final List _currentNodes; + final List _iterators; + _LineDeliveryPipeline? _linePipeline; + + /// Executes the node with the given name, and returns a future that finished + /// 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 nextLine = iterator.current; + switch (nextLine.kind) { + case StatementKind.line: + await _deliverLine(nextLine as DialogueLine); + break; + case StatementKind.choice: + await _deliverChoices(nextLine as DialogueChoice); + break; + case StatementKind.command: + await _deliverCommand(nextLine as Command); + break; + } + } else { + _iterators.removeLast(); + _currentNodes.removeLast(); + } + } + await _combineFutures( + [for (final view in _dialogueViews) view.onDialogueFinish()], + ); + } + + void sendSignal(dynamic signal) { + assert(_linePipeline != null); + final line = _linePipeline!.line; + for (final view in _dialogueViews) { + view.onLineSignal(line, signal); + } + } + + void stopLine() { + _linePipeline?.stop(); + } + + Future _deliverLine(DialogueLine line) async { + final pipeline = _LineDeliveryPipeline(line, _dialogueViews); + _linePipeline = pipeline; + pipeline.start(); + await pipeline.future; + _linePipeline = null; + } + + Future _deliverChoices(DialogueChoice choice) async { + // Compute which options are available and which aren't. This must be done + // only once, because some options may have non-deterministic conditionals + // which may produce different results on each invocation. + for (final option in choice.options) { + option.available = option.condition?.value ?? true; + } + final futures = [ + for (final view in _dialogueViews) view.onChoiceStart(choice) + ]; + if (futures.every((future) => future == DialogueView.never)) { + _error('No dialogue views capable of making a dialogue choice'); + } + final chosenIndex = await Future.any(futures); + if (chosenIndex < 0 || chosenIndex >= choice.options.length) { + _error('Invalid option index chosen in a dialogue: $chosenIndex'); + } + final chosenOption = choice.options[chosenIndex]; + if (!chosenOption.available) { + _error('A dialogue view selected a disabled option: $chosenOption'); + } + await _combineFutures( + [for (final view in _dialogueViews) view.onChoiceFinish(chosenOption)], + ); + enterBlock(chosenOption.block); + } + + FutureOr _deliverCommand(Command command) { + return command.execute(this); + } + + @internal + void enterBlock(Block block) { + _iterators.last.diveInto(block); + } + + @internal + Future jumpToNode(String nodeName) async { + _currentNodes.removeLast(); + _iterators.removeLast(); + return runNode(nodeName); + } + + @internal + void stop() { + _currentNodes.clear(); + _iterators.clear(); + } + + Future _combineFutures(List> maybeFutures) { + return Future.wait(>[ + for (final maybeFuture in maybeFutures) + if (maybeFuture is Future) maybeFuture + ]); + } + + Never _error(String message) { + stop(); + throw DialogueError(message); + } +} + +class _LineDeliveryPipeline { + _LineDeliveryPipeline(this.line, this.views) + : _completer = Completer(), + _futures = List.generate(views.length, (i) => null, growable: false); + + final DialogueLine line; + final List views; + final List> _futures; + final Completer _completer; + int _numPendingFutures = 0; + bool _interrupted = false; + + Future get future => _completer.future; + + void start() { + assert(_numPendingFutures == 0); + for (var i = 0; i < views.length; i++) { + final maybeFuture = views[i].onLineStart(line); + if (maybeFuture is Future) { + // ignore: cast_nullable_to_non_nullable + final future = maybeFuture as Future; + _futures[i] = future.then((_) => startCompleted(i)); + _numPendingFutures++; + } else { + continue; + } + } + if (_numPendingFutures == 0) { + finish(); + } + } + + void stop() { + _interrupted = true; + for (var i = 0; i < views.length; i++) { + if (_futures[i] != null) { + _futures[i] = views[i].onLineStop(line); + } + } + } + + void finish() { + assert(_numPendingFutures == 0); + for (var i = 0; i < views.length; i++) { + final maybeFuture = views[i].onLineFinish(line); + if (maybeFuture is Future) { + // ignore: unnecessary_cast + final future = maybeFuture as Future; + _futures[i] = future.then((_) => finishCompleted(i)); + _numPendingFutures++; + } else { + continue; + } + } + if (_numPendingFutures == 0) { + _completer.complete(); + } + } + + void startCompleted(int i) { + if (!_interrupted) { + assert(_futures[i] != null); + assert(_numPendingFutures > 0); + _futures[i] = null; + _numPendingFutures -= 1; + if (_numPendingFutures == 0) { + finish(); + } + } + } + + void finishCompleted(int i) { + assert(_futures[i] != null); + assert(_numPendingFutures > 0); + _futures[i] = null; + _numPendingFutures -= 1; + if (_numPendingFutures == 0) { + _completer.complete(); + } + } +} diff --git a/packages/flame_jenny/jenny/lib/src/dialogue_view.dart b/packages/flame_jenny/jenny/lib/src/dialogue_view.dart new file mode 100644 index 00000000000..23c25800a30 --- /dev/null +++ b/packages/flame_jenny/jenny/lib/src/dialogue_view.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:jenny/src/errors.dart'; +import 'package:jenny/src/structure/dialogue_choice.dart'; +import 'package:jenny/src/structure/dialogue_line.dart'; +import 'package:jenny/src/structure/node.dart'; +import 'package:jenny/src/structure/option.dart'; +import 'package:meta/meta.dart'; + +abstract class DialogueView { + const DialogueView(); + + /// Called before the start of a new dialogue, i.e. before any lines, options, + /// or commands are delivered. + /// + /// This method is a good place to prepare the game's UI, such as fading in/ + /// animating dialogue panels, or loading resources. If this method returns a + /// future, then the dialogue will start running only after the future + /// completes. + FutureOr onDialogueStart() {} + + /// Called when the dialogue enters a new [node]. + /// + /// This will be called immediately after the [onDialogueStart], and then + /// possibly several times more over the course of the dialogue if it visits + /// multiple nodes. + /// + /// If this method returns a future, then the dialogue runner will wait for it + /// to complete before proceeding with the actual dialogue. + FutureOr onNodeStart(Node node) {} + + /// 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" [DialogueView] should return `true`, + /// while all others `false` (especially if a 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 the + /// line was not shown to the user in a meaningful way. + /// + /// If this method returns a future, then the dialogue runner will wait for + /// that future to complete before advancing to the next line. If multiple + /// [DialogueView]s return such futures, then the dialogue runner will wait + /// for all of them to complete before proceeding. + /// + /// Returning a future is quite common for non-trivial [DialogueView]s. After + /// all, if this method were to return immediately, the dialogue runner would + /// immediately advance to the next line, and the player wouldn't have time + /// to read the first one. 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 *show* the line to the player, so + /// do not try to hide it at the end -- for that, there is a dedicated method + /// [onLineFinish]. + /// + /// Also, given that this method may take a significant amount of time, there + /// are two additional methods that may attempt to interfere into this + /// process: [onLineSignal] and [onLineStop]. + FutureOr onLineStart(DialogueLine line) => false; + + /// Called when the game sends a [signal] to all dialogue views. + /// + /// The signal will be sent to all views, regardless of whether they have + /// finished running [onLineStart] or not. The interpretation of the signal + /// and the appropriate response is up to the [DialogueView]. + /// + /// For example, one possible scenario would be to speed up a typewriter + /// effect and reveal the text immediately in response to the RUSH signal. + /// Or make some kind of an interjection in response to an OMG event. 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. + void onLineSignal(DialogueLine line, dynamic signal) {} + + /// Called when the game demands that the [line] finished presenting as soon + /// as possible. + /// + /// By itself, the dialogue runner will never call this method. However, it + /// may be invoked as a result of an explicit request by the game (or by one + /// of the dialogue views). Examples when this could be appropriate: (a) the + /// player was hit while talking to an NPC -- better stop talking and fight + /// for your life, (b) the user has pressed a "skip dialogue" button, so we + /// should stop the current line and proceed to the next one ASAP. + /// + /// This method returns a future that will be awaited before continuing to + /// the next line of the dialogue. At the same time, any future that's still + /// pending from the [onLineStart] call will be discarded and will no longer + /// be awaited. The [onLineFinish] method will not be called either. + FutureOr onLineStop(DialogueLine line) {} + + /// Called when the [line] has finished presenting in all dialog views. + /// + /// Some dialog views may need to clear their display when this event happens, + /// or make some other preparations to receive the next dialogue line. If this + /// method returns a future, that future will be awaited before proceeding to + /// the next line in the dialogue. + FutureOr onLineFinish(DialogueLine line) {} + + /// Called when the dialogue arrives at an option set, and the player must now + /// make a choice on how to proceed. If a dialogue view presents this choice + /// to the player and allows them to make a selection, then it must return a + /// future that completes when the choice is made. If the dialogue view does + /// not display menu choice, then it should return a future that never + /// completes (which is the default implementation). + /// + /// The dialogue runner will assume the choice has been made whenever any of + /// the dialogue views will have completed their futures. If none of the + /// dialogue views are capable of making a choice, then the dialogue will get + /// stuck. + /// + /// The future returned by this method should deliver an integer value of the + /// index of the option that was selected. This index must not exceed the + /// length of the [choice] list, and the indicated option must not be marked + /// as "unavailable". If these conditions are violated, an exception will be + /// raised. + Future onChoiceStart(DialogueChoice choice) => never; + + /// Called when the choice has been made, and the [option] was selected. + /// + /// If this method returns a future, the dialogue runner will wait for that + /// future to complete before proceeding with the dialogue. + FutureOr onChoiceFinish(Option option) {} + + /// 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/errors.dart b/packages/flame_jenny/jenny/lib/src/errors.dart index 7deef19aed2..416b6f9cb15 100644 --- a/packages/flame_jenny/jenny/lib/src/errors.dart +++ b/packages/flame_jenny/jenny/lib/src/errors.dart @@ -1,5 +1,5 @@ class SyntaxError implements Exception { - SyntaxError([this.message]); + SyntaxError(this.message); final String? message; @@ -11,7 +11,7 @@ class SyntaxError implements Exception { /// variable name, unknown node title, unrecognized function, unspecified /// command, etc. class NameError implements Exception { - NameError([this.message]); + NameError(this.message); final String? message; @@ -20,10 +20,19 @@ class NameError implements Exception { } class TypeError implements Exception { - TypeError([this.message]); + TypeError(this.message); final String? message; @override String toString() => 'TypeError: $message'; } + +class DialogueError implements Exception { + DialogueError(this.message); + + final String? message; + + @override + String toString() => 'DialogueError: $message'; +} diff --git a/packages/flame_jenny/jenny/lib/src/parse/parse.dart b/packages/flame_jenny/jenny/lib/src/parse/parse.dart index a05a8ebd82a..102f486386b 100644 --- a/packages/flame_jenny/jenny/lib/src/parse/parse.dart +++ b/packages/flame_jenny/jenny/lib/src/parse/parse.dart @@ -1,13 +1,15 @@ import 'package:jenny/src/errors.dart'; import 'package:jenny/src/parse/token.dart'; import 'package:jenny/src/parse/tokenize.dart'; +import 'package:jenny/src/structure/block.dart'; import 'package:jenny/src/structure/commands/command.dart'; import 'package:jenny/src/structure/commands/if_command.dart'; import 'package:jenny/src/structure/commands/jump_command.dart'; import 'package:jenny/src/structure/commands/set_command.dart'; import 'package:jenny/src/structure/commands/stop_command.dart'; import 'package:jenny/src/structure/commands/wait_command.dart'; -import 'package:jenny/src/structure/dialogue.dart'; +import 'package:jenny/src/structure/dialogue_choice.dart'; +import 'package:jenny/src/structure/dialogue_line.dart'; import 'package:jenny/src/structure/expressions/arithmetic.dart'; import 'package:jenny/src/structure/expressions/expression.dart'; import 'package:jenny/src/structure/expressions/functions.dart'; @@ -41,11 +43,11 @@ class _Parser { void parseMain() { while (position < tokens.length) { final header = parseNodeHeader(); - final statements = parseNodeBody(); + final block = parseNodeBody(); project.nodes[header.title!] = Node( title: header.title!, tags: header.tags, - lines: statements, + content: block, ); } } @@ -82,41 +84,45 @@ class _Parser { return _NodeHeader(title, tags.isEmpty ? null : tags); } - List parseNodeBody() { - final out = []; + Block parseNodeBody() { take(Token.startBody); if (peekToken() == Token.startIndent) { syntaxError('unexpected indent'); } - out.addAll(parseStatementList()); + final out = parseStatementList(); take(Token.endBody); return out; } - List parseStatementList() { - final result = []; + Block parseStatementList() { + final lines = []; while (true) { final nextToken = peekToken(); if (nextToken == Token.arrow) { - result.add(parseOption()); + final option = parseOption(); + if (lines.isNotEmpty && lines.last is DialogueChoice) { + (lines.last as DialogueChoice).options.add(option); + } else { + lines.add(DialogueChoice([option])); + } } else if (nextToken == Token.startCommand) { - result.add(parseCommand()); + lines.add(parseCommand()); } else if (nextToken.isText || nextToken.isPerson || nextToken == Token.startExpression) { - result.add(parseDialogueLine()); + lines.add(parseDialogueLine()); } else { break; } } - return result; + return Block(lines); } //#region Line parsing /// Consumes a regular line of text from the input, up to and including the /// NEWLINE token. - Dialogue parseDialogueLine() { + DialogueLine parseDialogueLine() { final person = maybeParseLinePerson(); final content = parseLineContent(); final tags = maybeParseHashtags(); @@ -124,8 +130,8 @@ class _Parser { syntaxError('commands are not allowed on a dialogue line'); } takeNewline(); - return Dialogue( - person: person, + return DialogueLine( + character: person, content: content, tags: tags, ); @@ -141,7 +147,7 @@ class _Parser { syntaxError('multiple commands are not allowed on an option line'); } take(Token.newline); - var block = const []; + var block = const Block.empty(); if (peekToken() == Token.startIndent) { position += 1; block = parseStatementList(); @@ -149,7 +155,7 @@ class _Parser { } return Option( content: content, - person: person, + character: person, tags: tags, condition: condition, block: block, @@ -317,15 +323,15 @@ class _Parser { take(Token.endCommand); take(Token.newline); if (peekToken() == Token.startCommand) { - return IfBlock(expression as BoolExpression, []); + return IfBlock(expression as BoolExpression, const Block.empty()); } if (peekToken() != Token.startIndent) { syntaxError('the body of the <<$commandName>> command must be indented'); } take(Token.startIndent); - final statements = parseStatementList(); + final block = parseStatementList(); take(Token.endIndent); - return IfBlock(expression as BoolExpression, statements); + return IfBlock(expression as BoolExpression, block); } IfBlock parseCommandElseBlock() { @@ -334,7 +340,7 @@ class _Parser { take(Token.endCommand); take(Token.newline); if (peekToken() == Token.startCommand) { - return const IfBlock(constTrue, []); + return const IfBlock(constTrue, Block.empty()); } if (peekToken() != Token.startIndent) { syntaxError('the body of the <> command must be indented'); diff --git a/packages/flame_jenny/jenny/lib/src/structure/block.dart b/packages/flame_jenny/jenny/lib/src/structure/block.dart new file mode 100644 index 00000000000..ae254b199f4 --- /dev/null +++ b/packages/flame_jenny/jenny/lib/src/structure/block.dart @@ -0,0 +1,12 @@ +import 'package:jenny/src/structure/statement.dart'; + +class Block { + const Block(this.lines); + const Block.empty() : lines = const []; + + final List lines; + + int get length => lines.length; + bool get isEmpty => lines.isEmpty; + bool get isNotEmpty => lines.isNotEmpty; +} diff --git a/packages/flame_jenny/jenny/lib/src/structure/commands/command.dart b/packages/flame_jenny/jenny/lib/src/structure/commands/command.dart index 15cebb2d6eb..972e589fd29 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/commands/command.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/commands/command.dart @@ -1,8 +1,13 @@ +import 'dart:async'; + +import 'package:jenny/src/dialogue_runner.dart'; import 'package:jenny/src/structure/statement.dart'; -import 'package:jenny/src/yarn_project.dart'; abstract class Command extends Statement { const Command(); - void execute(YarnProject project); + FutureOr execute(DialogueRunner dialogue); + + @override + StatementKind get kind => StatementKind.command; } diff --git a/packages/flame_jenny/jenny/lib/src/structure/commands/if_command.dart b/packages/flame_jenny/jenny/lib/src/structure/commands/if_command.dart index b0f09c041c5..a41bf83b63e 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/commands/if_command.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/commands/if_command.dart @@ -1,7 +1,7 @@ +import 'package:jenny/src/dialogue_runner.dart'; +import 'package:jenny/src/structure/block.dart'; import 'package:jenny/src/structure/commands/command.dart'; import 'package:jenny/src/structure/expressions/expression.dart'; -import 'package:jenny/src/structure/statement.dart'; -import 'package:jenny/src/yarn_project.dart'; class IfCommand extends Command { const IfCommand(this.ifs); @@ -13,10 +13,10 @@ class IfCommand extends Command { final List ifs; @override - void execute(YarnProject runtime) { - for (final block in ifs) { - if (block.condition.value) { - // runtime.executeEntries(block.entries); + void execute(DialogueRunner dialogue) { + for (final ifBlock in ifs) { + if (ifBlock.condition.value) { + dialogue.enterBlock(ifBlock.block); break; } } @@ -27,5 +27,5 @@ class IfBlock { const IfBlock(this.condition, this.block); final BoolExpression condition; - final List block; + final Block block; } diff --git a/packages/flame_jenny/jenny/lib/src/structure/commands/jump_command.dart b/packages/flame_jenny/jenny/lib/src/structure/commands/jump_command.dart index b2e8a5b5f4f..56aa72ca9a3 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/commands/jump_command.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/commands/jump_command.dart @@ -1,6 +1,6 @@ +import 'package:jenny/src/dialogue_runner.dart'; import 'package:jenny/src/structure/commands/command.dart'; import 'package:jenny/src/structure/expressions/expression.dart'; -import 'package:jenny/src/yarn_project.dart'; class JumpCommand extends Command { const JumpCommand(this.target); @@ -8,7 +8,7 @@ class JumpCommand extends Command { final StringExpression target; @override - void execute(YarnProject project) { - project.jumpToNode(target.value); + Future execute(DialogueRunner dialogue) { + return dialogue.jumpToNode(target.value); } } diff --git a/packages/flame_jenny/jenny/lib/src/structure/commands/set_command.dart b/packages/flame_jenny/jenny/lib/src/structure/commands/set_command.dart index 483b8d1027a..c28dcb13221 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/commands/set_command.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/commands/set_command.dart @@ -1,6 +1,6 @@ +import 'package:jenny/src/dialogue_runner.dart'; import 'package:jenny/src/structure/commands/command.dart'; import 'package:jenny/src/structure/expressions/expression.dart'; -import 'package:jenny/src/yarn_project.dart'; class SetCommand extends Command { const SetCommand(this.variable, this.expression); @@ -9,7 +9,7 @@ class SetCommand extends Command { final Expression expression; @override - void execute(YarnProject runtime) { - runtime.variables.setVariable(variable, expression.value); + void execute(DialogueRunner dialogue) { + dialogue.project.variables.setVariable(variable, expression.value); } } diff --git a/packages/flame_jenny/jenny/lib/src/structure/commands/stop_command.dart b/packages/flame_jenny/jenny/lib/src/structure/commands/stop_command.dart index a7f4ca0494c..8cb5f9a3df6 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/commands/stop_command.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/commands/stop_command.dart @@ -1,11 +1,11 @@ +import 'package:jenny/src/dialogue_runner.dart'; import 'package:jenny/src/structure/commands/command.dart'; -import 'package:jenny/src/yarn_project.dart'; class StopCommand extends Command { const StopCommand(); @override - void execute(YarnProject project) { - project.stopNode(); + void execute(DialogueRunner dialogue) { + dialogue.stop(); } } diff --git a/packages/flame_jenny/jenny/lib/src/structure/commands/wait_command.dart b/packages/flame_jenny/jenny/lib/src/structure/commands/wait_command.dart index 9791157ea62..142644951aa 100644 --- a/packages/flame_jenny/jenny/lib/src/structure/commands/wait_command.dart +++ b/packages/flame_jenny/jenny/lib/src/structure/commands/wait_command.dart @@ -1,6 +1,6 @@ +import 'package:jenny/src/dialogue_runner.dart'; import 'package:jenny/src/structure/commands/command.dart'; import 'package:jenny/src/structure/expressions/expression.dart'; -import 'package:jenny/src/yarn_project.dart'; class WaitCommand extends Command { const WaitCommand(this.arg); @@ -8,7 +8,10 @@ class WaitCommand extends Command { final NumExpression arg; @override - void execute(YarnProject project) { - project.wait(arg.value.toDouble()); + Future execute(DialogueRunner dialogue) { + final duration = Duration( + microseconds: (arg.value.toDouble() * 1000000).toInt(), + ); + return Future.delayed(duration); } } diff --git a/packages/flame_jenny/jenny/lib/src/structure/dialogue.dart b/packages/flame_jenny/jenny/lib/src/structure/dialogue.dart deleted file mode 100644 index ace76b5de9d..00000000000 --- a/packages/flame_jenny/jenny/lib/src/structure/dialogue.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:jenny/src/structure/expressions/expression.dart'; -import 'package:jenny/src/structure/statement.dart'; - -class Dialogue extends Statement { - const Dialogue({ - this.person, - required this.content, - this.tags, - }); - - final String? person; - final StringExpression content; - final List? tags; -} diff --git a/packages/flame_jenny/jenny/lib/src/structure/dialogue_choice.dart b/packages/flame_jenny/jenny/lib/src/structure/dialogue_choice.dart new file mode 100644 index 00000000000..91f320ba1aa --- /dev/null +++ b/packages/flame_jenny/jenny/lib/src/structure/dialogue_choice.dart @@ -0,0 +1,14 @@ +import 'package:jenny/src/structure/option.dart'; +import 'package:jenny/src/structure/statement.dart'; + +class DialogueChoice extends Statement { + const DialogueChoice(this.options); + + final List