mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
feat: Adding FlameConsole (#3329)
Adding Flame Console package. --------- Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
This commit is contained in:
109
doc/bridge_packages/flame_console/flame_console.md
Normal file
109
doc/bridge_packages/flame_console/flame_console.md
Normal file
@ -0,0 +1,109 @@
|
||||
# flame_console
|
||||
|
||||
Flame Console is a terminal overlay for Flame games which allows developers to debug and interact
|
||||
with their games.
|
||||
|
||||
It offers an overlay that can be plugged in to your `GameWidget` which when activated will show a
|
||||
terminal-like interface written with Flutter widgets where commands can be executed to see
|
||||
information about the running game and components, or perform actions.
|
||||
|
||||
It comes with a set of built-in commands, but it is also possible to add custom commands.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Flame Console is an overlay, so to use it, you will need to register it in your game widget.
|
||||
|
||||
Then, showing the overlay is up to you, below we see an example of a floating action button that will
|
||||
show the console when pressed.
|
||||
|
||||
```dart
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: GameWidget(
|
||||
game: _game,
|
||||
overlayBuilderMap: {
|
||||
'console': (BuildContext context, MyGame game) => ConsoleView(
|
||||
game: game,
|
||||
onClose: () {
|
||||
_game.overlays.remove('console');
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'console_button',
|
||||
onPressed: () {
|
||||
_game.overlays.add('console');
|
||||
},
|
||||
child: const Icon(Icons.developer_mode),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Built-in commands
|
||||
|
||||
- `help` - List available commands and their usage.
|
||||
- `ls` - List components.
|
||||
- `rm` - Remove components.
|
||||
- `debug` - Toggle debug mode on components.
|
||||
- `pause` - Pauses the game loop.
|
||||
- `resume` -Resumes the game loop.
|
||||
|
||||
|
||||
## Custom commands
|
||||
|
||||
Custom commands can be created by extending the `ConsoleCommand` class and adding them to the
|
||||
the `customCommands` list in the `ConsoleView` widget.
|
||||
|
||||
```dart
|
||||
class MyCustomCommand extends ConsoleCommand<MyGame> {
|
||||
MyCustomCommand();
|
||||
|
||||
@override
|
||||
String get name => 'my_command';
|
||||
|
||||
@override
|
||||
String get description => 'Description of my command';
|
||||
|
||||
// The execute method is supposed to return a tuple where the first
|
||||
// element is an error message in case of failure, and the second
|
||||
// element is the output of the command.
|
||||
@override
|
||||
(String?, String) execute(MyGame game, List<String> args) {
|
||||
// do something on the game
|
||||
return (null, 'Hello World');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then when creating the `ConsoleView` widget, add the custom command to the `customCommands` list.
|
||||
|
||||
```dart
|
||||
ConsoleView(
|
||||
game: game,
|
||||
onClose: () {
|
||||
_game.overlays.remove('console');
|
||||
},
|
||||
customCommands: [MyCustomCommand()],
|
||||
),
|
||||
```
|
||||
|
||||
|
||||
## Customizing the console UI
|
||||
|
||||
The console look and feel can also be customized. When creating the `ConsoleView` widget, there are
|
||||
a couple of properties that can be used to customize it:
|
||||
|
||||
- `containerBuilder`: It is used to created the decorated container where the history and the
|
||||
command input is displayed.
|
||||
- `cursorBuilder`: It is used to create the cursor widget.
|
||||
- `historyBuilder`: It is used to create the scrolling element of the history, by default a simple
|
||||
`SingleChildScrollView` is used.
|
||||
- `cursorColor`: The color of the cursor. Can be used when just wanting to change the color
|
||||
of the cursor.
|
||||
- `textStyle`: The text style of the console.
|
||||
|
||||
29
packages/flame_console/.gitignore
vendored
Normal file
29
packages/flame_console/.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
||||
10
packages/flame_console/.metadata
Normal file
10
packages/flame_console/.metadata
Normal file
@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "5874a72aa4c779a02553007c47dacbefba2374dc"
|
||||
channel: "stable"
|
||||
|
||||
project_type: package
|
||||
3
packages/flame_console/CHANGELOG.md
Normal file
3
packages/flame_console/CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
## 0.1.0
|
||||
|
||||
* First release
|
||||
22
packages/flame_console/LICENSE
Normal file
22
packages/flame_console/LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Blue Fire
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
7
packages/flame_console/README.md
Normal file
7
packages/flame_console/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Flame Console 💻
|
||||
|
||||
Terminal overlay for Flame games which allows developers to debug and interact with their running games.
|
||||
|
||||
Check out the documentation
|
||||
[here](https://docs.flame-engine.org/latest/bridge_packages/flame_console/flame_console.html) for
|
||||
more information.
|
||||
1
packages/flame_console/analysis_options.yaml
Normal file
1
packages/flame_console/analysis_options.yaml
Normal file
@ -0,0 +1 @@
|
||||
include: package:flame_lint/analysis_options_with_dcm.yaml
|
||||
45
packages/flame_console/example/.gitignore
vendored
Normal file
45
packages/flame_console/example/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
devtools_options.yaml
|
||||
30
packages/flame_console/example/.metadata
Normal file
30
packages/flame_console/example/.metadata
Normal file
@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "5874a72aa4c779a02553007c47dacbefba2374dc"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc
|
||||
base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc
|
||||
- platform: web
|
||||
create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc
|
||||
base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
3
packages/flame_console/example/README.md
Normal file
3
packages/flame_console/example/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# flame_console example
|
||||
|
||||
Example of using the flame console package
|
||||
1
packages/flame_console/example/analysis_options.yaml
Normal file
1
packages/flame_console/example/analysis_options.yaml
Normal file
@ -0,0 +1 @@
|
||||
include: package:flame_lint/analysis_options_with_dcm.yaml
|
||||
90
packages/flame_console/example/lib/game.dart
Normal file
90
packages/flame_console/example/lib/game.dart
Normal file
@ -0,0 +1,90 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/palette.dart';
|
||||
|
||||
class MyGame extends FlameGame with HasKeyboardHandlerComponents {
|
||||
@override
|
||||
FutureOr<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
world.addAll([
|
||||
RectangleComponent(
|
||||
position: Vector2(100, 0),
|
||||
size: Vector2(100, 100),
|
||||
paint: BasicPalette.white.paint(),
|
||||
children: [
|
||||
RectangleHitbox.relative(
|
||||
Vector2.all(0.8),
|
||||
parentSize: Vector2(100, 100),
|
||||
),
|
||||
SequenceEffect(
|
||||
[
|
||||
MoveEffect.by(
|
||||
Vector2(-200, 0),
|
||||
LinearEffectController(1),
|
||||
),
|
||||
MoveEffect.by(
|
||||
Vector2(200, 0),
|
||||
LinearEffectController(1),
|
||||
),
|
||||
],
|
||||
infinite: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
RectangleComponent(
|
||||
position: Vector2(200, 100),
|
||||
size: Vector2(100, 100),
|
||||
paint: BasicPalette.white.paint(),
|
||||
children: [
|
||||
RectangleHitbox.relative(
|
||||
Vector2.all(0.4),
|
||||
parentSize: Vector2(100, 100),
|
||||
),
|
||||
SequenceEffect(
|
||||
[
|
||||
MoveEffect.by(
|
||||
Vector2(-200, 0),
|
||||
LinearEffectController(1),
|
||||
),
|
||||
MoveEffect.by(
|
||||
Vector2(200, 0),
|
||||
LinearEffectController(1),
|
||||
),
|
||||
],
|
||||
infinite: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
RectangleComponent(
|
||||
position: Vector2(300, 200),
|
||||
size: Vector2(100, 100),
|
||||
paint: BasicPalette.white.paint(),
|
||||
children: [
|
||||
RectangleHitbox.relative(
|
||||
Vector2.all(0.2),
|
||||
parentSize: Vector2(100, 100),
|
||||
),
|
||||
SequenceEffect(
|
||||
[
|
||||
MoveEffect.by(
|
||||
Vector2(-200, 0),
|
||||
LinearEffectController(1),
|
||||
),
|
||||
MoveEffect.by(
|
||||
Vector2(200, 0),
|
||||
LinearEffectController(1),
|
||||
),
|
||||
],
|
||||
infinite: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
packages/flame_console/example/lib/main.dart
Normal file
50
packages/flame_console/example/lib/main.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
import 'package:flame_console_example/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MaterialApp(home: MyGameApp()));
|
||||
}
|
||||
|
||||
class MyGameApp extends StatefulWidget {
|
||||
const MyGameApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyGameApp> createState() => _MyGameAppState();
|
||||
}
|
||||
|
||||
class _MyGameAppState extends State<MyGameApp> {
|
||||
late final MyGame _game;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_game = MyGame();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: GameWidget(
|
||||
game: _game,
|
||||
overlayBuilderMap: {
|
||||
'console': (BuildContext context, MyGame game) => ConsoleView(
|
||||
game: game,
|
||||
onClose: () {
|
||||
_game.overlays.remove('console');
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'console_button',
|
||||
onPressed: () {
|
||||
_game.overlays.add('console');
|
||||
},
|
||||
child: const Icon(Icons.developer_mode),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
packages/flame_console/example/pubspec.yaml
Normal file
23
packages/flame_console/example/pubspec.yaml
Normal file
@ -0,0 +1,23 @@
|
||||
name: flame_console_example
|
||||
description: "Example of using the flame console package"
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=3.4.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flame: ^1.19.0
|
||||
flame_console: ^0.1.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flame_lint: ^1.2.1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
1
packages/flame_console/lib/flame_console.dart
Normal file
1
packages/flame_console/lib/flame_console.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'src/flame_console.dart';
|
||||
111
packages/flame_console/lib/src/commands/commands.dart
Normal file
111
packages/flame_console/lib/src/commands/commands.dart
Normal file
@ -0,0 +1,111 @@
|
||||
import 'package:args/args.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/src/commands/commands.dart';
|
||||
import 'package:flame_console/src/commands/pause_command.dart';
|
||||
import 'package:flame_console/src/commands/resume_command.dart';
|
||||
|
||||
export 'debug_command.dart';
|
||||
export 'ls_command.dart';
|
||||
export 'remove_command.dart';
|
||||
|
||||
abstract class ConsoleCommand<G extends FlameGame> {
|
||||
ArgParser get parser;
|
||||
String get description;
|
||||
String get name;
|
||||
|
||||
List<Component> listAllChildren(Component component) {
|
||||
return [
|
||||
for (final child in component.children) ...[
|
||||
child,
|
||||
...listAllChildren(child),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
void onChildMatch(
|
||||
void Function(Component) onChild, {
|
||||
required Component rootComponent,
|
||||
List<String> ids = const [],
|
||||
List<String> types = const [],
|
||||
int? limit,
|
||||
}) {
|
||||
final components = listAllChildren(rootComponent);
|
||||
|
||||
var count = 0;
|
||||
|
||||
for (final element in components) {
|
||||
if (limit != null && count >= limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
final isIdMatch =
|
||||
ids.isEmpty || ids.contains(element.hashCode.toString());
|
||||
final isTypeMatch =
|
||||
types.isEmpty || types.contains(element.runtimeType.toString());
|
||||
|
||||
if (isIdMatch && isTypeMatch) {
|
||||
count++;
|
||||
onChild(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(String?, String) run(G game, List<String> args) {
|
||||
final results = parser.parse(args);
|
||||
return execute(game, results);
|
||||
}
|
||||
|
||||
(String?, String) execute(G game, ArgResults results);
|
||||
|
||||
int? optionalIntResult(String key, ArgResults results) {
|
||||
if (results[key] != null) {
|
||||
return int.tryParse(results[key] as String);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class QueryCommand<G extends FlameGame> extends ConsoleCommand<G> {
|
||||
(String?, String) processChildren(List<Component> children);
|
||||
|
||||
@override
|
||||
(String?, String) execute(G game, ArgResults results) {
|
||||
final children = <Component>[];
|
||||
|
||||
onChildMatch(
|
||||
children.add,
|
||||
rootComponent: game,
|
||||
ids: results['id'] as List<String>? ?? [],
|
||||
types: results['type'] as List<String>? ?? [],
|
||||
limit: optionalIntResult('limit', results),
|
||||
);
|
||||
|
||||
return processChildren(children);
|
||||
}
|
||||
|
||||
@override
|
||||
ArgParser get parser => ArgParser()
|
||||
..addMultiOption(
|
||||
'id',
|
||||
abbr: 'i',
|
||||
)
|
||||
..addMultiOption(
|
||||
'type',
|
||||
abbr: 't',
|
||||
)
|
||||
..addOption(
|
||||
'limit',
|
||||
abbr: 'l',
|
||||
);
|
||||
}
|
||||
|
||||
class ConsoleCommands {
|
||||
static List<ConsoleCommand> commands = [
|
||||
LsConsoleCommand(),
|
||||
RemoveConsoleCommand(),
|
||||
DebugConsoleCommand(),
|
||||
PauseConsoleCommand(),
|
||||
ResumeConsoleCommand(),
|
||||
];
|
||||
}
|
||||
19
packages/flame_console/lib/src/commands/debug_command.dart
Normal file
19
packages/flame_console/lib/src/commands/debug_command.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/src/commands/commands.dart';
|
||||
|
||||
class DebugConsoleCommand<G extends FlameGame> extends QueryCommand<G> {
|
||||
@override
|
||||
(String?, String) processChildren(List<Component> children) {
|
||||
for (final child in children) {
|
||||
child.debugMode = !child.debugMode;
|
||||
}
|
||||
return (null, '');
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'debug';
|
||||
|
||||
@override
|
||||
String get description => 'Toggle debug mode on the matched components.';
|
||||
}
|
||||
22
packages/flame_console/lib/src/commands/ls_command.dart
Normal file
22
packages/flame_console/lib/src/commands/ls_command.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
|
||||
class LsConsoleCommand<G extends FlameGame> extends QueryCommand<G> {
|
||||
@override
|
||||
(String?, String) processChildren(List<Component> children) {
|
||||
final out = StringBuffer();
|
||||
for (final component in children) {
|
||||
final componentType = component.runtimeType.toString();
|
||||
out.writeln('${component.hashCode}@$componentType');
|
||||
}
|
||||
|
||||
return (null, out.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'ls';
|
||||
|
||||
@override
|
||||
String get description => 'List components that match the query arguments.';
|
||||
}
|
||||
27
packages/flame_console/lib/src/commands/pause_command.dart
Normal file
27
packages/flame_console/lib/src/commands/pause_command.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:args/args.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
|
||||
class PauseConsoleCommand<G extends FlameGame> extends ConsoleCommand<G> {
|
||||
@override
|
||||
(String?, String) execute(G game, ArgResults results) {
|
||||
if (game.paused) {
|
||||
return (
|
||||
'Game is already paused, use the resume command start it again',
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
game.pauseEngine();
|
||||
return (null, '');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ArgParser get parser => ArgParser();
|
||||
|
||||
@override
|
||||
String get name => 'pause';
|
||||
|
||||
@override
|
||||
String get description => 'Pauses the game loop.';
|
||||
}
|
||||
20
packages/flame_console/lib/src/commands/remove_command.dart
Normal file
20
packages/flame_console/lib/src/commands/remove_command.dart
Normal file
@ -0,0 +1,20 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
|
||||
class RemoveConsoleCommand<G extends FlameGame> extends QueryCommand<G> {
|
||||
@override
|
||||
(String?, String) processChildren(List<Component> children) {
|
||||
for (final component in children) {
|
||||
component.removeFromParent();
|
||||
}
|
||||
return (null, '');
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'rm';
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
'Removes components that match the query arguments.';
|
||||
}
|
||||
24
packages/flame_console/lib/src/commands/resume_command.dart
Normal file
24
packages/flame_console/lib/src/commands/resume_command.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:args/args.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
|
||||
class ResumeConsoleCommand<G extends FlameGame> extends ConsoleCommand<G> {
|
||||
@override
|
||||
(String?, String) execute(G game, ArgResults results) {
|
||||
if (!game.paused) {
|
||||
return ('Game is not paused, use the pause command to pause it', '');
|
||||
} else {
|
||||
game.resumeEngine();
|
||||
return (null, '');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ArgParser get parser => ArgParser();
|
||||
|
||||
@override
|
||||
String get name => 'resume';
|
||||
|
||||
@override
|
||||
String get description => 'Resumes the game loop.';
|
||||
}
|
||||
172
packages/flame_console/lib/src/controller.dart
Normal file
172
packages/flame_console/lib/src/controller.dart
Normal file
@ -0,0 +1,172 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ConsoleState {
|
||||
const ConsoleState({
|
||||
this.showHistory = false,
|
||||
this.commandHistoryIndex = 0,
|
||||
this.commandHistory = const [],
|
||||
this.history = const [],
|
||||
this.cmd = '',
|
||||
});
|
||||
|
||||
final bool showHistory;
|
||||
final int commandHistoryIndex;
|
||||
final List<String> commandHistory;
|
||||
final List<String> history;
|
||||
final String cmd;
|
||||
|
||||
ConsoleState copyWith({
|
||||
bool? showHistory,
|
||||
int? commandHistoryIndex,
|
||||
List<String>? commandHistory,
|
||||
List<String>? history,
|
||||
String? cmd,
|
||||
}) {
|
||||
return ConsoleState(
|
||||
showHistory: showHistory ?? this.showHistory,
|
||||
commandHistoryIndex: commandHistoryIndex ?? this.commandHistoryIndex,
|
||||
commandHistory: commandHistory ?? this.commandHistory,
|
||||
history: history ?? this.history,
|
||||
cmd: cmd ?? this.cmd,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConsoleController<G extends FlameGame> {
|
||||
ConsoleController({
|
||||
required this.repository,
|
||||
required this.game,
|
||||
required this.scrollController,
|
||||
required this.onClose,
|
||||
required this.commands,
|
||||
ConsoleState state = const ConsoleState(),
|
||||
}) : state = ValueNotifier(state);
|
||||
|
||||
final ValueNotifier<ConsoleState> state;
|
||||
final ConsoleRepository repository;
|
||||
final G game;
|
||||
final VoidCallback onClose;
|
||||
final ScrollController scrollController;
|
||||
final Map<String, ConsoleCommand<G>> commands;
|
||||
|
||||
Future<void> init() async {
|
||||
final history = await repository.listCommandHistory();
|
||||
state.value = state.value.copyWith(history: history);
|
||||
}
|
||||
|
||||
void handleKeyEvent(KeyEvent event) {
|
||||
if (event is KeyUpEvent) {
|
||||
return;
|
||||
}
|
||||
final char = event.character;
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape &&
|
||||
!state.value.showHistory) {
|
||||
onClose();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp &&
|
||||
!state.value.showHistory) {
|
||||
final newState = state.value.copyWith(
|
||||
showHistory: true,
|
||||
commandHistoryIndex: state.value.commandHistory.length - 1,
|
||||
);
|
||||
state.value = newState;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter &&
|
||||
state.value.showHistory) {
|
||||
final newState = state.value.copyWith(
|
||||
cmd: state.value.commandHistory[state.value.commandHistoryIndex],
|
||||
showHistory: false,
|
||||
);
|
||||
state.value = newState;
|
||||
} else if ((event.logicalKey == LogicalKeyboardKey.arrowUp ||
|
||||
event.logicalKey == LogicalKeyboardKey.arrowDown) &&
|
||||
state.value.showHistory) {
|
||||
final newState = state.value.copyWith(
|
||||
commandHistoryIndex: event.logicalKey == LogicalKeyboardKey.arrowUp
|
||||
? (state.value.commandHistoryIndex - 1)
|
||||
.clamp(0, state.value.commandHistory.length - 1)
|
||||
: (state.value.commandHistoryIndex + 1)
|
||||
.clamp(0, state.value.commandHistory.length - 1),
|
||||
);
|
||||
state.value = newState;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape &&
|
||||
state.value.showHistory) {
|
||||
state.value = state.value.copyWith(
|
||||
showHistory: false,
|
||||
);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter &&
|
||||
!state.value.showHistory) {
|
||||
final split = state.value.cmd.split(' ');
|
||||
|
||||
if (split.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (split.first == 'clear') {
|
||||
state.value = state.value.copyWith(
|
||||
history: [],
|
||||
cmd: '',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (split.first == 'help') {
|
||||
final output = commands.entries.fold('', (previous, entry) {
|
||||
final help = '${entry.key} - ${entry.value.description}\n\n'
|
||||
'${entry.value.parser.usage}\n\n';
|
||||
|
||||
return '$previous\n$help';
|
||||
});
|
||||
|
||||
state.value = state.value.copyWith(
|
||||
history: [...state.value.history, output],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final originalCommand = state.value.cmd;
|
||||
state.value = state.value.copyWith(
|
||||
history: [...state.value.history, state.value.cmd],
|
||||
cmd: '',
|
||||
);
|
||||
|
||||
final command = commands[split.first];
|
||||
|
||||
if (command == null) {
|
||||
state.value = state.value.copyWith(
|
||||
history: [...state.value.history, 'Command not found'],
|
||||
);
|
||||
} else {
|
||||
repository.addToCommandHistory(originalCommand);
|
||||
state.value = state.value.copyWith(
|
||||
commandHistory: [...state.value.commandHistory, originalCommand],
|
||||
);
|
||||
final result = command.run(game, split.skip(1).toList());
|
||||
|
||||
if (result.$1 != null) {
|
||||
state.value = state.value.copyWith(
|
||||
history: [...state.value.history, ...result.$1!.split('\n')],
|
||||
);
|
||||
} else if (result.$2.isNotEmpty) {
|
||||
state.value = state.value.copyWith(
|
||||
history: [...state.value.history, ...result.$2.split('\n')],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
});
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
state.value = state.value.copyWith(
|
||||
cmd: state.value.cmd.substring(0, state.value.cmd.length - 1),
|
||||
);
|
||||
} else if (char != null) {
|
||||
state.value = state.value.copyWith(
|
||||
cmd: state.value.cmd + char,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/flame_console/lib/src/flame_console.dart
Normal file
3
packages/flame_console/lib/src/flame_console.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'commands/commands.dart';
|
||||
export 'repository/repository.dart';
|
||||
export 'view/view.dart';
|
||||
@ -0,0 +1,7 @@
|
||||
/// A repository to persist and read history of commands.
|
||||
abstract class ConsoleRepository {
|
||||
const ConsoleRepository();
|
||||
|
||||
Future<void> addToCommandHistory(String command);
|
||||
Future<List<String>> listCommandHistory();
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
|
||||
/// An implementation of a [ConsoleRepository] that stores the command history
|
||||
/// in memory.
|
||||
class MemoryConsoleRepository extends ConsoleRepository {
|
||||
const MemoryConsoleRepository({
|
||||
List<String> commands = const [],
|
||||
}) : _commands = commands;
|
||||
|
||||
final List<String> _commands;
|
||||
|
||||
@override
|
||||
Future<void> addToCommandHistory(String command) async {
|
||||
_commands.add(command);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> listCommandHistory() async {
|
||||
return _commands;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export 'console_repository.dart';
|
||||
export 'memory_console_repository.dart';
|
||||
235
packages/flame_console/lib/src/view/console_view.dart
Normal file
235
packages/flame_console/lib/src/view/console_view.dart
Normal file
@ -0,0 +1,235 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
import 'package:flame_console/src/controller.dart';
|
||||
import 'package:flame_console/src/view/container_builder.dart';
|
||||
import 'package:flame_console/src/view/cursor_builder.dart';
|
||||
import 'package:flame_console/src/view/history_builder.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef HistoryBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
ScrollController scrollController,
|
||||
Widget child,
|
||||
);
|
||||
|
||||
typedef ContainerBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
);
|
||||
|
||||
/// A Console like view that can be used to interact with a game.
|
||||
///
|
||||
/// It should be registered as an overlay in the game widget
|
||||
/// of the game you want to interact with.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```dart
|
||||
/// GameWidget(
|
||||
/// game: _game,
|
||||
/// overlayBuilderMap: {
|
||||
/// 'console': (BuildContext context, MyGame game) => ConsoleView(
|
||||
/// game: game,
|
||||
/// onClose: () {
|
||||
/// _game.overlays.remove('console');
|
||||
/// },
|
||||
/// ),
|
||||
/// },
|
||||
/// )
|
||||
class ConsoleView<G extends FlameGame> extends StatefulWidget {
|
||||
const ConsoleView({
|
||||
required this.game,
|
||||
required this.onClose,
|
||||
this.customCommands,
|
||||
ConsoleRepository? repository,
|
||||
this.containerBuilder,
|
||||
this.cursorBuilder,
|
||||
this.cursorColor,
|
||||
this.historyBuilder,
|
||||
this.textStyle,
|
||||
@visibleForTesting this.controller,
|
||||
super.key,
|
||||
}) : repository = repository ?? const MemoryConsoleRepository();
|
||||
|
||||
final G game;
|
||||
final List<ConsoleCommand<G>>? customCommands;
|
||||
final VoidCallback onClose;
|
||||
final ConsoleRepository repository;
|
||||
final ConsoleController? controller;
|
||||
|
||||
final ContainerBuilder? containerBuilder;
|
||||
final WidgetBuilder? cursorBuilder;
|
||||
final HistoryBuilder? historyBuilder;
|
||||
|
||||
final Color? cursorColor;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
State<ConsoleView> createState() => _ConsoleViewState();
|
||||
}
|
||||
|
||||
class _ConsoleKeyboardHandler extends Component with KeyboardHandler {
|
||||
_ConsoleKeyboardHandler(this._onKeyEvent);
|
||||
|
||||
final void Function(KeyEvent) _onKeyEvent;
|
||||
|
||||
@override
|
||||
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
|
||||
_onKeyEvent(event);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsoleViewState extends State<ConsoleView> {
|
||||
late final List<ConsoleCommand> _commandList = [
|
||||
...ConsoleCommands.commands,
|
||||
if (widget.customCommands != null) ...widget.customCommands!,
|
||||
];
|
||||
|
||||
late final Map<String, ConsoleCommand> _commandsMap = {
|
||||
for (final command in _commandList) command.name: command,
|
||||
};
|
||||
|
||||
late final _controller = widget.controller ??
|
||||
ConsoleController(
|
||||
repository: widget.repository,
|
||||
game: widget.game,
|
||||
scrollController: _scrollController,
|
||||
onClose: widget.onClose,
|
||||
commands: _commandsMap,
|
||||
);
|
||||
|
||||
late final _scrollController = ScrollController();
|
||||
late final KeyboardHandler _keyboardHandler;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.game.add(
|
||||
_keyboardHandler = _ConsoleKeyboardHandler(
|
||||
_controller.handleKeyEvent,
|
||||
),
|
||||
);
|
||||
|
||||
_controller.init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_keyboardHandler.removeFromParent();
|
||||
_scrollController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cursorColor = widget.cursorColor ?? Colors.white;
|
||||
|
||||
final textStyle = widget.textStyle ??
|
||||
Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
);
|
||||
|
||||
final historyBuilder = widget.historyBuilder ?? defaultHistoryBuilder;
|
||||
final containerBuilder = widget.containerBuilder ?? defaultContainerBuilder;
|
||||
final cursorBuilder = widget.cursorBuilder ?? defaultCursorBuilder;
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _controller.state,
|
||||
builder: (context, state, _) {
|
||||
return SizedBox(
|
||||
height: 400,
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 48,
|
||||
child: containerBuilder(
|
||||
context,
|
||||
historyBuilder(
|
||||
context,
|
||||
_scrollController,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final line in state.history)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(line, style: textStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.showHistory)
|
||||
Positioned(
|
||||
bottom: 48,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: containerBuilder(
|
||||
context,
|
||||
SizedBox(
|
||||
height: 168,
|
||||
child: Column(
|
||||
verticalDirection: VerticalDirection.up,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.commandHistory.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text('No history', style: textStyle),
|
||||
),
|
||||
for (var i = state.commandHistoryIndex;
|
||||
i >= 0 && i >= state.commandHistoryIndex - 5;
|
||||
i--)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: ColoredBox(
|
||||
color: i == state.commandHistoryIndex
|
||||
? cursorColor.withOpacity(.5)
|
||||
: Colors.transparent,
|
||||
child: Text(
|
||||
state.commandHistory[i],
|
||||
style: textStyle?.copyWith(
|
||||
color: i == state.commandHistoryIndex
|
||||
? cursorColor
|
||||
: textStyle.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: containerBuilder(
|
||||
context,
|
||||
Row(
|
||||
children: [
|
||||
Text(state.cmd, style: textStyle),
|
||||
SizedBox(width: (textStyle?.fontSize ?? 12) / 4),
|
||||
cursorBuilder(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
14
packages/flame_console/lib/src/view/container_builder.dart
Normal file
14
packages/flame_console/lib/src/view/container_builder.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget defaultContainerBuilder(BuildContext context, Widget child) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.8),
|
||||
border: Border.all(color: Colors.white),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
11
packages/flame_console/lib/src/view/cursor_builder.dart
Normal file
11
packages/flame_console/lib/src/view/cursor_builder.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget defaultCursorBuilder(BuildContext context) {
|
||||
return const ColoredBox(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
height: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
12
packages/flame_console/lib/src/view/history_builder.dart
Normal file
12
packages/flame_console/lib/src/view/history_builder.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget defaultHistoryBuilder(
|
||||
BuildContext context,
|
||||
ScrollController scrollController,
|
||||
Widget child,
|
||||
) {
|
||||
return SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
1
packages/flame_console/lib/src/view/view.dart
Normal file
1
packages/flame_console/lib/src/view/view.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'console_view.dart';
|
||||
20
packages/flame_console/pubspec.yaml
Normal file
20
packages/flame_console/pubspec.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
name: flame_console
|
||||
description: "An extensible and customizable console to help debug Flame games."
|
||||
version: 0.1.0
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
dependencies:
|
||||
args: ^2.5.0
|
||||
flame: ^1.19.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flame_lint: ^1.2.1
|
||||
flame_test: ^1.17.1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
139
packages/flame_console/test/src/commands_test.dart
Normal file
139
packages/flame_console/test/src/commands_test.dart
Normal file
@ -0,0 +1,139 @@
|
||||
import 'package:args/src/arg_parser.dart';
|
||||
import 'package:args/src/arg_results.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class _NoopCommand extends ConsoleCommand {
|
||||
@override
|
||||
String get description => '';
|
||||
|
||||
@override
|
||||
String get name => '';
|
||||
|
||||
@override
|
||||
(String?, String) execute(FlameGame<World> game, ArgResults results) {
|
||||
return (null, '');
|
||||
}
|
||||
|
||||
@override
|
||||
ArgParser get parser => ArgParser();
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Commands', () {
|
||||
testWithGame(
|
||||
'listAllChildren crawls on all children',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
await game.world.add(
|
||||
RectangleComponent(
|
||||
children: [
|
||||
PositionComponent(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await game.ready();
|
||||
|
||||
final command = _NoopCommand();
|
||||
final components = command.listAllChildren(game.world);
|
||||
|
||||
expect(components, hasLength(2));
|
||||
expect(components[0], isA<RectangleComponent>());
|
||||
expect(components[1], isA<PositionComponent>());
|
||||
},
|
||||
);
|
||||
|
||||
group('onChildMatch', () {
|
||||
testWithGame(
|
||||
'match children with the given types',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
await game.world.addAll([
|
||||
RectangleComponent(
|
||||
children: [
|
||||
PositionComponent(),
|
||||
],
|
||||
),
|
||||
PositionComponent(),
|
||||
]);
|
||||
|
||||
await game.ready();
|
||||
|
||||
final command = _NoopCommand();
|
||||
final components = <Component>[];
|
||||
command.onChildMatch(
|
||||
components.add,
|
||||
rootComponent: game.world,
|
||||
types: ['PositionComponent'],
|
||||
);
|
||||
|
||||
expect(components, hasLength(2));
|
||||
expect(components[0], isA<PositionComponent>());
|
||||
expect(components[1], isA<PositionComponent>());
|
||||
},
|
||||
);
|
||||
|
||||
testWithGame(
|
||||
'match children with the given types and limit',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
await game.world.addAll([
|
||||
RectangleComponent(
|
||||
children: [
|
||||
PositionComponent(),
|
||||
],
|
||||
),
|
||||
PositionComponent(),
|
||||
]);
|
||||
|
||||
await game.ready();
|
||||
|
||||
final command = _NoopCommand();
|
||||
final components = <Component>[];
|
||||
command.onChildMatch(
|
||||
components.add,
|
||||
rootComponent: game.world,
|
||||
types: ['PositionComponent'],
|
||||
limit: 1,
|
||||
);
|
||||
|
||||
expect(components, hasLength(1));
|
||||
expect(components[0], isA<PositionComponent>());
|
||||
},
|
||||
);
|
||||
|
||||
testWithGame(
|
||||
'match children with the given id',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
late Component target;
|
||||
await game.world.addAll([
|
||||
target = RectangleComponent(
|
||||
children: [
|
||||
PositionComponent(),
|
||||
],
|
||||
),
|
||||
PositionComponent(),
|
||||
]);
|
||||
|
||||
await game.ready();
|
||||
|
||||
final command = _NoopCommand();
|
||||
final components = <Component>[];
|
||||
command.onChildMatch(
|
||||
components.add,
|
||||
rootComponent: game.world,
|
||||
ids: [target.hashCode.toString()],
|
||||
);
|
||||
|
||||
expect(components, hasLength(1));
|
||||
expect(components[0], isA<PositionComponent>());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
35
packages/flame_console/test/src/debug_command_test.dart
Normal file
35
packages/flame_console/test/src/debug_command_test.dart
Normal file
@ -0,0 +1,35 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/flame_console.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('Debug Command', () {
|
||||
final components = [
|
||||
RectangleComponent(),
|
||||
PositionComponent(),
|
||||
];
|
||||
testWithGame(
|
||||
'toggle debug mode on components',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
await game.world.addAll(components);
|
||||
|
||||
await game.ready();
|
||||
|
||||
final command = DebugConsoleCommand();
|
||||
command.execute(game, command.parser.parse([]));
|
||||
|
||||
for (final component in components) {
|
||||
expect(component.debugMode, isTrue);
|
||||
}
|
||||
|
||||
command.execute(game, command.parser.parse([]));
|
||||
for (final component in components) {
|
||||
expect(component.debugMode, isFalse);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
38
packages/flame_console/test/src/pause_command_test.dart
Normal file
38
packages/flame_console/test/src/pause_command_test.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/src/commands/pause_command.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('Pause Command', () {
|
||||
testWithGame(
|
||||
'pauses the game',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
expect(game.paused, isFalse);
|
||||
final command = PauseConsoleCommand();
|
||||
command.execute(game, command.parser.parse([]));
|
||||
expect(game.paused, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
group('when the game is already paused', () {
|
||||
testWithGame(
|
||||
'returns error',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
game.pauseEngine();
|
||||
expect(game.paused, isTrue);
|
||||
final command = PauseConsoleCommand();
|
||||
final result = command.execute(game, command.parser.parse([]));
|
||||
expect(game.paused, isTrue);
|
||||
|
||||
expect(
|
||||
result.$1,
|
||||
'Game is already paused, use the resume command start it again',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
38
packages/flame_console/test/src/resume_command_test.dart
Normal file
38
packages/flame_console/test/src/resume_command_test.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_console/src/commands/resume_command.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('Resume Command', () {
|
||||
testWithGame(
|
||||
'resumes the game',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
game.pauseEngine();
|
||||
expect(game.paused, isTrue);
|
||||
final command = ResumeConsoleCommand();
|
||||
command.execute(game, command.parser.parse([]));
|
||||
expect(game.paused, isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
group('when the game is not paused', () {
|
||||
testWithGame(
|
||||
'returns error',
|
||||
FlameGame.new,
|
||||
(game) async {
|
||||
expect(game.paused, isFalse);
|
||||
final command = ResumeConsoleCommand();
|
||||
final result = command.execute(game, command.parser.parse([]));
|
||||
expect(game.paused, isFalse);
|
||||
|
||||
expect(
|
||||
result.$1,
|
||||
'Game is not paused, use the pause command to pause it',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user