diff --git a/examples/assets/images/dialogue_box.png b/examples/assets/images/dialogue_box.png new file mode 100644 index 000000000..d9bdbbc66 Binary files /dev/null and b/examples/assets/images/dialogue_box.png differ diff --git a/examples/assets/images/green_button_sqr.png b/examples/assets/images/green_button_sqr.png new file mode 100644 index 000000000..f5edc8d12 Binary files /dev/null and b/examples/assets/images/green_button_sqr.png differ diff --git a/examples/assets/images/red_button_sqr.png b/examples/assets/images/red_button_sqr.png new file mode 100644 index 000000000..041864dd3 Binary files /dev/null and b/examples/assets/images/red_button_sqr.png differ diff --git a/examples/assets/yarn/advanced.yarn b/examples/assets/yarn/advanced.yarn new file mode 100644 index 000000000..7012d8008 --- /dev/null +++ b/examples/assets/yarn/advanced.yarn @@ -0,0 +1,21 @@ +<> +<> +title: gamble +--- +Jenny: Hello {$playerName}. This is a game of chance. +Jenny: You can win or lose up to 10 coins. Do you want to play? +-> No + Jenny: No bids made. +-> Yes + <> // returns a random value from -10 to 10 + <> + Jenny: Too bad, you did not win anything. + <> + Jenny: Bad luck. You lost {$winnings} coins. + Jenny: Play again to change your fortunes. + <> + Jenny: Congratulations! You won {$winnings} coins. + Jenny: Play again to win even more. + <> + <> +=== \ No newline at end of file diff --git a/examples/assets/yarn/simple.yarn b/examples/assets/yarn/simple.yarn new file mode 100644 index 000000000..f312b2362 --- /dev/null +++ b/examples/assets/yarn/simple.yarn @@ -0,0 +1,6 @@ +<> +title: hello_world +--- +Jenny: Hello world. My name is Jenny. +Jenny: Thanks for using Flame! +=== \ No newline at end of file diff --git a/examples/lib/main.dart b/examples/lib/main.dart index f4fb18a89..b7ac835fb 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -16,6 +16,7 @@ import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_ import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart'; import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart'; import 'package:examples/stories/bridge_libraries/flame_isolate/isolate.dart'; +import 'package:examples/stories/bridge_libraries/flame_jenny/jenny.dart'; import 'package:examples/stories/bridge_libraries/flame_lottie/lottie.dart'; import 'package:examples/stories/bridge_libraries/flame_spine/flame_spine.dart'; import 'package:examples/stories/camera_and_viewport/camera_and_viewport.dart'; @@ -98,6 +99,7 @@ void runAsDashbook() { // Bridge package examples addForge2DStories(dashbook); addFlameIsolateExample(dashbook); + addFlameJennyExample(dashbook); addFlameLottieExample(dashbook); addFlameSpineExamples(dashbook); diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/commons/commons.dart b/examples/lib/stories/bridge_libraries/flame_jenny/commons/commons.dart new file mode 100644 index 000000000..79f92abd1 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/commons/commons.dart @@ -0,0 +1,8 @@ +String baseLink(String path) { + const basePath = + 'https://github.com/flame-engine/flame/blob/main/packages/flame_jenny/'; + + return '$basePath$path'; +} + +const double fontSize = 24; diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/components/button_row.dart b/examples/lib/stories/bridge_libraries/flame_jenny/components/button_row.dart new file mode 100644 index 000000000..52524946d --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/components/button_row.dart @@ -0,0 +1,71 @@ +import 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart'; +import 'package:flame/components.dart'; +import 'package:jenny/jenny.dart'; + +class ButtonRow extends PositionComponent { + ButtonRow({required super.size}) : super(position: Vector2(0, 96)); + + void removeButtons() { + final buttonList = children.query(); + if (buttonList.isNotEmpty) { + for (final dialogueButton in buttonList) { + if (dialogueButton.parent != null) { + dialogueButton.removeFromParent(); + } + } + } + } + + void showNextButton(Function() onNextButtonPressed) { + removeButtons(); + final nextButton = DialogueButton( + assetPath: 'green_button_sqr.png', + text: 'Next', + position: Vector2(size.x / 2, 0), + onPressed: () { + onNextButtonPressed(); + removeButtons(); + }, + ); + add(nextButton); + } + + void showOptionButtons({ + required Function(int optionNumber) onChoice, + required DialogueOption option1, + required DialogueOption option2, + }) { + removeButtons(); + final optionButtons = [ + DialogueButton( + assetPath: 'green_button_sqr.png', + text: option1.text, + position: Vector2(size.x / 4, 0), + onPressed: () { + onChoice(0); + removeButtons(); + }, + ), + DialogueButton( + assetPath: 'red_button_sqr.png', + text: option2.text, + position: Vector2(size.x * 3 / 4, 0), + onPressed: () { + onChoice(1); + removeButtons(); + }, + ), + ]; + addAll(optionButtons); + } + + void showCloseButton(Function() onClose) { + final closeButton = DialogueButton( + assetPath: 'green_button_sqr.png', + text: 'Close', + onPressed: () => onClose(), + position: Vector2(size.x / 2, 0), + ); + add(closeButton); + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart new file mode 100644 index 000000000..4820ce8d0 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart @@ -0,0 +1,43 @@ +import 'package:examples/stories/bridge_libraries/flame_jenny/components/button_row.dart'; +import 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart'; +import 'package:flame/components.dart'; +import 'package:jenny/jenny.dart'; + +class DialogueBoxComponent extends SpriteComponent with HasGameReference { + DialogueTextBox textBox = DialogueTextBox(text: ''); + final Vector2 spriteSize = Vector2(736, 128); + late final ButtonRow buttonRow = ButtonRow(size: spriteSize); + + @override + Future onLoad() async { + position = Vector2(game.size.x / 2, 96); + anchor = Anchor.center; + sprite = await Sprite.load( + 'dialogue_box.png', + srcSize: spriteSize, + ); + await addAll([buttonRow, textBox]); + return super.onLoad(); + } + + void changeText(String newText, Function() goNextLine) { + textBox.text = newText; + buttonRow.showNextButton(goNextLine); + } + + void showOptions({ + required Function(int optionNumber) onChoice, + required DialogueOption option1, + required DialogueOption option2, + }) { + buttonRow.showOptionButtons( + onChoice: onChoice, + option1: option1, + option2: option2, + ); + } + + void showCloseButton(Function() onClose) { + buttonRow.showCloseButton(onClose); + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart new file mode 100644 index 000000000..f685782ce --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart @@ -0,0 +1,36 @@ +import 'package:examples/stories/bridge_libraries/flame_jenny/commons/commons.dart'; +import 'package:flame/components.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class DialogueButton extends SpriteButtonComponent { + DialogueButton({ + required super.position, + required this.assetPath, + required this.text, + required super.onPressed, + super.anchor = Anchor.center, + }); + + final String text; + final String assetPath; + + @override + Future onLoad() async { + button = await Sprite.load(assetPath); + add( + TextComponent( + text: text, + position: Vector2(48, 16), + anchor: Anchor.center, + size: Vector2(88, 28), + textRenderer: TextPaint( + style: const TextStyle( + fontSize: fontSize, + color: Colors.white70, + ), + ), + ), + ); + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart new file mode 100644 index 000000000..79580df5a --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart @@ -0,0 +1,85 @@ +import 'dart:async'; +import 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart'; +import 'package:flame/components.dart' hide Timer; +import 'package:jenny/jenny.dart'; + +class DialogueControllerComponent extends Component + with DialogueView, HasGameReference { + Completer _forwardCompleter = Completer(); + Completer _choiceCompleter = Completer(); + Completer _closeCompleter = Completer(); + late final DialogueBoxComponent _dialogueBoxComponent = + DialogueBoxComponent(); + + @override + Future onNodeStart(Node node) async { + _closeCompleter = Completer(); + _addDialogueBox(); + } + + void _addDialogueBox() { + game.camera.viewport.add(_dialogueBoxComponent); + } + + @override + Future onNodeFinish(Node node) async { + _dialogueBoxComponent.showCloseButton(_onClose); + return _closeCompleter.future; + } + + void _onClose() { + if (!_closeCompleter.isCompleted) { + _closeCompleter.complete(); + } + final list = game.camera.viewport.children.query(); + if (list.isNotEmpty) { + game.camera.viewport.removeAll(list); + } + } + + Future _advance() async { + return _forwardCompleter.future; + } + + @override + FutureOr onLineStart(DialogueLine line) async { + _forwardCompleter = Completer(); + _changeTextAndShowNextButton(line); + await _advance(); + return super.onLineStart(line); + } + + void _changeTextAndShowNextButton(DialogueLine line) { + final characterName = line.character?.name ?? ''; + final dialogueLineText = '$characterName: ${line.text}'; + _dialogueBoxComponent.changeText(dialogueLineText, _goNextLine); + } + + void _goNextLine() { + if (!_forwardCompleter.isCompleted) { + _forwardCompleter.complete(); + } + } + + @override + FutureOr onChoiceStart(DialogueChoice choice) async { + _forwardCompleter = Completer(); + _choiceCompleter = Completer(); + _dialogueBoxComponent.showOptions( + onChoice: _onChoice, + option1: choice.options[0], + option2: choice.options[1], + ); + await _advance(); + return _choiceCompleter.future; + } + + void _onChoice(int optionNumber) { + if (!_forwardCompleter.isCompleted) { + _forwardCompleter.complete(); + } + if (!_choiceCompleter.isCompleted) { + _choiceCompleter.complete(optionNumber); + } + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart new file mode 100644 index 000000000..fe38ebfcc --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart @@ -0,0 +1,17 @@ +import 'package:examples/stories/bridge_libraries/flame_jenny/commons/commons.dart'; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; + +class DialogueTextBox extends TextBoxComponent { + DialogueTextBox({required super.text}) + : super( + position: Vector2(16, 16), + size: Vector2(704, 96), + textRenderer: TextPaint( + style: const TextStyle( + fontSize: fontSize, + color: Colors.black, + ), + ), + ); +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/components/menu_button.dart b/examples/lib/stories/bridge_libraries/flame_jenny/components/menu_button.dart new file mode 100644 index 000000000..de1619652 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/components/menu_button.dart @@ -0,0 +1,35 @@ +import 'package:flame/components.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/material.dart'; + +class MenuButton extends ButtonComponent { + MenuButton({ + required super.position, + required super.onPressed, + required this.text, + }) : super(size: Vector2(128, 42)); + + late String text; + + final Paint white = BasicPalette.white.paint(); + final TextPaint topTextPaint = TextPaint( + style: TextStyle(color: BasicPalette.black.color), + ); + + @override + Future onLoad() async { + button = RectangleComponent(paint: white, size: size); + anchor = Anchor.center; + add( + TextComponent( + text: text, + textRenderer: topTextPaint, + position: size / 2, + anchor: Anchor.center, + priority: 1, + ), + ); + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/jenny.dart b/examples/lib/stories/bridge_libraries/flame_jenny/jenny.dart new file mode 100644 index 000000000..e9d512e76 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/jenny.dart @@ -0,0 +1,29 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/stories/bridge_libraries/flame_jenny/commons/commons.dart'; +import 'package:examples/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart'; +import 'package:flame/game.dart'; + +void addFlameJennyExample(Dashbook dashbook) { + dashbook.storiesOf('FlameJenny') + ..add( + 'Simple Jenny example', + (_) => GameWidget( + game: JennySimpleExample(), + ), + codeLink: baseLink( + 'bridge_libraries/flame_jenny/jenny_simple_example.dart', + ), + info: JennySimpleExample.description, + ) + ..add( + 'Advanced Jenny example', + (_) => GameWidget( + game: JennyAdvancedExample(), + ), + codeLink: baseLink( + 'bridge_libraries/flame_jenny/jenny_advanced_example.dart', + ), + info: JennyAdvancedExample.description, + ); +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart b/examples/lib/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart new file mode 100644 index 000000000..c7686aa2f --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart @@ -0,0 +1,76 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart'; +import 'package:examples/stories/bridge_libraries/flame_jenny/components/menu_button.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/services.dart'; +import 'package:jenny/jenny.dart'; + +class JennyAdvancedExample extends FlameGame { + static const String description = ''' + This is an advanced example of how to use the Jenny Package. + It includes implementing dialogue choices, setting custom variables, + using commands and implementing User-Defined Commands, . + '''; + + int coins = 0; + + final Paint white = BasicPalette.white.paint(); + final TextPaint mainTextPaint = TextPaint( + style: TextStyle(color: BasicPalette.white.color), + ); + final TextPaint buttonTextPaint = TextPaint( + style: TextStyle(color: BasicPalette.black.color), + ); + final startButtonSize = Vector2(128, 56); + + late final TextComponent header = TextComponent( + text: 'Select player name.', + position: Vector2(size.x / 2, 56), + size: startButtonSize, + anchor: Anchor.center, + textRenderer: mainTextPaint, + ); + + Future startDialogue(String playerName) async { + final dialogueControllerComponent = DialogueControllerComponent(); + add(dialogueControllerComponent); + + final yarnProject = YarnProject(); + + yarnProject + ..commands.addCommand1('updateCoins', updateCoins) + ..variables.setVariable(r'$playerName', playerName) + ..parse(await rootBundle.loadString('assets/yarn/advanced.yarn')); + final dialogueRunner = DialogueRunner( + yarnProject: yarnProject, + dialogueViews: [dialogueControllerComponent], + ); + dialogueRunner.startDialogue('gamble'); + } + + void updateCoins(int amountChange) { + coins += amountChange; + header.text = 'Select player name. Current coins: $coins'; + } + + @override + Future onLoad() async { + addAll([ + header, + MenuButton( + position: Vector2(size.x / 4, 128), + onPressed: () => startDialogue('Jessie'), + text: 'Jessie', + ), + MenuButton( + position: Vector2(size.x * 3 / 4, 128), + onPressed: () => startDialogue('James'), + text: 'James', + ), + ]); + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart b/examples/lib/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart new file mode 100644 index 000000000..7aacc0831 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart @@ -0,0 +1,36 @@ +import 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart'; +import 'package:examples/stories/bridge_libraries/flame_jenny/components/menu_button.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/services.dart'; +import 'package:jenny/jenny.dart'; + +class JennySimpleExample extends FlameGame { + static const String description = ''' + This is a simple example of how to use the Jenny Package. + It includes instantiating YarnProject and parsing a .yarn script. + '''; + + Future startDialogue() async { + final dialogueControllerComponent = DialogueControllerComponent(); + add(dialogueControllerComponent); + + final yarnProject = YarnProject(); + yarnProject.parse(await rootBundle.loadString('assets/yarn/simple.yarn')); + final dialogueRunner = DialogueRunner( + yarnProject: yarnProject, + dialogueViews: [dialogueControllerComponent], + ); + dialogueRunner.startDialogue('hello_world'); + } + + @override + Future onLoad() async { + addAll([ + MenuButton( + position: Vector2(size.x / 2, 96), + onPressed: startDialogue, + text: 'Start conversation', + ), + ]); + } +} diff --git a/examples/pubspec.yaml b/examples/pubspec.yaml index abd00ba16..0036431dc 100644 --- a/examples/pubspec.yaml +++ b/examples/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: flutter: sdk: flutter google_fonts: ^4.0.4 + jenny: ^1.3.0 meta: ^1.9.1 padracing: ^1.0.0 provider: ^6.0.5 @@ -49,3 +50,4 @@ flutter: - assets/tiles/ - assets/audio/music/ - assets/audio/sfx/ + - assets/yarn/