Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added command <<visit>> #2233

Merged
merged 5 commits into from
Dec 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/other_modules/jenny/language/commands/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ scripts. For a full description of these commands, see the document on [user-def
**[\<\<stop\>\>](stop.md)**
: Stops executing the current node.

**[\<\<visit\>\>](visit.md)**
: Temporarily jumps to another node, and then comes back.

**[\<\<wait\>\>](wait.md)**
: Pauses the dialogue for the specified amount of time.

Expand All @@ -53,6 +56,7 @@ scripts. For a full description of these commands, see the document on [user-def
<<local>> <local.md>
<<set>> <set.md>
<<stop>> <stop.md>
<<visit>> <visit.md>
<<wait>> <wait.md>
User-defined commands <user_defined_commands.md>
```
6 changes: 6 additions & 0 deletions doc/other_modules/jenny/language/commands/jump.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ node ID, or as an expression in curly braces:

If the expression evaluates at runtime to an unknown name, then a `NameError` exception will be
thrown.


## See Also

- [\<\<visit\>\>](visit.md) command, which jumps into the destination node temporarily and then
returns to the same place in the dialogue as before.
38 changes: 38 additions & 0 deletions doc/other_modules/jenny/language/commands/visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `<<visit>>`

The **\<\<visit\>\>** command temporarily puts the current node on hold, executes the target node,
and after it finishes, resumes execution of the previous node. This is similar to a function call
in many programming languages.

The `<<visit>>` command can be useful for splitting a large dialogue into several smaller nodes,
or for reusing some common dialogue lines in several nodes. For example:

```yarn
title: RoamingTrader1
---
<<if $roaming_trader_introduced>>
Hello again, {$player}!
<<else>>
<<visit RoamingTraderIntro>>
<<endif>>

-> What do you think about the Calamity? <<if $calamity_started>>
<<visit RoamingTrader_Calamity>>
-> Have you seen a weird-looking girl running by? <<if $quest_little_girl>>
<<visit RoamingTrader_LittleGirl>>
-> What do you have for trade?
<<OpenTrade>>

Pleasure doing business with you! #auto
===
```

The argument of this command is the id of the node to jump to. It can be given either as a plain
node ID, or as an expression in curly braces:

```yarn
<<visit {"RewardChoice_" + string($choice)}>>
```

If the expression evaluates at runtime to an unknown name, then a `NameError` exception will be
thrown.
1 change: 1 addition & 0 deletions packages/flame_jenny/jenny/lib/src/command_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class CommandStorage {
'set',
'stop',
'stop',
'visit',
'wait',
'while',
];
Expand Down
49 changes: 29 additions & 20 deletions packages/flame_jenny/jenny/lib/src/dialogue_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,7 @@ class DialogueRunner {
view.dialogueRunner = this;
});
await _event((view) => view.onDialogueStart());
_nextNode = nodeName;
while (_nextNode != null) {
await _runNode(_nextNode!);
}
await _runNode(nodeName);
await _event((view) => view.onDialogueFinish());
} finally {
_dialogueViews.forEach((dv) => dv.dialogueRunner = null);
Expand All @@ -92,25 +89,28 @@ class DialogueRunner {
}

Future<void> _runNode(String nodeName) async {
final node = project.nodes[nodeName];
if (node == null) {
throw NameError('Node "$nodeName" could not be found');
}
_nextNode = nodeName;
while (_nextNode != null) {
final node = project.nodes[_nextNode!];
if (node == null) {
throw NameError('Node "$_nextNode" could not be found');
}

_nextNode = null;
_currentNode = node;
_currentIterator = node.iterator;
_nextNode = null;
_currentNode = node;
_currentIterator = node.iterator;

await _event((view) => view.onNodeStart(node));
while (_currentIterator?.moveNext() ?? false) {
final entry = _currentIterator!.current;
await entry.processInDialogueRunner(this);
}
_incrementNodeVisitCount();
await _event((view) => view.onNodeFinish(node));
await _event((view) => view.onNodeStart(node));
while (_currentIterator?.moveNext() ?? false) {
final entry = _currentIterator!.current;
await entry.processInDialogueRunner(this);
}
_incrementNodeVisitCount();
await _event((view) => view.onNodeFinish(node));

_currentNode = null;
_currentIterator = null;
_currentNode = null;
_currentIterator = null;
}
}

void _incrementNodeVisitCount() {
Expand Down Expand Up @@ -196,6 +196,15 @@ class DialogueRunner {
_nextNode = nodeName;
}

@internal
Future<void> visitNode(String nodeName) async {
final node = _currentNode;
final iterator = _currentIterator;
await _runNode(nodeName);
_currentNode = node;
_currentIterator = iterator;
}

/// Similar to `Future.wait()`, but accepts `FutureOr`s.
FutureOr<void> _combineFutures(List<FutureOr<void>> maybeFutures) {
final futures = maybeFutures.whereType<Future<void>>().toList();
Expand Down
16 changes: 10 additions & 6 deletions packages/flame_jenny/jenny/lib/src/parse/parse.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:jenny/src/structure/commands/local_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/user_defined_command.dart';
import 'package:jenny/src/structure/commands/visit_command.dart';
import 'package:jenny/src/structure/commands/wait_command.dart';
import 'package:jenny/src/structure/dialogue_choice.dart';
import 'package:jenny/src/structure/dialogue_entry.dart';
Expand Down Expand Up @@ -400,8 +401,8 @@ class _Parser {
final token = peekToken(1);
if (token == Token.commandIf) {
return parseCommandIf();
} else if (token == Token.commandJump) {
return parseCommandJump();
} else if (token == Token.commandJump || token == Token.commandVisit) {
return parseCommandJumpOrVisit();
} else if (token == Token.commandStop) {
return parseCommandStop();
} else if (token == Token.commandWait) {
Expand Down Expand Up @@ -493,9 +494,12 @@ class _Parser {
return IfBlock(constTrue, statements);
}

Command parseCommandJump() {
Command parseCommandJumpOrVisit() {
take(Token.startCommand);
take(Token.commandJump);
final isJump = peekToken() == Token.commandJump;
final isVisit = peekToken() == Token.commandVisit;
assert(isJump || isVisit);
position += 1;
final token = peekToken();
StringExpression target;
if (token.isId) {
Expand All @@ -516,7 +520,7 @@ class _Parser {
}
take(Token.endCommand);
take(Token.newline);
return JumpCommand(target);
return isJump ? JumpCommand(target) : VisitCommand(target);
}

Command parseCommandStop() {
Expand Down Expand Up @@ -724,9 +728,9 @@ class _Parser {
/// The initial [lhs] sub-expression is provided, and the parsing position
/// should be at the start of the next operator.
Expression _parseExpressionImpl(Expression lhs, int minPrecedence) {
var position0 = position;
var result = lhs;
while ((precedences[peekToken()] ?? -1) >= minPrecedence) {
var position0 = position;
final op = peekToken();
final opPrecedence = precedences[op]!;
position += 1;
Expand Down
2 changes: 2 additions & 0 deletions packages/flame_jenny/jenny/lib/src/parse/token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Token {
static const commandLocal = Token._(TokenType.commandLocal);
static const commandSet = Token._(TokenType.commandSet);
static const commandStop = Token._(TokenType.commandStop);
static const commandVisit = Token._(TokenType.commandVisit);
static const commandWait = Token._(TokenType.commandWait);
static const constFalse = Token._(TokenType.constFalse);
static const constTrue = Token._(TokenType.constTrue);
Expand Down Expand Up @@ -129,6 +130,7 @@ enum TokenType {
commandLocal, // 'local'
commandSet, // 'set'
commandStop, // 'stop'
commandVisit, // 'visit'
commandWait, // 'wait'
constFalse, // 'false'
constTrue, // 'true'
Expand Down
19 changes: 16 additions & 3 deletions packages/flame_jenny/jenny/lib/src/parse/tokenize.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ class _Lexer {
lineStart = 0,
tokens = [],
modeStack = [],
indentStack = [];
indentStack = [],
assert(
commandTokens.length ==
simpleCommands.length +
bareExpressionCommands.length +
nodeTargetingCommands.length,
);

final String text;
final List<Token> tokens;
Expand Down Expand Up @@ -229,11 +235,12 @@ class _Lexer {
(bareExpressionCommands.contains(tokens.last) &&
pushToken(Token.startExpression, position) &&
pushMode(modeCommandExpression)) ||
(tokens.last == Token.commandJump &&
(nodeTargetingCommands.contains(tokens.last) &&
(eatId() ||
(eatExpressionStart() &&
pushMode(modeTextExpression)) ||
error('an ID or an expression expected'))) ||
error('an ID or an expression in curly braces '
'expected'))) ||
(tokens.last.isCommand && // user-defined commands
pushMode(modeCommandText)))) ||
(eatCommandEnd() && popMode(modeCommand)) ||
Expand Down Expand Up @@ -973,6 +980,7 @@ class _Lexer {
'local': Token.commandLocal,
'set': Token.commandSet,
'stop': Token.commandStop,
'visit': Token.commandVisit,
'wait': Token.commandWait,
};

Expand All @@ -993,6 +1001,11 @@ class _Lexer {
Token.commandWait,
};

static final Set<Token> nodeTargetingCommands = {
Token.commandJump,
Token.commandVisit,
};

/// Throws a [SyntaxError] with the given [message], augmenting it with the
/// information about the current parsing location.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:jenny/src/dialogue_runner.dart';
import 'package:jenny/src/structure/commands/command.dart';
import 'package:jenny/src/structure/expressions/expression.dart';

class VisitCommand extends Command {
VisitCommand(this.target);

final StringExpression target;

@override
Future<void> execute(DialogueRunner dialogue) {
return dialogue.visitNode(target.value);
}

@override
String get name => 'visit';
}
56 changes: 56 additions & 0 deletions packages/flame_jenny/jenny/test/dialogue_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,62 @@ void main() {
);
});

test('jumps and visits', () async {
final yarn = YarnProject()
..parse(
dedent('''
title: Start
---
First line
<<visit AnotherNode>>
Second line
<<jump SomewhereElse>>
===
title: AnotherNode
---
Inside another node
<<jump SomewhereElse>>
===
title: SomewhereElse
---
This is nowhere...
===
'''),
);
final view1 = _DefaultDialogueView();
final view2 = _RecordingDialogueView();
final dialogueRunner = DialogueRunner(
yarnProject: yarn,
dialogueViews: [view1, view2],
);
await dialogueRunner.startDialogue('Start');
expect(
view2.events,
const [
'onDialogueStart',
'onNodeStart(Start)',
'onLineStart(First line)',
'onLineFinish(First line)',
'onNodeStart(AnotherNode)',
'onLineStart(Inside another node)',
'onLineFinish(Inside another node)',
'onNodeFinish(AnotherNode)',
'onNodeStart(SomewhereElse)',
'onLineStart(This is nowhere...)',
'onLineFinish(This is nowhere...)',
'onNodeFinish(SomewhereElse)',
'onLineStart(Second line)',
'onLineFinish(Second line)',
'onNodeFinish(Start)',
'onNodeStart(SomewhereElse)',
'onLineStart(This is nowhere...)',
'onLineFinish(This is nowhere...)',
'onNodeFinish(SomewhereElse)',
'onDialogueFinish()',
],
);
});

test('stop line', () async {
final yarn = YarnProject()
..parse(
Expand Down
1 change: 1 addition & 0 deletions packages/flame_jenny/jenny/test/parse/token_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ void main() {
expect('${Token.colon}', 'Token.colon');
expect('${Token.commandEndif}', 'Token.commandEndif');
expect('${Token.commandLocal}', 'Token.commandLocal');
expect('${Token.commandVisit}', 'Token.commandVisit');
expect('${Token.constTrue}', 'Token.constTrue');
expect('${Token.endIndent}', 'Token.endIndent');
expect('${Token.endMarkupTag}', 'Token.endMarkupTag');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ void main() {
'===\n',
),
hasSyntaxError(
'SyntaxError: an ID or an expression expected\n'
'SyntaxError: an ID or an expression in curly braces expected\n'
'> at line 3 column 8:\n'
'> <<jump 1>>\n'
'> ^\n',
Expand Down
Loading